1 /* ============================================================
2  *
3  * This file is a part of digiKam project
4  * https://www.digikam.org
5  *
6  * Date        : 2003-03-09
7  * Description : Captions, Tags, and Rating properties editor
8  *
9  * Copyright (C) 2003-2005 by Renchi Raju <renchi dot raju at gmail dot com>
10  * Copyright (C) 2003-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
11  * Copyright (C) 2006-2011 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
12  * Copyright (C) 2009-2011 by Andi Clemens <andi dot clemens at gmail dot com>
13  * Copyright (C) 2009-2011 by Johannes Wienke <languitar at semipol dot de>
14  * Copyright (C) 2015      by Veaceslav Munteanu <veaceslav dot munteanu90 at gmail dot com>
15  *
16  * This program is free software; you can redistribute it
17  * and/or modify it under the terms of the GNU General
18  * Public License as published by the Free Software Foundation;
19  * either version 2, or (at your option)
20  * any later version.
21  *
22  * This program is distributed in the hope that it will be useful,
23  * but WITHOUT ANY WARRANTY; without even the implied warranty of
24  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
25  * GNU General Public License for more details.
26  *
27  * ============================================================ */
28 
29 #include "itemdescedittab.h"
30 
31 // Qt includes
32 
33 #include <QTextEdit>
34 #include <QStyle>
35 #include <QGridLayout>
36 #include <QScrollArea>
37 #include <QTimer>
38 #include <QToolButton>
39 #include <QApplication>
40 #include <QPushButton>
41 #include <QMenu>
42 #include <QIcon>
43 #include <QCheckBox>
44 #include <QMessageBox>
45 #include <QPointer>
46 
47 // KDE includes
48 
49 #include <kconfiggroup.h>
50 #include <klocalizedstring.h>
51 
52 // Local includes
53 
54 #include "digikam_debug.h"
55 #include "addtagslineedit.h"
56 #include "applicationsettings.h"
57 #include "albumthumbnailloader.h"
58 #include "captionedit.h"
59 #include "collectionscanner.h"
60 #include "coredbtransaction.h"
61 #include "dnotificationwrapper.h"
62 #include "ddatetimeedit.h"
63 #include "digikamapp.h"
64 #include "fileactionmngr.h"
65 #include "ratingwidget.h"
66 #include "scancontroller.h"
67 #include "tagcheckview.h"
68 #include "templateselector.h"
69 #include "templateviewer.h"
70 #include "itemattributeswatch.h"
71 #include "statusprogressbar.h"
72 #include "tagmodificationhelper.h"
73 #include "template.h"
74 #include "iteminfolist.h"
75 #include "iteminfo.h"
76 #include "colorlabelwidget.h"
77 #include "picklabelwidget.h"
78 #include "fileactionprogress.h"
79 #include "tagsmanager.h"
80 #include "tagtreeview.h"
81 #include "searchtextbardb.h"
82 #include "disjointmetadata.h"
83 #include "altlangstredit.h"
84 
85 namespace Digikam
86 {
87 
88 class Q_DECL_HIDDEN ItemDescEditTab::Private
89 {
90 
91 public:
92 
93     enum DescEditTab
94     {
95         DESCRIPTIONS = 0,
96         TAGS,
97         INFOS
98     };
99 
Private()100     explicit Private()
101       : modified                    (false),
102         ignoreItemAttributesWatch   (false),
103         ignoreTagChanges            (false),
104         togglingSearchSettings      (false),
105         recentTagsBtn               (nullptr),
106         assignedTagsBtn             (nullptr),
107         revertBtn                   (nullptr),
108         openTagMngr                 (nullptr),
109         moreMenu                    (nullptr),
110         applyBtn                    (nullptr),
111         moreButton                  (nullptr),
112         applyToAllVersionsButton    (nullptr),
113         lastSelectedWidget          (nullptr),
114         titleEdit                   (nullptr),
115         captionsEdit                (nullptr),
116         dateTimeEdit                (nullptr),
117         tabWidget                   (nullptr),
118         tagsSearchBar               (nullptr),
119         newTagEdit                  (nullptr),
120         tagCheckView                (nullptr),
121         templateSelector            (nullptr),
122         templateViewer              (nullptr),
123         ratingWidget                (nullptr),
124         colorLabelSelector          (nullptr),
125         pickLabelSelector           (nullptr),
126         tagModel                    (nullptr),
127         metadataChangeTimer         (nullptr)
128     {
129     }
130 
131     bool                 modified;
132     bool                 ignoreItemAttributesWatch;
133     bool                 ignoreTagChanges;
134     bool                 togglingSearchSettings;
135 
136     QToolButton*         recentTagsBtn;
137     QToolButton*         assignedTagsBtn;
138     QToolButton*         revertBtn;
139     QPushButton*         openTagMngr;
140 
141     QMenu*               moreMenu;
142 
143     QPushButton*         applyBtn;
144     QPushButton*         moreButton;
145     QPushButton*         applyToAllVersionsButton;
146 
147     QWidget*             lastSelectedWidget;
148 
149     AltLangStrEdit*      titleEdit;
150 
151     CaptionEdit*         captionsEdit;
152 
153     DDateTimeEdit*       dateTimeEdit;
154 
155     QTabWidget*          tabWidget;
156 
157     SearchTextBarDb*     tagsSearchBar;
158     AddTagsLineEdit*     newTagEdit;
159 
160     ItemInfoList         currInfos;
161 
162     TagCheckView*        tagCheckView;
163 
164     TemplateSelector*    templateSelector;
165     TemplateViewer*      templateViewer;
166 
167     RatingWidget*        ratingWidget;
168     ColorLabelSelector*  colorLabelSelector;
169     PickLabelSelector*   pickLabelSelector;
170 
171     DisjointMetadata     hub;
172 
173     TagModel*            tagModel;
174 
175     QTimer*              metadataChangeTimer;
176     QList<int>           metadataChangeIds;
177 };
178 
ItemDescEditTab(QWidget * const parent)179 ItemDescEditTab::ItemDescEditTab(QWidget* const parent)
180     : DVBox(parent),
181       d    (new Private)
182 {
183     const int spacing      = QApplication::style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing);
184 
185     setContentsMargins(QMargins());
186     setSpacing(spacing);
187     d->tabWidget           = new QTabWidget(this);
188 
189     d->metadataChangeTimer = new QTimer(this);
190     d->metadataChangeTimer->setSingleShot(true);
191     d->metadataChangeTimer->setInterval(250);
192 
193     // Captions/Date/Rating view -----------------------------------
194 
195     QScrollArea* const sv          = new QScrollArea(d->tabWidget);
196     sv->setFrameStyle(QFrame::NoFrame);
197     sv->setWidgetResizable(true);
198 
199     QWidget* const captionTagsArea = new QWidget(sv->viewport());
200     QGridLayout* const grid1       = new QGridLayout(captionTagsArea);
201     sv->setWidget(captionTagsArea);
202 
203     d->titleEdit          = new AltLangStrEdit(captionTagsArea);
204     d->titleEdit->setTitle(i18nc("@title: comment title string", "Title:"));
205     d->titleEdit->setPlaceholderText(i18n("Enter title here."));
206 
207     d->captionsEdit       = new CaptionEdit(captionTagsArea);
208 
209     DHBox* const dateBox  = new DHBox(captionTagsArea);
210     new QLabel(i18n("Date:"), dateBox);
211     d->dateTimeEdit       = new DDateTimeEdit(dateBox, QLatin1String("datepicker"));
212 
213     DHBox* const pickBox  = new DHBox(captionTagsArea);
214     new QLabel(i18n("Pick Label:"), pickBox);
215     d->pickLabelSelector  = new PickLabelSelector(pickBox);
216     pickBox->layout()->setAlignment(d->pickLabelSelector, Qt::AlignVCenter | Qt::AlignRight);
217 
218     DHBox* const colorBox = new DHBox(captionTagsArea);
219     new QLabel(i18n("Color Label:"), colorBox);
220     d->colorLabelSelector = new ColorLabelSelector(colorBox);
221     colorBox->layout()->setAlignment(d->colorLabelSelector, Qt::AlignVCenter | Qt::AlignRight);
222 
223     DHBox* const rateBox  = new DHBox(captionTagsArea);
224     new QLabel(i18n("Rating:"), rateBox);
225     d->ratingWidget       = new RatingWidget(rateBox);
226     rateBox->layout()->setAlignment(d->ratingWidget, Qt::AlignVCenter | Qt::AlignRight);
227 
228     // Buttons -----------------------------------------
229 
230     DHBox* const applyButtonBox = new DHBox(this);
231     applyButtonBox->setSpacing(spacing);
232 
233     d->applyBtn                 = new QPushButton(i18n("Apply"), applyButtonBox);
234     d->applyBtn->setIcon(QIcon::fromTheme(QLatin1String("dialog-ok-apply")));
235     d->applyBtn->setEnabled(false);
236     d->applyBtn->setToolTip(i18n("Apply all changes to images"));
237     //buttonsBox->setStretchFactor(d->applyBtn, 10);
238 
239     DHBox* const buttonsBox     = new DHBox(this);
240     buttonsBox->setSpacing(spacing);
241 
242     d->revertBtn                = new QToolButton(buttonsBox);
243     d->revertBtn->setIcon(QIcon::fromTheme(QLatin1String("document-revert")));
244     d->revertBtn->setToolTip(i18n("Revert all changes"));
245     d->revertBtn->setEnabled(false);
246 
247     d->applyToAllVersionsButton = new QPushButton(i18n("Apply to all versions"), buttonsBox);
248     d->applyToAllVersionsButton->setIcon(QIcon::fromTheme(QLatin1String("dialog-ok-apply")));
249     d->applyToAllVersionsButton->setEnabled(false);
250     d->applyToAllVersionsButton->setToolTip(i18n("Apply all changes to all versions of this image"));
251 
252     d->moreButton               = new QPushButton(i18nc("@action: more actions to apply changes", "More"), buttonsBox);
253     d->moreMenu                 = new QMenu(captionTagsArea);
254     d->moreButton->setMenu(d->moreMenu);
255 
256     // --------------------------------------------------
257 
258     grid1->addWidget(d->titleEdit,    0, 0, 1, 2);
259     grid1->addWidget(d->captionsEdit, 1, 0, 1, 2);
260     grid1->addWidget(dateBox,         2, 0, 1, 2);
261     grid1->addWidget(pickBox,         3, 0, 1, 2);
262     grid1->addWidget(colorBox,        4, 0, 1, 2);
263     grid1->addWidget(rateBox,         5, 0, 1, 2);
264     grid1->setRowStretch(1, 10);
265     grid1->setContentsMargins(spacing, spacing, spacing, spacing);
266     grid1->setSpacing(spacing);
267 
268     d->tabWidget->insertTab(Private::DESCRIPTIONS, sv, i18n("Description"));
269 
270     // Tags view ---------------------------------------------------
271 
272     QScrollArea* const sv3    = new QScrollArea(d->tabWidget);
273     sv3->setFrameStyle(QFrame::NoFrame);
274     sv3->setWidgetResizable(true);
275 
276     QWidget* const tagsArea   = new QWidget(sv3->viewport());
277     QGridLayout* const grid3  = new QGridLayout(tagsArea);
278     sv3->setWidget(tagsArea);
279 
280     d->tagModel     = new TagModel(AbstractAlbumModel::IncludeRootAlbum, this);
281     d->tagModel->setCheckable(true);
282     d->tagModel->setRootCheckable(false);
283     d->tagCheckView = new TagCheckView(tagsArea, d->tagModel);
284     d->tagCheckView->setCheckNewTags(true);
285 
286     d->openTagMngr = new QPushButton(i18n("Open Tag Manager"));
287 
288     d->newTagEdit  = new AddTagsLineEdit(tagsArea);
289     d->newTagEdit->setSupportingTagModel(d->tagModel);
290     d->newTagEdit->setTagTreeView(d->tagCheckView);
291     //, "ItemDescEditTabNewTagEdit",
292     //d->newTagEdit->setCaseSensitive(false);
293     d->newTagEdit->setPlaceholderText(i18n("Enter tag here."));
294     d->newTagEdit->setWhatsThis(i18n("Enter the text used to create tags here. "
295                                      "'/' can be used to create a hierarchy of tags. "
296                                      "',' can be used to create more than one hierarchy at the same time."));
297 
298     DHBox* const tagsSearch = new DHBox(tagsArea);
299     tagsSearch->setSpacing(spacing);
300 
301     d->tagsSearchBar        = new SearchTextBarDb(tagsSearch, QLatin1String("ItemDescEditTabTagsSearchBar"));
302 
303     d->tagsSearchBar->setModel(d->tagCheckView->filteredModel(),
304                                AbstractAlbumModel::AlbumIdRole,
305                                AbstractAlbumModel::AlbumTitleRole);
306 
307     d->tagsSearchBar->setFilterModel(d->tagCheckView->albumFilterModel());
308 
309     d->assignedTagsBtn      = new QToolButton(tagsSearch);
310     d->assignedTagsBtn->setToolTip(i18n("Tags already assigned"));
311     d->assignedTagsBtn->setIcon(QIcon::fromTheme(QLatin1String("tag-assigned")));
312     d->assignedTagsBtn->setCheckable(true);
313 
314     d->recentTagsBtn            = new QToolButton(tagsSearch);
315     QMenu* const recentTagsMenu = new QMenu(d->recentTagsBtn);
316     d->recentTagsBtn->setToolTip(i18n("Recent Tags"));
317     d->recentTagsBtn->setIcon(QIcon::fromTheme(QLatin1String("tag-recents")));
318     d->recentTagsBtn->setIconSize(QSize(16, 16));
319     d->recentTagsBtn->setMenu(recentTagsMenu);
320     d->recentTagsBtn->setPopupMode(QToolButton::InstantPopup);
321 
322     grid3->addWidget(d->openTagMngr,  0, 0, 1, 2);
323     grid3->addWidget(d->newTagEdit,   1, 0, 1, 2);
324     grid3->addWidget(d->tagCheckView, 2, 0, 1, 2);
325     grid3->addWidget(tagsSearch,      3, 0, 1, 2);
326     grid3->setRowStretch(1, 10);
327 
328     d->tabWidget->insertTab(Private::TAGS, sv3, i18n("Tags"));
329 
330     // Information Management View --------------------------------------
331 
332     QScrollArea* const sv2    = new QScrollArea(d->tabWidget);
333     sv2->setFrameStyle(QFrame::NoFrame);
334     sv2->setWidgetResizable(true);
335 
336     QWidget* const infoArea   = new QWidget(sv2->viewport());
337     QGridLayout* const grid2  = new QGridLayout(infoArea);
338     sv2->setWidget(infoArea);
339 
340     d->templateSelector       = new TemplateSelector(infoArea);
341     d->templateViewer         = new TemplateViewer(infoArea);
342     d->templateViewer->setObjectName(QLatin1String("ItemDescEditTab Expander"));
343 
344     grid2->addWidget(d->templateSelector, 0, 0, 1, 2);
345     grid2->addWidget(d->templateViewer,   1, 0, 1, 2);
346     grid2->setRowStretch(1, 10);
347     grid2->setContentsMargins(spacing, spacing, spacing, spacing);
348     grid2->setSpacing(spacing);
349 
350     d->tabWidget->insertTab(Private::INFOS, sv2, i18n("Information"));
351 
352     // --------------------------------------------------
353 
354     connect(d->openTagMngr, SIGNAL(clicked()),
355             this, SLOT(slotOpenTagsManager()));
356 
357     connect(d->tagCheckView->checkableModel(), SIGNAL(checkStateChanged(Album*,Qt::CheckState)),
358             this, SLOT(slotTagStateChanged(Album*,Qt::CheckState)));
359 
360     connect(d->titleEdit, SIGNAL(signalModified(QString,QString)),
361             this, SLOT(slotTitleChanged()));
362 
363     connect(d->titleEdit, SIGNAL(signalValueAdded(QString,QString)),
364             this, SLOT(slotTitleChanged()));
365 
366     connect(d->titleEdit, SIGNAL(signalValueDeleted(QString)),
367             this, SLOT(slotTitleChanged()));
368 
369     connect(d->captionsEdit, SIGNAL(signalModified()),
370             this, SLOT(slotCommentChanged()));
371 
372     connect(d->dateTimeEdit, SIGNAL(dateTimeChanged(QDateTime)),
373             this, SLOT(slotDateTimeChanged(QDateTime)));
374 
375     connect(d->pickLabelSelector, SIGNAL(signalPickLabelChanged(int)),
376             this, SLOT(slotPickLabelChanged(int)));
377 
378     connect(d->colorLabelSelector, SIGNAL(signalColorLabelChanged(int)),
379             this, SLOT(slotColorLabelChanged(int)));
380 
381     connect(d->ratingWidget, SIGNAL(signalRatingChanged(int)),
382             this, SLOT(slotRatingChanged(int)));
383 
384     connect(d->templateSelector, SIGNAL(signalTemplateSelected()),
385             this, SLOT(slotTemplateSelected()));
386 
387     connect(d->tagsSearchBar, SIGNAL(signalSearchTextSettings(SearchTextSettings)),
388             this, SLOT(slotTagsSearchChanged(SearchTextSettings)));
389 
390     connect(d->assignedTagsBtn, SIGNAL(toggled(bool)),
391             this, SLOT(slotAssignedTagsToggled(bool)));
392 
393     connect(d->newTagEdit, SIGNAL(taggingActionActivated(TaggingAction)),
394             this, SLOT(slotTaggingActionActivated(TaggingAction)));
395 
396     connect(d->applyBtn, SIGNAL(clicked()),
397             this, SLOT(slotApplyAllChanges()));
398 
399     connect(d->applyToAllVersionsButton, SIGNAL(clicked()),
400             this, SLOT(slotApplyChangesToAllVersions()));
401 
402     connect(d->revertBtn, SIGNAL(clicked()),
403             this, SLOT(slotRevertAllChanges()));
404 
405     connect(d->moreMenu, SIGNAL(aboutToShow()),
406             this, SLOT(slotMoreMenu()));
407 
408     connect(d->metadataChangeTimer, SIGNAL(timeout()),
409             this, SLOT(slotReloadForMetadataChange()));
410 
411     connect(this, SIGNAL(askToApplyChanges(QList<ItemInfo>,DisjointMetadata*)),
412             this, SLOT(slotAskToApplyChanges(QList<ItemInfo>,DisjointMetadata*)),
413             Qt::QueuedConnection);
414 
415     // Initialize ---------------------------------------------
416 
417     d->titleEdit->textEdit()->installEventFilter(this);
418     d->captionsEdit->textEdit()->installEventFilter(this);
419 
420     d->dateTimeEdit->installEventFilter(this);
421     d->pickLabelSelector->installEventFilter(this);
422     d->colorLabelSelector->installEventFilter(this);
423     d->ratingWidget->installEventFilter(this);
424 
425     // TODO update, what does this filter?
426 
427     d->tagCheckView->installEventFilter(this);
428     d->newTagEdit->installEventFilter(this);
429     updateRecentTags();
430 
431     // Connect to attribute watch ------------------------------
432 
433     ItemAttributesWatch* const watch = ItemAttributesWatch::instance();
434 
435     connect(watch, SIGNAL(signalImageTagsChanged(qlonglong)),
436             this, SLOT(slotImageTagsChanged(qlonglong)));
437 
438     connect(watch, SIGNAL(signalImagesChanged(int)),
439             this, SLOT(slotImagesChanged(int)));
440 
441     connect(watch, SIGNAL(signalImageRatingChanged(qlonglong)),
442             this, SLOT(slotImageRatingChanged(qlonglong)));
443 
444     connect(watch, SIGNAL(signalImageDateChanged(qlonglong)),
445             this, SLOT(slotImageDateChanged(qlonglong)));
446 
447     connect(watch, SIGNAL(signalImageCaptionChanged(qlonglong)),
448             this, SLOT(slotImageCaptionChanged(qlonglong)));
449 }
450 
~ItemDescEditTab()451 ItemDescEditTab::~ItemDescEditTab()
452 {
453     delete d;
454 }
455 
readSettings(KConfigGroup & group)456 void ItemDescEditTab::readSettings(KConfigGroup& group)
457 {
458     d->tabWidget->setCurrentIndex(group.readEntry(QLatin1String("ItemDescEdit Tab"), (int)Private::DESCRIPTIONS));
459     d->titleEdit->setCurrentLanguageCode(group.readEntry(QLatin1String("ItemDescEditTab TitleLang"), QString()));
460     d->captionsEdit->setCurrentLanguageCode(group.readEntry(QLatin1String("ItemDescEditTab CaptionsLang"), QString()));
461 
462     d->templateViewer->readSettings(group);
463 
464     d->tagCheckView->setConfigGroup(group);
465     d->tagCheckView->setEntryPrefix(QLatin1String("ItemDescEditTab TagCheckView"));
466     d->tagCheckView->loadState();
467     d->tagsSearchBar->setConfigGroup(group);
468     d->tagsSearchBar->setEntryPrefix(QLatin1String("ItemDescEditTab SearchBar"));
469     d->tagsSearchBar->loadState();
470 }
471 
writeSettings(KConfigGroup & group)472 void ItemDescEditTab::writeSettings(KConfigGroup& group)
473 {
474     group.writeEntry(QLatin1String("ItemDescEdit Tab"),             d->tabWidget->currentIndex());
475     group.writeEntry(QLatin1String("ItemDescEditTab TitleLang"),    d->titleEdit->currentLanguageCode());
476     group.writeEntry(QLatin1String("ItemDescEditTab CaptionsLang"), d->captionsEdit->currentLanguageCode());
477 
478     d->templateViewer->writeSettings(group);
479 
480     d->tagCheckView->saveState();
481     d->tagsSearchBar->saveState();
482 }
483 
setFocusToLastSelectedWidget()484 void ItemDescEditTab::setFocusToLastSelectedWidget()
485 {
486     if (d->lastSelectedWidget)
487     {
488         d->lastSelectedWidget->setFocus();
489     }
490 
491     d->lastSelectedWidget = nullptr;
492 }
493 
setFocusToTagsView()494 void ItemDescEditTab::setFocusToTagsView()
495 {
496     d->lastSelectedWidget = qobject_cast<QWidget*>(d->tagCheckView);
497     d->tagCheckView->setFocus();
498     d->tabWidget->setCurrentIndex(Private::TAGS);
499 }
500 
setFocusToNewTagEdit()501 void ItemDescEditTab::setFocusToNewTagEdit()
502 {
503     // select "Tags" tab and focus the NewTagLineEdit widget
504 
505     d->tabWidget->setCurrentIndex(Private::TAGS);
506     d->newTagEdit->setFocus();
507 }
508 
setFocusToTitlesEdit()509 void ItemDescEditTab::setFocusToTitlesEdit()
510 {
511     d->tabWidget->setCurrentIndex(Private::DESCRIPTIONS);
512     d->titleEdit->textEdit()->setFocus();
513 }
514 
setFocusToCommentsEdit()515 void ItemDescEditTab::setFocusToCommentsEdit()
516 {
517     d->tabWidget->setCurrentIndex(Private::DESCRIPTIONS);
518     d->captionsEdit->textEdit()->setFocus();
519 }
520 
activateAssignedTagsButton()521 void ItemDescEditTab::activateAssignedTagsButton()
522 {
523     d->tabWidget->setCurrentIndex(Private::TAGS);
524     d->assignedTagsBtn->click();
525 }
526 
singleSelection() const527 bool ItemDescEditTab::singleSelection() const
528 {
529     return (d->currInfos.count() == 1);
530 }
531 
slotChangingItems()532 void ItemDescEditTab::slotChangingItems()
533 {
534     if (!d->modified)
535     {
536         return;
537     }
538 
539     if (d->currInfos.isEmpty())
540     {
541         return;
542     }
543 
544     if (!ApplicationSettings::instance()->getApplySidebarChangesDirectly())
545     {
546         // Open dialog via queued connection out-of-scope, see bug 302311
547 
548         DisjointMetadata* const hub2 = new DisjointMetadata();
549         hub2->setDataFields(d->hub.dataFields());
550 
551         emit askToApplyChanges(d->currInfos, hub2);
552 
553         reset();
554     }
555     else
556     {
557         slotApplyAllChanges();
558     }
559 }
560 
slotAskToApplyChanges(const QList<ItemInfo> & infos,DisjointMetadata * hub)561 void ItemDescEditTab::slotAskToApplyChanges(const QList<ItemInfo>& infos, DisjointMetadata* hub)
562 {
563     int changedFields = 0;
564 
565     if (hub->titlesChanged())
566     {
567         ++changedFields;
568     }
569 
570     if (hub->commentsChanged())
571     {
572         ++changedFields;
573     }
574 
575     if (hub->dateTimeChanged())
576     {
577         ++changedFields;
578     }
579 
580     if (hub->ratingChanged())
581     {
582         ++changedFields;
583     }
584 
585     if (hub->pickLabelChanged())
586     {
587         ++changedFields;
588     }
589 
590     if (hub->colorLabelChanged())
591     {
592         ++changedFields;
593     }
594 
595     if (hub->tagsChanged())
596     {
597         ++changedFields;
598     }
599 
600     QString text;
601 
602     if (changedFields == 1)
603     {
604         if      (hub->commentsChanged())
605         {
606             text = i18np("You have edited the image caption. ",
607                          "You have edited the captions of %1 images. ",
608                          infos.count());
609         }
610         else if (hub->titlesChanged())
611         {
612             text = i18np("You have edited the image title. ",
613                          "You have edited the titles of %1 images. ",
614                          infos.count());
615         }
616         else if (hub->dateTimeChanged())
617         {
618             text = i18np("You have edited the date of the image. ",
619                          "You have edited the date of %1 images. ",
620                          infos.count());
621         }
622         else if (hub->pickLabelChanged())
623         {
624             text = i18np("You have edited the pick label of the image. ",
625                          "You have edited the pick label of %1 images. ",
626                          infos.count());
627         }
628         else if (hub->colorLabelChanged())
629         {
630             text = i18np("You have edited the color label of the image. ",
631                          "You have edited the color label of %1 images. ",
632                          infos.count());
633         }
634         else if (hub->ratingChanged())
635         {
636             text = i18np("You have edited the rating of the image. ",
637                          "You have edited the rating of %1 images. ",
638                          infos.count());
639         }
640         else if (hub->tagsChanged())
641         {
642             text = i18np("You have edited the tags of the image. ",
643                          "You have edited the tags of %1 images. ",
644                          infos.count());
645         }
646 
647         text += i18n("Do you want to apply your changes?");
648     }
649     else
650     {
651         text = i18np("<p>You have edited the metadata of the image: </p>",
652                      "<p>You have edited the metadata of %1 images: </p>",
653                      infos.count());
654 
655         text += QLatin1String("<p><ul>");
656 
657         if (hub->titlesChanged())
658         {
659             text += i18n("<li>title</li>");
660         }
661 
662         if (hub->commentsChanged())
663         {
664             text += i18n("<li>caption</li>");
665         }
666 
667         if (hub->dateTimeChanged())
668         {
669             text += i18n("<li>date</li>");
670         }
671 
672         if (hub->pickLabelChanged())
673         {
674             text += i18n("<li>pick label</li>");
675         }
676 
677         if (hub->colorLabelChanged())
678         {
679             text += i18n("<li>color label</li>");
680         }
681 
682         if (hub->ratingChanged())
683         {
684             text += i18n("<li>rating</li>");
685         }
686 
687         if (hub->tagsChanged())
688         {
689             text += i18n("<li>tags</li>");
690         }
691 
692         text += QLatin1String("</ul></p>");
693 
694         text += i18n("<p>Do you want to apply your changes?</p>");
695     }
696 
697     QCheckBox* const alwaysCBox  = new QCheckBox(i18n("Always apply changes without confirmation"));
698 
699     QPointer<QMessageBox> msgBox = new QMessageBox(QMessageBox::Information,
700                                                    i18n("Apply changes?"),
701                                                    text,
702                                                    QMessageBox::Yes | QMessageBox::No,
703                                                    qApp->activeWindow());
704     msgBox->setCheckBox(alwaysCBox);
705     msgBox->setDefaultButton(QMessageBox::No);
706     msgBox->setEscapeButton(QMessageBox::No);
707 
708     // Pop-up a message in desktop notification manager
709 
710     DNotificationWrapper(QString(), i18n("Apply changes?"),
711                          DigikamApp::instance(), DigikamApp::instance()->windowTitle());
712 
713     int returnCode   = msgBox->exec();
714     bool alwaysApply = msgBox->checkBox()->isChecked();
715     delete msgBox;
716 
717     if (alwaysApply)
718     {
719         ApplicationSettings::instance()->setApplySidebarChangesDirectly(true);
720     }
721 
722     if (returnCode == QMessageBox::No)
723     {
724         delete hub;
725         return;
726     }
727 
728     // otherwise apply:
729 
730     FileActionMngr::instance()->applyMetadata(infos, hub);
731 }
732 
reset()733 void ItemDescEditTab::reset()
734 {
735     d->modified = false;
736     d->hub.resetChanged();
737     d->applyBtn->setEnabled(false);
738     d->revertBtn->setEnabled(false);
739     d->applyToAllVersionsButton->setEnabled(false);
740 }
741 
slotApplyAllChanges()742 void ItemDescEditTab::slotApplyAllChanges()
743 {
744     if (!d->modified)
745     {
746         return;
747     }
748 
749     if (d->currInfos.isEmpty())
750     {
751         return;
752     }
753 
754     FileActionMngr::instance()->applyMetadata(d->currInfos, d->hub);
755     reset();
756 }
757 
slotRevertAllChanges()758 void ItemDescEditTab::slotRevertAllChanges()
759 {
760     if (!d->modified)
761     {
762         return;
763     }
764 
765     if (d->currInfos.isEmpty())
766     {
767         return;
768     }
769 
770     setInfos(d->currInfos);
771 }
772 
setItem(const ItemInfo & info)773 void ItemDescEditTab::setItem(const ItemInfo& info)
774 {
775     slotChangingItems();
776     ItemInfoList list;
777 
778     if (!info.isNull())
779     {
780         list << info;
781     }
782 
783     setInfos(list);
784 }
785 
setItems(const ItemInfoList & infos)786 void ItemDescEditTab::setItems(const ItemInfoList& infos)
787 {
788     slotChangingItems();
789     setInfos(infos);
790 }
791 
setInfos(const ItemInfoList & infos)792 void ItemDescEditTab::setInfos(const ItemInfoList& infos)
793 {
794     if (infos.isEmpty())
795     {
796         d->hub.reset();
797         d->captionsEdit->blockSignals(true);
798         d->captionsEdit->reset();
799         d->captionsEdit->blockSignals(false);
800         d->titleEdit->blockSignals(true);
801         d->titleEdit->reset();
802         d->titleEdit->blockSignals(false);
803         d->currInfos.clear();
804         resetMetadataChangeInfo();
805         setEnabled(false);
806         return;
807     }
808 
809     setEnabled(true);
810     d->currInfos = infos;
811     d->modified  = false;
812     resetMetadataChangeInfo();
813     d->hub.reset();
814     d->applyBtn->setEnabled(false);
815     d->revertBtn->setEnabled(false);
816 
817     if (d->currInfos.count() > 1000)
818     {
819         QApplication::setOverrideCursor(Qt::WaitCursor);
820     }
821 
822     // First, we read all tags of the items into the cache with one SQL query.
823     // This is faster than each item individually.
824 
825     d->currInfos.loadTagIds();
826 
827     foreach (const ItemInfo& info, d->currInfos)
828     {
829         d->hub.load(info);
830     }
831 
832     updateComments();
833     updatePickLabel();
834     updateColorLabel();
835     updateRating();
836     updateDate();
837     updateTemplate();
838     updateTagsView();
839     updateRecentTags();
840     setFocusToLastSelectedWidget();
841 
842     if (d->currInfos.count() > 1000)
843     {
844         QApplication::restoreOverrideCursor();
845     }
846 }
847 
slotReadFromFileMetadataToDatabase()848 void ItemDescEditTab::slotReadFromFileMetadataToDatabase()
849 {
850     initProgressIndicator();
851 
852     emit signalProgressMessageChanged(i18n("Reading metadata from files. Please wait..."));
853 
854     d->ignoreItemAttributesWatch = true;
855     int i                        = 0;
856 
857     ScanController::instance()->suspendCollectionScan();
858 
859     CollectionScanner scanner;
860 
861     foreach (const ItemInfo& info, d->currInfos)
862     {
863         scanner.scanFile(info, CollectionScanner::Rescan);
864 
865         emit signalProgressValueChanged(i++/(float)d->currInfos.count());
866 
867         qApp->processEvents();
868     }
869 
870     ScanController::instance()->resumeCollectionScan();
871     d->ignoreItemAttributesWatch = false;
872 
873     emit signalProgressFinished();
874 
875     // reload everything
876 
877     setInfos(d->currInfos);
878 }
879 
slotWriteToFileMetadataFromDatabase()880 void ItemDescEditTab::slotWriteToFileMetadataFromDatabase()
881 {
882     initProgressIndicator();
883 
884     emit signalProgressMessageChanged(i18n("Writing metadata to files. Please wait..."));
885 
886     int i = 0;
887 
888     foreach (const ItemInfo& info, d->currInfos)
889     {
890         MetadataHub fileHub;
891 
892         // read in from database
893 
894         fileHub.load(info);
895 
896         // write out to file DMetadata
897 
898         fileHub.write(info.filePath());
899 
900         emit signalProgressValueChanged(i++ / (float)d->currInfos.count());
901         qApp->processEvents();
902     }
903 
904     emit signalProgressFinished();
905 }
906 
eventFilter(QObject * o,QEvent * e)907 bool ItemDescEditTab::eventFilter(QObject* o, QEvent* e)
908 {
909     if (e->type() == QEvent::KeyPress)
910     {
911         QKeyEvent* const k = static_cast<QKeyEvent*>(e);
912 
913         if ((k->key() == Qt::Key_Enter) || (k->key() == Qt::Key_Return))
914         {
915             if      (k->modifiers() == Qt::ControlModifier)
916             {
917                 d->lastSelectedWidget = qobject_cast<QWidget*>(o);
918                 emit signalNextItem();
919                 return true;
920             }
921             else if (k->modifiers() == Qt::ShiftModifier)
922             {
923                 d->lastSelectedWidget = qobject_cast<QWidget*>(o);
924                 emit signalPrevItem();
925                 return true;
926             }
927         }
928 
929         if (k->key() == Qt::Key_PageUp)
930         {
931             d->lastSelectedWidget = qobject_cast<QWidget*>(o);
932             emit signalPrevItem();
933             return true;
934         }
935 
936         if (k->key() == Qt::Key_PageDown)
937         {
938             d->lastSelectedWidget = qobject_cast<QWidget*>(o);
939             emit signalNextItem();
940             return true;
941         }
942 
943         if ((d->newTagEdit == o) &&
944             !d->newTagEdit->completer()->popup()->isVisible())
945         {
946             if (k->key() == Qt::Key_Up)
947             {
948                 d->lastSelectedWidget = qobject_cast<QWidget*>(o);
949                 emit signalPrevItem();
950                 return true;
951             }
952 
953             if (k->key() == Qt::Key_Down)
954             {
955                 d->lastSelectedWidget = qobject_cast<QWidget*>(o);
956                 emit signalNextItem();
957                 return true;
958             }
959         }
960     }
961 
962     return DVBox::eventFilter(o, e);
963 }
964 
populateTags()965 void ItemDescEditTab::populateTags()
966 {
967     // TODO update, this wont work... crashes
968     //KConfigGroup group;
969     //d->tagCheckView->loadViewState(group);
970 }
971 
slotTagStateChanged(Album * album,Qt::CheckState checkState)972 void ItemDescEditTab::slotTagStateChanged(Album* album, Qt::CheckState checkState)
973 {
974     TAlbum* const tag = dynamic_cast<TAlbum*>(album);
975 
976     if (!tag || d->ignoreTagChanges)
977     {
978         return;
979     }
980 
981     switch (checkState)
982     {
983         case Qt::Checked:
984             d->hub.setTag(tag->id());
985             break;
986 
987         default:
988             d->hub.setTag(tag->id(), DisjointMetadataDataFields::MetadataInvalid);
989             break;
990     }
991 
992     slotModified();
993 }
994 
slotCommentChanged()995 void ItemDescEditTab::slotCommentChanged()
996 {
997     d->hub.setComments(d->captionsEdit->values());
998     setMetadataWidgetStatus(d->hub.commentsStatus(), d->captionsEdit);
999     slotModified();
1000 }
1001 
slotTitleChanged()1002 void ItemDescEditTab::slotTitleChanged()
1003 {
1004     CaptionsMap titles;
1005 
1006     titles.fromAltLangMap(d->titleEdit->values());
1007     d->hub.setTitles(titles);
1008     setMetadataWidgetStatus(d->hub.titlesStatus(), d->titleEdit);
1009     slotModified();
1010 }
1011 
slotDateTimeChanged(const QDateTime & dateTime)1012 void ItemDescEditTab::slotDateTimeChanged(const QDateTime& dateTime)
1013 {
1014     d->hub.setDateTime(dateTime);
1015     setMetadataWidgetStatus(d->hub.dateTimeStatus(), d->dateTimeEdit);
1016     slotModified();
1017 }
1018 
slotTemplateSelected()1019 void ItemDescEditTab::slotTemplateSelected()
1020 {
1021     d->hub.setMetadataTemplate(d->templateSelector->getTemplate());
1022     d->templateViewer->setTemplate(d->templateSelector->getTemplate());
1023     setMetadataWidgetStatus(d->hub.templateStatus(), d->templateSelector);
1024     slotModified();
1025 }
1026 
slotPickLabelChanged(int pickId)1027 void ItemDescEditTab::slotPickLabelChanged(int pickId)
1028 {
1029     d->hub.setPickLabel(pickId);
1030 
1031     // no handling for MetadataDisjoint needed for pick label,
1032     // we set it to 0 when disjoint, see below
1033 
1034     slotModified();
1035 }
1036 
slotColorLabelChanged(int colorId)1037 void ItemDescEditTab::slotColorLabelChanged(int colorId)
1038 {
1039     d->hub.setColorLabel(colorId);
1040 
1041     // no handling for MetadataDisjoint needed for color label,
1042     // we set it to 0 when disjoint, see below
1043 
1044     slotModified();
1045 }
1046 
slotRatingChanged(int rating)1047 void ItemDescEditTab::slotRatingChanged(int rating)
1048 {
1049     d->hub.setRating(rating);
1050 
1051     // no handling for MetadataDisjoint needed for rating,
1052     // we set it to 0 when disjoint, see below
1053 
1054     slotModified();
1055 }
1056 
slotModified()1057 void ItemDescEditTab::slotModified()
1058 {
1059     d->modified = true;
1060     d->applyBtn->setEnabled(true);
1061     d->revertBtn->setEnabled(true);
1062 
1063     if (d->currInfos.size() == 1)
1064     {
1065         d->applyToAllVersionsButton->setEnabled(true);
1066     }
1067 }
1068 
slotTaggingActionActivated(const TaggingAction & action)1069 void ItemDescEditTab::slotTaggingActionActivated(const TaggingAction& action)
1070 {
1071     TAlbum* assigned = nullptr;
1072 
1073     if (action.shallAssignTag())
1074     {
1075         assigned = AlbumManager::instance()->findTAlbum(action.tagId());
1076 
1077         if (assigned)
1078         {
1079             d->tagModel->setChecked(assigned, true);
1080             d->tagCheckView->checkableAlbumFilterModel()->updateFilter();
1081         }
1082     }
1083     else if (action.shallCreateNewTag())
1084     {
1085         TAlbum* const parent = AlbumManager::instance()->findTAlbum(action.parentTagId());
1086 
1087         // tag is assigned automatically
1088 
1089         assigned = d->tagCheckView->tagModificationHelper()->slotTagNew(parent, action.newTagName());
1090     }
1091 
1092     if (assigned)
1093     {
1094         d->tagCheckView->scrollTo(d->tagCheckView->albumFilterModel()->indexForAlbum(assigned));
1095         QTimer::singleShot(0, d->newTagEdit, SLOT(clear()));
1096     }
1097 }
1098 
assignPickLabel(int pickId)1099 void ItemDescEditTab::assignPickLabel(int pickId)
1100 {
1101     d->pickLabelSelector->setPickLabel((PickLabel)pickId);
1102 }
1103 
assignColorLabel(int colorId)1104 void ItemDescEditTab::assignColorLabel(int colorId)
1105 {
1106     d->colorLabelSelector->setColorLabel((ColorLabel)colorId);
1107 }
1108 
assignRating(int rating)1109 void ItemDescEditTab::assignRating(int rating)
1110 {
1111     d->ratingWidget->setRating(rating);
1112 }
1113 
setTagState(TAlbum * const tag,DisjointMetadataDataFields::Status status)1114 void ItemDescEditTab::setTagState(TAlbum* const tag, DisjointMetadataDataFields::Status status)
1115 {
1116     if (!tag)
1117     {
1118         return;
1119     }
1120 
1121     switch (status)
1122     {
1123         case DisjointMetadataDataFields::MetadataDisjoint:
1124             d->tagModel->setCheckState(tag, Qt::PartiallyChecked);
1125             break;
1126 
1127         case DisjointMetadataDataFields::MetadataAvailable:
1128             d->tagModel->setChecked(tag, true);
1129             break;
1130 
1131         case DisjointMetadataDataFields::MetadataInvalid:
1132             d->tagModel->setChecked(tag, false);
1133             break;
1134 
1135         default:
1136             qCWarning(DIGIKAM_GENERAL_LOG) << "Untreated tag status enum value " << status;
1137             d->tagModel->setCheckState(tag, Qt::PartiallyChecked);
1138             break;
1139     }
1140 }
1141 
updateTagsView()1142 void ItemDescEditTab::updateTagsView()
1143 {
1144     // Avoid that the automatic tag toggling handles these calls and
1145     // modification is indicated to this widget
1146 
1147     TagCheckView::ToggleAutoTags toggle = d->tagCheckView->getToggleAutoTags();
1148     d->tagCheckView->setToggleAutoTags(TagCheckView::NoToggleAuto);
1149     d->ignoreTagChanges                 = true;
1150 
1151     // first reset the tags completely
1152 
1153     d->tagModel->resetAllCheckedAlbums();
1154 
1155     // Then update checked state for all tags of the currently selected images
1156 
1157     const QMap<int, DisjointMetadataDataFields::Status> hubMap = d->hub.tags();
1158 
1159     for (QMap<int, DisjointMetadataDataFields::Status>::const_iterator it = hubMap.begin() ;
1160          it != hubMap.end() ; ++it)
1161     {
1162         TAlbum* const tag = AlbumManager::instance()->findTAlbum(it.key());
1163         setTagState(tag, it.value());
1164     }
1165 
1166     d->ignoreTagChanges = false;
1167     d->tagCheckView->setToggleAutoTags(toggle);
1168 
1169     // The condition is a temporary fix not to destroy name filtering on image change.
1170     // See comments in these methods.
1171 
1172     if (d->assignedTagsBtn->isChecked())
1173     {
1174         slotAssignedTagsToggled(d->assignedTagsBtn->isChecked());
1175     }
1176 }
1177 
updateComments()1178 void ItemDescEditTab::updateComments()
1179 {
1180     d->captionsEdit->blockSignals(true);
1181     d->captionsEdit->setValues(d->hub.comments());
1182     setMetadataWidgetStatus(d->hub.commentsStatus(), d->captionsEdit);
1183     d->captionsEdit->blockSignals(false);
1184 
1185     d->titleEdit->blockSignals(true);
1186     d->titleEdit->setValues(d->hub.titles().toAltLangMap());
1187     setMetadataWidgetStatus(d->hub.titlesStatus(), d->titleEdit);
1188     d->titleEdit->blockSignals(false);
1189 }
1190 
updatePickLabel()1191 void ItemDescEditTab::updatePickLabel()
1192 {
1193     d->pickLabelSelector->blockSignals(true);
1194 
1195     if (d->hub.pickLabelStatus() == DisjointMetadataDataFields::MetadataDisjoint)
1196     {
1197         d->pickLabelSelector->setPickLabel(NoPickLabel);
1198     }
1199     else
1200     {
1201         d->pickLabelSelector->setPickLabel((PickLabel)d->hub.pickLabel());
1202     }
1203 
1204     d->pickLabelSelector->blockSignals(false);
1205 }
1206 
updateColorLabel()1207 void ItemDescEditTab::updateColorLabel()
1208 {
1209     d->colorLabelSelector->blockSignals(true);
1210 
1211     if (d->hub.colorLabelStatus() == DisjointMetadataDataFields::MetadataDisjoint)
1212     {
1213         d->colorLabelSelector->setColorLabel(NoColorLabel);
1214     }
1215     else
1216     {
1217         d->colorLabelSelector->setColorLabel((ColorLabel)d->hub.colorLabel());
1218     }
1219 
1220     d->colorLabelSelector->blockSignals(false);
1221 }
1222 
updateRating()1223 void ItemDescEditTab::updateRating()
1224 {
1225     d->ratingWidget->blockSignals(true);
1226 
1227     if (d->hub.ratingStatus() == DisjointMetadataDataFields::MetadataDisjoint)
1228     {
1229         d->ratingWidget->setRating(0);
1230     }
1231     else
1232     {
1233         d->ratingWidget->setRating(d->hub.rating());
1234     }
1235 
1236     d->ratingWidget->blockSignals(false);
1237 }
1238 
updateDate()1239 void ItemDescEditTab::updateDate()
1240 {
1241     d->dateTimeEdit->blockSignals(true);
1242     d->dateTimeEdit->setDateTime(d->hub.dateTime());
1243     setMetadataWidgetStatus(d->hub.dateTimeStatus(), d->dateTimeEdit);
1244     d->dateTimeEdit->blockSignals(false);
1245 }
1246 
updateTemplate()1247 void ItemDescEditTab::updateTemplate()
1248 {
1249     d->templateSelector->blockSignals(true);
1250     d->templateSelector->setTemplate(d->hub.metadataTemplate());
1251     d->templateViewer->setTemplate(d->hub.metadataTemplate());
1252     setMetadataWidgetStatus(d->hub.templateStatus(), d->templateSelector);
1253     d->templateSelector->blockSignals(false);
1254 }
1255 
setMetadataWidgetStatus(int status,QWidget * const widget)1256 void ItemDescEditTab::setMetadataWidgetStatus(int status, QWidget* const widget)
1257 {
1258     if (status == DisjointMetadataDataFields::MetadataDisjoint)
1259     {
1260         // For text widgets: Set text color to color of disabled text.
1261 
1262         QPalette palette = widget->palette();
1263         palette.setColor(QPalette::Text, palette.color(QPalette::Disabled, QPalette::Text));
1264         widget->setPalette(palette);
1265     }
1266     else
1267     {
1268         widget->setPalette(QPalette());
1269     }
1270 }
1271 
slotMoreMenu()1272 void ItemDescEditTab::slotMoreMenu()
1273 {
1274     d->moreMenu->clear();
1275 
1276     if (singleSelection())
1277     {
1278         d->moreMenu->addAction(i18n("Read metadata from file to database"), this, SLOT(slotReadFromFileMetadataToDatabase()));
1279         QAction* const writeAction = d->moreMenu->addAction(i18n("Write metadata to each file"), this,
1280                                                             SLOT(slotWriteToFileMetadataFromDatabase()));
1281 
1282         // we do not need a "Write to file" action here because the apply button will do just that
1283         // if selection is a single file.
1284         // Adding the option will confuse users: Does the apply button not write to file?
1285         // Removing the option will confuse users: There is not option to write to file! (not visible in single selection)
1286         // Disabling will confuse users: Why is it disabled?
1287 
1288         writeAction->setEnabled(false);
1289     }
1290     else
1291     {
1292         // We need to make clear that this action is different from the Apply button,
1293         // which saves the same changes to all files. These batch operations operate on each single file.
1294 
1295         d->moreMenu->addAction(i18n("Read metadata from each file to database"), this, SLOT(slotReadFromFileMetadataToDatabase()));
1296         d->moreMenu->addAction(i18n("Write metadata to each file"), this, SLOT(slotWriteToFileMetadataFromDatabase()));
1297     }
1298 }
1299 
slotOpenTagsManager()1300 void ItemDescEditTab::slotOpenTagsManager()
1301 {
1302     TagsManager* const tagMngr = TagsManager::instance();
1303     tagMngr->show();
1304     tagMngr->activateWindow();
1305     tagMngr->raise();
1306 }
1307 
slotImagesChanged(int albumId)1308 void ItemDescEditTab::slotImagesChanged(int albumId)
1309 {
1310     if (d->ignoreItemAttributesWatch || d->modified)
1311     {
1312         return;
1313     }
1314 
1315     Album* const a = AlbumManager::instance()->findAlbum(albumId);
1316 
1317     if (d->currInfos.isEmpty() || !a || a->isRoot() || (a->type() != Album::TAG))
1318     {
1319         return;
1320     }
1321 
1322     setInfos(d->currInfos);
1323 }
1324 
slotImageTagsChanged(qlonglong imageId)1325 void ItemDescEditTab::slotImageTagsChanged(qlonglong imageId)
1326 {
1327     metadataChange(imageId);
1328 }
1329 
slotImageRatingChanged(qlonglong imageId)1330 void ItemDescEditTab::slotImageRatingChanged(qlonglong imageId)
1331 {
1332     metadataChange(imageId);
1333 }
1334 
slotImageCaptionChanged(qlonglong imageId)1335 void ItemDescEditTab::slotImageCaptionChanged(qlonglong imageId)
1336 {
1337     metadataChange(imageId);
1338 }
1339 
slotImageDateChanged(qlonglong imageId)1340 void ItemDescEditTab::slotImageDateChanged(qlonglong imageId)
1341 {
1342     metadataChange(imageId);
1343 }
1344 
1345 // private common code for above methods
1346 
metadataChange(qlonglong imageId)1347 void ItemDescEditTab::metadataChange(qlonglong imageId)
1348 {
1349     if (d->ignoreItemAttributesWatch || d->modified)
1350     {
1351         // Don't lose modifications
1352 
1353         return;
1354     }
1355 
1356     d->metadataChangeIds << imageId;
1357     d->metadataChangeTimer->start();
1358 }
1359 
resetMetadataChangeInfo()1360 void ItemDescEditTab::resetMetadataChangeInfo()
1361 {
1362     d->metadataChangeTimer->stop();
1363     d->metadataChangeIds.clear();
1364 }
1365 
slotReloadForMetadataChange()1366 void ItemDescEditTab::slotReloadForMetadataChange()
1367 {
1368     // NOTE: What to do if d->modified? Reloading is no option.
1369     // It may be a little change the user wants to ignore, or a large conflict.
1370 
1371     if (d->currInfos.isEmpty() || d->modified)
1372     {
1373         resetMetadataChangeInfo();
1374         return;
1375     }
1376 
1377     if (singleSelection())
1378     {
1379         if (d->metadataChangeIds.contains(d->currInfos.first().id()))
1380         {
1381             setInfos(d->currInfos);
1382         }
1383     }
1384     else
1385     {
1386         // if image id is in our list, update
1387 
1388         foreach (const ItemInfo& info, d->currInfos)
1389         {
1390             if (d->metadataChangeIds.contains(info.id()))
1391             {
1392                 setInfos(d->currInfos);
1393                 break;
1394             }
1395         }
1396     }
1397 }
1398 
updateRecentTags()1399 void ItemDescEditTab::updateRecentTags()
1400 {
1401     QMenu* const menu = dynamic_cast<QMenu*>(d->recentTagsBtn->menu());
1402 
1403     if (!menu)
1404     {
1405         return;
1406     }
1407 
1408     menu->clear();
1409 
1410     AlbumList recentTags = AlbumManager::instance()->getRecentlyAssignedTags();
1411 
1412     if (recentTags.isEmpty())
1413     {
1414         QAction* const noTagsAction = menu->addAction(i18n("No Recently Assigned Tags"));
1415         noTagsAction->setEnabled(false);
1416     }
1417     else
1418     {
1419         for (AlbumList::const_iterator it = recentTags.constBegin() ;
1420              it != recentTags.constEnd() ; ++it)
1421         {
1422             TAlbum* const album = static_cast<TAlbum*>(*it);
1423 
1424             if (album)
1425             {
1426                 AlbumThumbnailLoader* const loader = AlbumThumbnailLoader::instance();
1427                 QPixmap                     icon;
1428 
1429                 if (!loader->getTagThumbnail(album, icon))
1430                 {
1431                     if (icon.isNull())
1432                     {
1433                         icon = loader->getStandardTagIcon(album, AlbumThumbnailLoader::SmallerSize);
1434                     }
1435                 }
1436 
1437                 TAlbum* const parent = dynamic_cast<TAlbum*> (album->parent());
1438 
1439                 if (parent)
1440                 {
1441                     QString text          = album->title() + QLatin1String(" (") + parent->prettyUrl() + QLatin1Char(')');
1442                     QAction* const action = menu->addAction(icon, text);
1443                     int id                = album->id();
1444 
1445                     connect(action, &QAction::triggered,
1446                             this, [this, id]() { slotRecentTagsMenuActivated(id); });
1447                 }
1448                 else
1449                 {
1450                     qCDebug(DIGIKAM_GENERAL_LOG) << "Tag" << album
1451                                                  << "do not have a valid parent";
1452                 }
1453             }
1454         }
1455     }
1456 }
1457 
slotRecentTagsMenuActivated(int id)1458 void ItemDescEditTab::slotRecentTagsMenuActivated(int id)
1459 {
1460     AlbumManager* const albumMan = AlbumManager::instance();
1461 
1462     if (id > 0)
1463     {
1464         TAlbum* const album = albumMan->findTAlbum(id);
1465 
1466         if (album)
1467         {
1468             d->tagModel->setChecked(album, true);
1469             d->tagCheckView->checkableAlbumFilterModel()->updateFilter();
1470         }
1471     }
1472 }
1473 
slotTagsSearchChanged(const SearchTextSettings & settings)1474 void ItemDescEditTab::slotTagsSearchChanged(const SearchTextSettings& settings)
1475 {
1476     Q_UNUSED(settings);
1477 
1478     // if we filter, we should reset the assignedTagsBtn again.
1479 
1480     if (d->assignedTagsBtn->isChecked() && !d->togglingSearchSettings)
1481     {
1482         d->togglingSearchSettings = true;
1483         d->assignedTagsBtn->setChecked(false);
1484         d->togglingSearchSettings = false;
1485     }
1486 }
1487 
slotAssignedTagsToggled(bool t)1488 void ItemDescEditTab::slotAssignedTagsToggled(bool t)
1489 {
1490     d->tagCheckView->checkableAlbumFilterModel()->setFilterChecked(t);
1491     d->tagCheckView->checkableAlbumFilterModel()->setFilterPartiallyChecked(t);
1492     d->tagCheckView->checkableAlbumFilterModel()->setFilterBehavior(t ? AlbumFilterModel::StrictFiltering
1493                                                                       : AlbumFilterModel::FullFiltering);
1494 
1495     if (t)
1496     {
1497         // if we filter by assigned, we should initially clear the normal search.
1498 
1499         if (!d->togglingSearchSettings)
1500         {
1501             d->togglingSearchSettings = true;
1502             d->tagsSearchBar->clear();
1503             d->togglingSearchSettings = false;
1504         }
1505 
1506         // Only after above change, do this.
1507 
1508         d->tagCheckView->expandMatches(d->tagCheckView->rootIndex());
1509    }
1510 }
1511 
slotApplyChangesToAllVersions()1512 void ItemDescEditTab::slotApplyChangesToAllVersions()
1513 {
1514     if (!d->modified)
1515     {
1516         return;
1517     }
1518 
1519     if (d->currInfos.isEmpty())
1520     {
1521         return;
1522     }
1523 
1524     QSet<qlonglong>                     tmpSet;
1525     QList<QPair<qlonglong, qlonglong> > relations;
1526 
1527     foreach (const ItemInfo& info, d->currInfos)
1528     {
1529         // Collect all ids in all image's relations.
1530 
1531         relations.append(info.relationCloud());
1532     }
1533 
1534     if (relations.isEmpty())
1535     {
1536         slotApplyAllChanges();
1537         return;
1538     }
1539 
1540     for (int i = 0 ; i < relations.size() ; ++i)
1541     {
1542         // Use QSet to prevent duplicates.
1543 
1544         tmpSet.insert(relations.at(i).first);
1545         tmpSet.insert(relations.at(i).second);
1546     }
1547 
1548     FileActionMngr::instance()->applyMetadata(ItemInfoList(tmpSet.values()), d->hub);
1549 
1550     d->modified = false;
1551     d->hub.resetChanged();
1552     d->applyBtn->setEnabled(false);
1553     d->revertBtn->setEnabled(false);
1554     d->applyToAllVersionsButton->setEnabled(false);
1555 }
1556 
initProgressIndicator()1557 void ItemDescEditTab::initProgressIndicator()
1558 {
1559     if (!ProgressManager::instance()->findItembyId(QLatin1String("ItemDescEditTabProgress")))
1560     {
1561         FileActionProgress* const item = new FileActionProgress(QLatin1String("ItemDescEditTabProgress"));
1562 
1563         connect(this, SIGNAL(signalProgressMessageChanged(QString)),
1564                 item, SLOT(slotProgressStatus(QString)));
1565 
1566         connect(this, SIGNAL(signalProgressValueChanged(float)),
1567                 item, SLOT(slotProgressValue(float)));
1568 
1569         connect(this, SIGNAL(signalProgressFinished()),
1570                 item, SLOT(slotCompleted()));
1571     }
1572 }
1573 
getNewTagEdit() const1574 AddTagsLineEdit* ItemDescEditTab::getNewTagEdit() const
1575 {
1576     return d->newTagEdit;
1577 }
1578 
1579 } // namespace Digikam
1580