1 /* ============================================================
2  *
3  * This file is a part of digiKam project
4  * https://www.digikam.org
5  *
6  * Date        : 2008-02-26
7  * Description : Upper widget in the search sidebar
8  *
9  * Copyright (C) 2008-2012 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
10  *
11  * This program is free software; you can redistribute it
12  * and/or modify it under the terms of the GNU General
13  * Public License as published by the Free Software Foundation;
14  * either version 2, or (at your option)
15  * any later version.
16  *
17  * This program is distributed in the hope that it will be useful,
18  * but WITHOUT ANY WARRANTY; without even the implied warranty of
19  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20  * GNU General Public License for more details.
21  *
22  * ============================================================ */
23 
24 #include "searchtabheader.h"
25 
26 // Qt includes
27 
28 #include <QGroupBox>
29 #include <QHBoxLayout>
30 #include <QLabel>
31 #include <QPushButton>
32 #include <QStackedLayout>
33 #include <QTimer>
34 #include <QToolButton>
35 #include <QVBoxLayout>
36 #include <QApplication>
37 #include <QStyle>
38 #include <QLineEdit>
39 #include <QInputDialog>
40 #include <QIcon>
41 #include <QMenu>
42 #include <QContextMenuEvent>
43 
44 // KDE includes
45 
46 #include <klocalizedstring.h>
47 #include <kconfiggroup.h>
48 #include <ksharedconfig.h>
49 
50 // Local includes
51 
52 #include "digikam_debug.h"
53 #include "album.h"
54 #include "albummanager.h"
55 #include "searchfolderview.h"
56 #include "searchwindow.h"
57 #include "coredbsearchxml.h"
58 #include "dexpanderbox.h"
59 
60 namespace Digikam
61 {
62 
63 class Q_DECL_HIDDEN KeywordLineEdit : public QLineEdit
64 {
65     Q_OBJECT
66 
67 public:
68 
KeywordLineEdit(QWidget * const parent=nullptr)69     explicit KeywordLineEdit(QWidget* const parent = nullptr)
70         : QLineEdit    (parent),
71           m_hasAdvanced(false)
72     {
73         KSharedConfig::Ptr config = KSharedConfig::openConfig();
74         KConfigGroup group        = config->group(QLatin1String("KeywordSearchEdit Settings"));
75         m_autoSearch              = group.readEntry(QLatin1String("Autostart Search"), true);
76     }
77 
showAdvancedSearch(bool hasAdvanced)78     void showAdvancedSearch(bool hasAdvanced)
79     {
80         if (m_hasAdvanced == hasAdvanced)
81         {
82             return;
83         }
84 
85         m_hasAdvanced = hasAdvanced;
86         adjustStatus(m_hasAdvanced);
87     }
88 
focusInEvent(QFocusEvent * e)89     void focusInEvent(QFocusEvent* e) override
90     {
91         if (m_hasAdvanced)
92         {
93             adjustStatus(false);
94         }
95 
96         QLineEdit::focusInEvent(e);
97     }
98 
focusOutEvent(QFocusEvent * e)99     void focusOutEvent(QFocusEvent* e) override
100     {
101         QLineEdit::focusOutEvent(e);
102 
103         if (m_hasAdvanced)
104         {
105             adjustStatus(true);
106         }
107     }
108 
contextMenuEvent(QContextMenuEvent * e)109     void contextMenuEvent(QContextMenuEvent* e) override
110     {
111         QAction* const action = new QAction(i18nc("@action:inmenu",
112                                                   "Autostart Search"), this);
113         action->setCheckable(true);
114         action->setChecked(m_autoSearch);
115 
116         connect(action, &QAction::triggered,
117                 this, &KeywordLineEdit::toggleAutoSearch);
118 
119         QMenu* const menu = createStandardContextMenu();
120         menu->addSeparator();
121         menu->addAction(action);
122         menu->exec(e->globalPos());
123         delete menu;
124     }
125 
autoSearchEnabled() const126     bool autoSearchEnabled() const
127     {
128         return m_autoSearch;
129     }
130 
adjustStatus(bool adv)131     void adjustStatus(bool adv)
132     {
133         if (adv)
134         {
135             QPalette p = palette();
136             p.setColor(QPalette::Text, p.color(QPalette::Disabled, QPalette::Text));
137             setPalette(p);
138 
139             setText(i18n("(Advanced Search)"));
140         }
141         else
142         {
143             setPalette(QPalette());
144 
145             if (text() == i18n("(Advanced Search)"))
146             {
147                 setText(QString());
148             }
149         }
150     }
151 
152 public Q_SLOTS:
153 
toggleAutoSearch()154     void toggleAutoSearch()
155     {
156         m_autoSearch              = !m_autoSearch;
157 
158         KSharedConfig::Ptr config = KSharedConfig::openConfig();
159         KConfigGroup group        = config->group(QLatin1String("KeywordSearchEdit Settings"));
160         group.writeEntry(QLatin1String("Autostart Search"), m_autoSearch);
161     }
162 
163 protected:
164 
165     bool m_hasAdvanced;
166     bool m_autoSearch;
167 };
168 
169 // -------------------------------------------------------------------------
170 
171 class Q_DECL_HIDDEN SearchTabHeader::Private
172 {
173 public:
174 
Private()175     explicit Private()
176       : newSearchWidget         (nullptr),
177         saveAsWidget            (nullptr),
178         editSimpleWidget        (nullptr),
179         editAdvancedWidget      (nullptr),
180         lowerArea               (nullptr),
181         keywordEdit             (nullptr),
182         advancedEditLabel       (nullptr),
183         saveNameEdit            (nullptr),
184         saveButton              (nullptr),
185         storedKeywordEditName   (nullptr),
186         storedKeywordEdit       (nullptr),
187         storedAdvancedEditName  (nullptr),
188         storedAdvancedEditLabel (nullptr),
189         keywordEditTimer        (nullptr),
190         storedKeywordEditTimer  (nullptr),
191         searchWindow            (nullptr),
192         currentAlbum            (nullptr)
193     {
194     }
195 
196     QGroupBox*          newSearchWidget;
197     QGroupBox*          saveAsWidget;
198     QGroupBox*          editSimpleWidget;
199     QGroupBox*          editAdvancedWidget;
200 
201     QStackedLayout*     lowerArea;
202 
203     KeywordLineEdit*    keywordEdit;
204     QPushButton*        advancedEditLabel;
205 
206     QLineEdit*          saveNameEdit;
207     QToolButton*        saveButton;
208 
209     DAdjustableLabel*   storedKeywordEditName;
210     QLineEdit*          storedKeywordEdit;
211     DAdjustableLabel*   storedAdvancedEditName;
212     QPushButton*        storedAdvancedEditLabel;
213 
214     QTimer*             keywordEditTimer;
215     QTimer*             storedKeywordEditTimer;
216 
217     SearchWindow*       searchWindow;
218 
219     SAlbum*             currentAlbum;
220 
221     QString             oldKeywordContent;
222     QString             oldStoredKeywordContent;
223 };
224 
SearchTabHeader(QWidget * const parent)225 SearchTabHeader::SearchTabHeader(QWidget* const parent)
226     : QWidget(parent),
227       d      (new Private)
228 {
229     const int spacing = QApplication::style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing);
230 
231     QVBoxLayout* const mainLayout = new QVBoxLayout(this);
232     mainLayout->setContentsMargins(QMargins());
233     setLayout(mainLayout);
234 
235     // upper part
236 
237     d->newSearchWidget      = new QGroupBox(this);
238     mainLayout->addWidget(d->newSearchWidget);
239 
240     // lower part
241 
242     d->lowerArea            = new QStackedLayout;
243     mainLayout->addLayout(d->lowerArea);
244 
245     d->saveAsWidget         = new QGroupBox(this);
246     d->editSimpleWidget     = new QGroupBox(this);
247     d->editAdvancedWidget   = new QGroupBox(this);
248     d->lowerArea->addWidget(d->saveAsWidget);
249     d->lowerArea->addWidget(d->editSimpleWidget);
250     d->lowerArea->addWidget(d->editAdvancedWidget);
251 
252     // ------------------- //
253 
254     // upper part
255 
256     d->newSearchWidget->setTitle(i18n("New Search"));
257     QGridLayout* const grid1  = new QGridLayout;
258     QLabel* const searchLabel = new QLabel(i18nc("@label: quick search properties", "Search:"), this);
259     d->keywordEdit            = new KeywordLineEdit(this);
260     d->keywordEdit->setClearButtonEnabled(true);
261     d->keywordEdit->setPlaceholderText(i18n("Enter keywords here..."));
262 
263     d->advancedEditLabel      = new QPushButton(i18n("Advanced Search..."), this);
264 
265     grid1->addWidget(searchLabel,          0, 0, 1, 1);
266     grid1->addWidget(d->keywordEdit,       0, 1, 1, 1);
267     grid1->addWidget(d->advancedEditLabel, 1, 0, 1, 2);
268     grid1->setContentsMargins(spacing, spacing, spacing, spacing);
269     grid1->setSpacing(spacing);
270 
271     d->newSearchWidget->setLayout(grid1);
272 
273     // ------------------- //
274 
275     // lower part, variant 1
276 
277     d->saveAsWidget->setTitle(i18n("Save Current Search"));
278 
279     QHBoxLayout* const hbox1 = new QHBoxLayout;
280     d->saveNameEdit          = new QLineEdit(this);
281     d->saveNameEdit->setWhatsThis(i18n("Enter a name for the current search to save it in the "
282                                        "\"Searches\" view"));
283 
284     d->saveButton            = new QToolButton(this);
285     d->saveButton->setIcon(QIcon::fromTheme(QLatin1String("document-save")));
286     d->saveButton->setToolTip(i18n("Save current search to a new virtual Album"));
287     d->saveButton->setWhatsThis(i18n("If you press this button, the current search "
288                                      "will be saved to a new virtual Search Album using the name "
289                                      "set on the left side."));
290 
291     hbox1->addWidget(d->saveNameEdit);
292     hbox1->addWidget(d->saveButton);
293     hbox1->setContentsMargins(spacing, spacing, spacing, spacing);
294     hbox1->setSpacing(spacing);
295 
296     d->saveAsWidget->setLayout(hbox1);
297 
298     // ------------------- //
299 
300     // lower part, variant 2
301 
302     d->editSimpleWidget->setTitle(i18n("Edit Stored Search"));
303 
304     QVBoxLayout* const vbox1 = new QVBoxLayout;
305     d->storedKeywordEditName = new DAdjustableLabel(this);
306     d->storedKeywordEditName->setElideMode(Qt::ElideRight);
307     d->storedKeywordEdit     = new QLineEdit(this);
308 
309     vbox1->addWidget(d->storedKeywordEditName);
310     vbox1->addWidget(d->storedKeywordEdit);
311     vbox1->setContentsMargins(spacing, spacing, spacing, spacing);
312     vbox1->setSpacing(spacing);
313 
314     d->editSimpleWidget->setLayout(vbox1);
315 
316     // ------------------- //
317 
318     // lower part, variant 3
319 
320     d->editAdvancedWidget->setTitle(i18n("Edit Stored Search"));
321 
322     QVBoxLayout* const vbox2   = new QVBoxLayout;
323 
324     d->storedAdvancedEditName  = new DAdjustableLabel(this);
325     d->storedAdvancedEditName->setElideMode(Qt::ElideRight);
326     d->storedAdvancedEditLabel = new QPushButton(i18n("Edit..."), this);
327 
328     vbox2->addWidget(d->storedAdvancedEditName);
329     vbox2->addWidget(d->storedAdvancedEditLabel);
330     d->editAdvancedWidget->setLayout(vbox2);
331 
332     // ------------------- //
333 
334     // timers
335 
336     d->keywordEditTimer       = new QTimer(this);
337     d->keywordEditTimer->setSingleShot(true);
338     d->keywordEditTimer->setInterval(800);
339 
340     d->storedKeywordEditTimer = new QTimer(this);
341     d->storedKeywordEditTimer->setSingleShot(true);
342     d->storedKeywordEditTimer->setInterval(800);
343 
344     // ------------------- //
345 
346     connect(d->keywordEdit, SIGNAL(textEdited(QString)),
347             d->keywordEditTimer, SLOT(start()));
348 
349     connect(d->keywordEditTimer, SIGNAL(timeout()),
350             this, SLOT(keywordChangedTimer()));
351 
352     connect(d->keywordEdit, SIGNAL(editingFinished()),
353             this, SLOT(keywordChanged()));
354 
355     connect(d->advancedEditLabel, SIGNAL(clicked()),
356             this, SLOT(editCurrentAdvancedSearch()));
357 
358     connect(d->saveNameEdit, SIGNAL(returnPressed()),
359             this, SLOT(saveSearch()));
360 
361     connect(d->saveButton, SIGNAL(clicked()),
362             this, SLOT(saveSearch()));
363 
364     connect(d->storedKeywordEditTimer, SIGNAL(timeout()),
365             this, SLOT(storedKeywordChanged()));
366 
367     connect(d->storedKeywordEdit, SIGNAL(editingFinished()),
368             this, SLOT(storedKeywordChanged()));
369 
370     connect(d->storedAdvancedEditLabel, SIGNAL(clicked()),
371             this, SLOT(editStoredAdvancedSearch()));
372 }
373 
~SearchTabHeader()374 SearchTabHeader::~SearchTabHeader()
375 {
376     delete d->searchWindow;
377     delete d;
378 }
379 
searchWindow() const380 SearchWindow* SearchTabHeader::searchWindow() const
381 {
382     if (!d->searchWindow)
383     {
384         qCDebug(DIGIKAM_GENERAL_LOG) << "Creating search window";
385 
386         // Create the advanced search edit window, deferred from constructor
387 
388         d->searchWindow = new SearchWindow;
389 
390         connect(d->searchWindow, SIGNAL(searchEdited(int,QString)),
391                 this, SLOT(advancedSearchEdited(int,QString)),
392                 Qt::QueuedConnection);
393     }
394 
395     return d->searchWindow;
396 }
397 
selectedSearchChanged(Album * a)398 void SearchTabHeader::selectedSearchChanged(Album* a)
399 {
400     SAlbum* album = dynamic_cast<SAlbum*>(a);
401 
402     // Signal from SearchFolderView that a search has been selected.
403     // Don't check on d->currentAlbum == album, rather update status (which may have changed on same album)
404 
405     d->currentAlbum = album;
406 
407     qCDebug(DIGIKAM_GENERAL_LOG) << "changing to SAlbum " << album;
408 
409     if (!album)
410     {
411         d->lowerArea->setCurrentWidget(d->saveAsWidget);
412         d->lowerArea->setEnabled(false);
413     }
414     else
415     {
416         d->lowerArea->setEnabled(true);
417 
418         if      (album->title() == SAlbum::getTemporaryTitle(DatabaseSearch::AdvancedSearch))
419         {
420             d->lowerArea->setCurrentWidget(d->saveAsWidget);
421 
422             if (album->isKeywordSearch())
423             {
424                 d->keywordEdit->setText(keywordsFromQuery(album->query()));
425                 d->keywordEdit->showAdvancedSearch(false);
426             }
427             else
428             {
429                 d->keywordEdit->showAdvancedSearch(true);
430             }
431         }
432         else if (album->isKeywordSearch())
433         {
434             d->lowerArea->setCurrentWidget(d->editSimpleWidget);
435             d->storedKeywordEditName->setAdjustedText(album->title());
436             d->storedKeywordEdit->setText(keywordsFromQuery(album->query()));
437             d->keywordEdit->showAdvancedSearch(false);
438         }
439         else
440         {
441             d->lowerArea->setCurrentWidget(d->editAdvancedWidget);
442             d->storedAdvancedEditName->setAdjustedText(album->title());
443             d->keywordEdit->showAdvancedSearch(false);
444         }
445     }
446 }
447 
editSearch(SAlbum * album)448 void SearchTabHeader::editSearch(SAlbum* album)
449 {
450     if (!album)
451     {
452         return;
453     }
454 
455     if      (album->isAdvancedSearch())
456     {
457         SearchWindow* window = searchWindow();
458         window->reset();
459         window->readSearch(album->id(), album->query());
460         window->show();
461         window->raise();
462     }
463     else if (album->isKeywordSearch())
464     {
465         d->storedKeywordEdit->selectAll();
466     }
467 }
468 
newKeywordSearch()469 void SearchTabHeader::newKeywordSearch()
470 {
471     d->keywordEdit->clear();
472     QString keywords = d->keywordEdit->text();
473     setCurrentSearch(DatabaseSearch::KeywordSearch, queryFromKeywords(keywords));
474     d->keywordEdit->setFocus();
475 }
476 
newAdvancedSearch()477 void SearchTabHeader::newAdvancedSearch()
478 {
479     SearchWindow* const window = searchWindow();
480     window->reset();
481     window->show();
482     window->raise();
483 }
484 
keywordChanged()485 void SearchTabHeader::keywordChanged()
486 {
487     QString keywords = d->keywordEdit->text();
488     qCDebug(DIGIKAM_GENERAL_LOG) << "keywords changed to '" << keywords << "'";
489 
490     if ((d->oldKeywordContent == keywords) || (keywords.trimmed().isEmpty()))
491     {
492         qCDebug(DIGIKAM_GENERAL_LOG) << "same keywords as before, ignoring...";
493         return;
494     }
495     else
496     {
497         d->oldKeywordContent = keywords;
498     }
499 
500     setCurrentSearch(DatabaseSearch::KeywordSearch, queryFromKeywords(keywords));
501     d->keywordEdit->setFocus();
502 }
503 
keywordChangedTimer()504 void SearchTabHeader::keywordChangedTimer()
505 {
506     if (d->keywordEdit->autoSearchEnabled())
507     {
508         keywordChanged();
509     }
510 }
511 
editCurrentAdvancedSearch()512 void SearchTabHeader::editCurrentAdvancedSearch()
513 {
514     SAlbum* const album        = AlbumManager::instance()->findSAlbum(SAlbum::getTemporaryTitle(DatabaseSearch::AdvancedSearch));
515     SearchWindow* const window = searchWindow();
516 
517     if (album)
518     {
519         window->readSearch(album->id(), album->query());
520     }
521     else
522     {
523         window->reset();
524     }
525 
526     window->show();
527     window->raise();
528 }
529 
saveSearch()530 void SearchTabHeader::saveSearch()
531 {
532     // Only applicable if:
533     // 1. current album is Search View Current Album Save this album as a user names search album.
534     // 2. user as processed a search before to save it.
535 
536     QString name = d->saveNameEdit->text();
537 
538     qCDebug(DIGIKAM_GENERAL_LOG) << "name = " << name;
539 
540     if (name.isEmpty() || !d->currentAlbum)
541     {
542         qCDebug(DIGIKAM_GENERAL_LOG) << "no current album, returning";
543 
544         // passive popup
545 
546         return;
547     }
548 
549     SAlbum* oldAlbum = AlbumManager::instance()->findSAlbum(name);
550 
551     while (oldAlbum)
552     {
553         QString label    = i18n("Search name already exists.\n"
554                                 "Please enter a new name:");
555         bool ok;
556         QString newTitle = QInputDialog::getText(this,
557                                                  i18n("Name exists"),
558                                                  label,
559                                                  QLineEdit::Normal,
560                                                  name,
561                                                  &ok);
562 
563         if (!ok)
564         {
565             return;
566         }
567 
568         name     = newTitle;
569         oldAlbum = AlbumManager::instance()->findSAlbum(name);
570     }
571 
572     SAlbum* newAlbum = AlbumManager::instance()->createSAlbum(name, d->currentAlbum->searchType(),
573                                                               d->currentAlbum->query());
574     emit searchShallBeSelected(QList<Album*>() << newAlbum);
575 }
576 
storedKeywordChanged()577 void SearchTabHeader::storedKeywordChanged()
578 {
579     QString keywords = d->storedKeywordEdit->text();
580 
581     if (d->oldStoredKeywordContent == keywords)
582     {
583         return;
584     }
585     else
586     {
587         d->oldStoredKeywordContent = keywords;
588     }
589 
590     if (d->currentAlbum)
591     {
592         AlbumManager::instance()->updateSAlbum(d->currentAlbum, queryFromKeywords(keywords));
593         emit searchShallBeSelected(QList<Album*>() << d->currentAlbum);
594     }
595 }
596 
editStoredAdvancedSearch()597 void SearchTabHeader::editStoredAdvancedSearch()
598 {
599     if (d->currentAlbum)
600     {
601         SearchWindow* window = searchWindow();
602         window->readSearch(d->currentAlbum->id(), d->currentAlbum->query());
603         window->show();
604         window->raise();
605     }
606 }
607 
advancedSearchEdited(int id,const QString & query)608 void SearchTabHeader::advancedSearchEdited(int id, const QString& query)
609 {
610     // if the user just pressed the button, but did not change anything in the window,
611     // the search is effectively still a keyword search.
612     // We go the hard way and check this case.
613 
614     KeywordSearchReader check(query);
615     DatabaseSearch::Type type = check.isSimpleKeywordSearch() ? DatabaseSearch::KeywordSearch
616                                                               : DatabaseSearch::AdvancedSearch;
617 
618     if (id == -1)
619     {
620         setCurrentSearch(type, query);
621     }
622     else
623     {
624         SAlbum* const album = AlbumManager::instance()->findSAlbum(id);
625 
626         if (album)
627         {
628             AlbumManager::instance()->updateSAlbum(album, query, album->title(), type);
629             emit searchShallBeSelected(QList<Album*>() << album);
630         }
631     }
632 }
633 
setCurrentSearch(DatabaseSearch::Type type,const QString & query,bool selectCurrentAlbum)634 void SearchTabHeader::setCurrentSearch(DatabaseSearch::Type type, const QString& query, bool selectCurrentAlbum)
635 {
636     SAlbum* album = AlbumManager::instance()->findSAlbum(SAlbum::getTemporaryTitle(DatabaseSearch::KeywordSearch));
637 
638     if (album)
639     {
640         AlbumManager::instance()->updateSAlbum(album, query,
641                                                SAlbum::getTemporaryTitle(DatabaseSearch::KeywordSearch),
642                                                type);
643     }
644     else
645     {
646         album = AlbumManager::instance()->createSAlbum(SAlbum::getTemporaryTitle(DatabaseSearch::KeywordSearch),
647                                                        type, query);
648     }
649 
650     if (selectCurrentAlbum)
651     {
652         emit searchShallBeSelected(QList<Album*>() << album);
653     }
654 }
655 
queryFromKeywords(const QString & keywords) const656 QString SearchTabHeader::queryFromKeywords(const QString& keywords) const
657 {
658     QStringList keywordList = KeywordSearch::split(keywords);
659 
660     // create xml
661 
662     KeywordSearchWriter writer;
663 
664     return writer.xml(keywordList);
665 }
666 
keywordsFromQuery(const QString & query) const667 QString SearchTabHeader::keywordsFromQuery(const QString& query) const
668 {
669     KeywordSearchReader reader(query);
670     QStringList keywordList = reader.keywords();
671 
672     return KeywordSearch::merge(keywordList);
673 }
674 
675 } // namespace Digikam
676 
677 #include "searchtabheader.moc"
678