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