1 /* ============================================================
2 *
3 * This file is a part of digiKam project
4 * https://www.digikam.org
5 *
6 * Date : 2000-12-05
7 * Description : helper class used to modify tag albums in views
8 *
9 * Copyright (C) 2009-2010 by Johannes Wienke <languitar at semipol dot de>
10 * Copyright (C) 2010-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
11 *
12 * This program is free software; you can redistribute it
13 * and/or modify it under the terms of the GNU General
14 * Public License as published by the Free Software Foundation;
15 * either version 2, or (at your option)
16 * any later version.
17 *
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * ============================================================ */
24
25 #include "tagmodificationhelper.h"
26
27 // Qt includes
28
29 #include <QApplication>
30 #include <QAction>
31 #include <QMessageBox>
32
33 // KDE includes
34
35 #include <klocalizedstring.h>
36
37 // Local includes
38
39 #include "digikam_debug.h"
40 #include "album.h"
41 #include "albumpointer.h"
42 #include "coredb.h"
43 #include "coredbtransaction.h"
44 #include "iteminfo.h"
45 #include "itemtagpair.h"
46 #include "metadatahub.h"
47 #include "scancontroller.h"
48 #include "statusprogressbar.h"
49 #include "tagsactionmngr.h"
50 #include "tagproperties.h"
51 #include "tageditdlg.h"
52 #include "facetags.h"
53 #include "facedbaccess.h"
54 #include "facedb.h"
55
56 namespace Digikam
57 {
58
59 class Q_DECL_HIDDEN TagModificationHelper::Private
60 {
61 public:
62
Private()63 explicit Private()
64 : parentTag (nullptr),
65 dialogParent(nullptr)
66 {
67 }
68
69 AlbumPointer<TAlbum> parentTag;
70 QWidget* dialogParent;
71 };
72
TagModificationHelper(QObject * const parent,QWidget * const dialogParent)73 TagModificationHelper::TagModificationHelper(QObject* const parent, QWidget* const dialogParent)
74 : QObject(parent),
75 d (new Private)
76 {
77 d->dialogParent = dialogParent;
78 }
79
~TagModificationHelper()80 TagModificationHelper::~TagModificationHelper()
81 {
82 delete d;
83 }
84
bindTag(QAction * action,TAlbum * album) const85 void TagModificationHelper::bindTag(QAction* action, TAlbum* album) const
86 {
87 action->setData(QVariant::fromValue(AlbumPointer<TAlbum>(album)));
88 }
89
boundTag(QObject * sender) const90 TAlbum* TagModificationHelper::boundTag(QObject* sender) const
91 {
92 QAction* action = nullptr;
93
94 if ((action = qobject_cast<QAction*>(sender)))
95 {
96 return action->data().value<AlbumPointer<TAlbum> >();
97 }
98
99 return nullptr;
100 }
101
bindMultipleTags(QAction * action,const QList<TAlbum * > & tags)102 void TagModificationHelper::bindMultipleTags(QAction* action, const QList<TAlbum*>& tags)
103 {
104 action->setData(QVariant::fromValue(tags));
105 }
106
boundMultipleTags(QObject * sender)107 QList<TAlbum*> TagModificationHelper::boundMultipleTags(QObject* sender)
108 {
109 QAction* action = nullptr;
110
111 if ((action = qobject_cast<QAction*>(sender)))
112 {
113 return (action->data().value<QList<TAlbum*> >());
114 }
115
116 return QList<TAlbum*>();
117 }
118
slotTagNew(TAlbum * parent,const QString & title,const QString & iconName)119 TAlbum* TagModificationHelper::slotTagNew(TAlbum* parent, const QString& title, const QString& iconName)
120 {
121 // ensure that there is a parent
122
123 AlbumPointer<TAlbum> p(parent);
124
125 if (!p)
126 {
127 p = AlbumManager::instance()->findTAlbum(0);
128
129 if (!p)
130 {
131 qCDebug(DIGIKAM_GENERAL_LOG) << "Could not find root tag album";
132 return nullptr;
133 }
134 }
135
136 QString editTitle = title;
137 QString editIconName = iconName;
138 QKeySequence ks;
139
140 if (title.isEmpty())
141 {
142 bool doCreate = TagEditDlg::tagCreate(d->dialogParent, p, editTitle, editIconName, ks);
143
144 if (!doCreate || !p)
145 {
146 return nullptr;
147 }
148 }
149
150 QMap<QString, QString> errMap;
151 AlbumList tList = TagEditDlg::createTAlbum(p, editTitle, editIconName, ks, errMap);
152 TagEditDlg::showtagsListCreationError(d->dialogParent, errMap);
153
154 if (errMap.isEmpty() && !tList.isEmpty())
155 {
156 TAlbum* tag = nullptr;
157
158 foreach (Album* const album, tList)
159 {
160 tag = static_cast<TAlbum*>(album);
161 emit tagCreated(tag);
162 }
163
164 return tag;
165 }
166 else
167 {
168 return nullptr;
169 }
170 }
171
slotTagNew()172 TAlbum* TagModificationHelper::slotTagNew()
173 {
174 return slotTagNew(boundTag(sender()));
175 }
176
slotTagEdit(TAlbum * t)177 void TagModificationHelper::slotTagEdit(TAlbum* t)
178 {
179 if (!t)
180 {
181 return;
182 }
183
184 AlbumPointer<TAlbum> tag(t);
185
186 QString title, icon;
187 QKeySequence ks;
188
189 bool doEdit = TagEditDlg::tagEdit(d->dialogParent, tag, title, icon, ks);
190
191 if (!doEdit || !tag)
192 {
193 return;
194 }
195
196 if (tag->title() != title)
197 {
198 QString errMsg;
199
200 if (AlbumManager::instance()->renameTAlbum(tag, title, errMsg))
201 {
202 // TODO: make an option to edit the full name of a face tag
203
204 if (FaceTags::isPerson(tag->id()))
205 {
206 TagProperties props(tag->id());
207 props.setProperty(TagPropertyName::person(), title);
208 props.setProperty(TagPropertyName::faceEngineName(), title);
209 }
210 }
211 else
212 {
213 QMessageBox::critical(qApp->activeWindow(), qApp->applicationName(), errMsg);
214 }
215 }
216
217 if (tag->icon() != icon)
218 {
219 QString errMsg;
220
221 if (!AlbumManager::instance()->updateTAlbumIcon(tag, icon, 0, errMsg))
222 {
223 QMessageBox::critical(qApp->activeWindow(), qApp->applicationName(), errMsg);
224 }
225 }
226
227 if (tag->property(TagPropertyName::tagKeyboardShortcut()) != ks.toString())
228 {
229 TagsActionMngr::defaultManager()->updateTagShortcut(tag->id(), ks);
230 }
231
232 emit tagEdited(tag);
233 }
234
slotTagEdit()235 void TagModificationHelper::slotTagEdit()
236 {
237 slotTagEdit(boundTag(sender()));
238 }
239
slotTagDelete(TAlbum * t)240 void TagModificationHelper::slotTagDelete(TAlbum* t)
241 {
242 if (!t || t->isRoot())
243 {
244 return;
245 }
246
247 AlbumPointer<TAlbum> tag(t);
248
249 // find number of subtags
250
251 int children = 0;
252 AlbumIterator iter(tag);
253
254 while (iter.current())
255 {
256 ++children;
257 ++iter;
258 }
259
260 // ask for deletion of children
261
262 if (children)
263 {
264 int result = QMessageBox::warning(d->dialogParent, qApp->applicationName(),
265 i18np("Tag '%2' has one subtag. "
266 "Deleting this will also delete "
267 "the subtag.\n"
268 "Do you want to continue?",
269 "Tag '%2' has %1 subtags. "
270 "Deleting this will also delete "
271 "the subtags.\n"
272 "Do you want to continue?",
273 children,
274 tag->title()),
275 QMessageBox::Yes | QMessageBox::Cancel);
276
277 if ((result != QMessageBox::Yes) || !tag)
278 {
279 return;
280 }
281 }
282
283 QString message;
284 QList<qlonglong> assignedItems = CoreDbAccess().db()->getItemIDsInTag(tag->id());
285
286 if (!assignedItems.isEmpty())
287 {
288 message = i18np("Tag '%2' is assigned to one item. "
289 "Do you want to continue?",
290 "Tag '%2' is assigned to %1 items. "
291 "Do you want to continue?",
292 assignedItems.count(), tag->title());
293 }
294 else
295 {
296 message = i18n("Delete '%1' tag?", tag->title());
297 }
298
299 int result = QMessageBox::warning(qApp->activeWindow(), i18n("Delete Tag"),
300 message,
301 QMessageBox::Yes | QMessageBox::Cancel);
302
303 if (result == QMessageBox::Yes && tag)
304 {
305 emit aboutToDeleteTag(tag);
306 QString errMsg;
307
308 if (!AlbumManager::instance()->deleteTAlbum(tag, errMsg))
309 {
310 QMessageBox::critical(qApp->activeWindow(), qApp->applicationName(), errMsg);
311 }
312 }
313 }
314
slotTagDelete()315 void TagModificationHelper::slotTagDelete()
316 {
317 slotTagDelete(boundTag(sender()));
318 }
319
slotMultipleTagDel(const QList<TAlbum * > & tags)320 void TagModificationHelper::slotMultipleTagDel(const QList<TAlbum*>& tags)
321 {
322 QString tagWithChildrens;
323 QString tagWithoutImages;
324 QString tagWithImages;
325 QMultiMap<int, TAlbum*> sortedTags;
326
327 foreach (TAlbum* const t, tags)
328 {
329 if (!t || t->isRoot())
330 {
331 continue;
332 }
333
334 AlbumPointer<TAlbum> tag(t);
335
336 // find number of subtags
337
338 int children = 0;
339 AlbumIterator iter(tag);
340
341 while (iter.current())
342 {
343 ++children;
344 ++iter;
345 }
346
347 if (children)
348 {
349 tagWithChildrens.append(tag->title() + QLatin1Char(' '));
350 }
351
352 QList<qlonglong> assignedItems = CoreDbAccess().db()->getItemIDsInTag(tag->id());
353
354 if (!assignedItems.isEmpty())
355 {
356 tagWithImages.append(tag->title() + QLatin1Char(' '));
357 }
358 else
359 {
360 tagWithoutImages.append(tag->title() + QLatin1Char(' '));
361 }
362
363 /**
364 * Tags must be deleted from children to parents, if we don't want
365 * to step on invalid index. Use QMultiMap to order them by distance
366 * to root tag
367 */
368
369 Album* parent = t;
370 int depth = 0;
371
372 while (!parent->isRoot())
373 {
374 parent = parent->parent();
375 depth++;
376 }
377
378 sortedTags.insert(depth, tag);
379 }
380
381 // ask for deletion of children
382
383 if (!tagWithChildrens.isEmpty())
384 {
385 int result = QMessageBox::warning(qApp->activeWindow(), qApp->applicationName(),
386 i18n("Tags '%1' have one or more subtags. "
387 "Deleting them will also delete "
388 "the subtags.\n"
389 "Do you want to continue?",
390 tagWithChildrens),
391 QMessageBox::Yes | QMessageBox::Cancel);
392
393 if (result != QMessageBox::Yes)
394 {
395 return;
396 }
397 }
398
399 QString message;
400
401 if (!tagWithImages.isEmpty())
402 {
403 message = i18n("Tags '%1' are assigned to one or more items. "
404 "Do you want to continue?",
405 tagWithImages);
406 }
407 else
408 {
409 message = i18n("Delete '%1' tag(s)?", tagWithoutImages);
410 }
411
412 int result = QMessageBox::warning(qApp->activeWindow(), i18n("Delete Tag"),
413 message,
414 QMessageBox::Yes | QMessageBox::Cancel);
415
416 if (result == QMessageBox::Yes)
417 {
418 QMultiMap<int, TAlbum*>::iterator it;
419
420 /**
421 * QMultimap doesn't provide reverse iterator, -1 is required
422 * because end() points after the last element
423 */
424
425 for (it = sortedTags.end()-1 ; it != sortedTags.begin()-1 ; --it)
426 {
427 emit aboutToDeleteTag(it.value());
428 QString errMsg;
429
430 if (!AlbumManager::instance()->deleteTAlbum(it.value(), errMsg))
431 {
432 QMessageBox::critical(qApp->activeWindow(), qApp->applicationName(), errMsg);
433 }
434 }
435 }
436 }
437
slotMultipleTagDel()438 void TagModificationHelper::slotMultipleTagDel()
439 {
440 QList<TAlbum*> lst = boundMultipleTags(sender());
441 qCDebug(DIGIKAM_GENERAL_LOG) << lst.size();
442 slotMultipleTagDel(lst);
443 }
444
slotFaceTagDelete(TAlbum * t)445 void TagModificationHelper::slotFaceTagDelete(TAlbum* t)
446 {
447 QList<TAlbum*> tag;
448 tag.append(t);
449 slotMultipleFaceTagDel(tag);
450 }
451
slotFaceTagDelete()452 void TagModificationHelper::slotFaceTagDelete()
453 {
454 slotFaceTagDelete(boundTag(sender()));
455 }
456
slotMultipleFaceTagDel(const QList<TAlbum * > & tags)457 void TagModificationHelper::slotMultipleFaceTagDel(const QList<TAlbum*>& tags)
458 {
459 QString tagsWithChildren;
460 QString tagsWithImages;
461
462 // We use a set here since else one tag could occur more than once
463 // which could lead to undefined behaviour.
464
465 QList<TAlbum*> allPersonTagsToDelete;
466 int tagsWithChildrenCount = 0;
467 QList<qlonglong> allAssignedItems;
468 int tagsWithImagesCount = 0;
469
470 foreach (TAlbum* const selectedTag, tags)
471 {
472 if (!selectedTag ||
473 selectedTag->isRoot() ||
474 (selectedTag->id() == FaceTags::unknownPersonTagId()) ||
475 (selectedTag->id() == FaceTags::ignoredPersonTagId()) ||
476 (selectedTag->id() == FaceTags::unconfirmedPersonTagId()))
477 {
478 continue;
479 }
480
481 // find tags and subtags with person property
482
483 QList<TAlbum*> personTagsToDelete = getFaceTags(selectedTag);
484
485 // If there is more than one person tag in the list,
486 // the tag to remove has at least one sub tag that is a face tag.
487 // Thus, we have to warn.
488 //
489 // If there is only one face tag, it is either the original tag,
490 // or it is the only sub tag of the original tag.
491 // Behave, like the face tag itself was selected.
492
493 if (personTagsToDelete.size() > 1)
494 {
495 if (tagsWithChildrenCount > 0)
496 {
497 tagsWithChildren.append(QLatin1String(" , "));
498 }
499
500 tagsWithChildren.append(selectedTag->title());
501 ++tagsWithChildrenCount;
502 }
503
504 // Get the assigned faces for all person tags to delete
505
506 foreach (TAlbum* const tAlbum, personTagsToDelete)
507 {
508 // If the global set does not yet contain the tag
509
510 if (!allPersonTagsToDelete.contains(tAlbum))
511 {
512 QList<qlonglong> assignedItems = CoreDbAccess().db()->getImagesWithImageTagProperty(
513 tAlbum->id(), ImageTagPropertyName::tagRegion());
514
515 QList<qlonglong> autodetected = CoreDbAccess().db()->getImagesWithImageTagProperty(
516 tAlbum->id(), ImageTagPropertyName::autodetectedFace());
517
518 foreach (const qlonglong& id1, autodetected)
519 {
520 if (!assignedItems.contains(id1))
521 {
522 assignedItems << id1;
523 }
524 }
525
526 if (!assignedItems.isEmpty())
527 {
528 // Add the items to the global set for potential untagging
529
530 foreach (const qlonglong& id2, assignedItems)
531 {
532 if (!allAssignedItems.contains(id2))
533 {
534 allAssignedItems << id2;
535 }
536 }
537
538 if (tagsWithImagesCount > 0)
539 {
540 tagsWithImages.append(QLatin1String(" , "));
541 }
542
543 tagsWithImages.append(tAlbum->title());
544 ++tagsWithImagesCount;
545 }
546 }
547 }
548
549 // Add the found tags to the global set.
550
551 foreach (TAlbum* const album, personTagsToDelete)
552 {
553 if (!allPersonTagsToDelete.contains(album))
554 {
555 allPersonTagsToDelete << album;
556 }
557 }
558 }
559
560 if (allPersonTagsToDelete.isEmpty() && allAssignedItems.isEmpty())
561 {
562 return;
563 }
564
565 // ask for deletion of children
566
567 if (tagsWithChildrenCount)
568 {
569 QString message = i18np("Face tag '%2' has at least one face tag child. "
570 "Removing it will also remove the children.\n"
571 "Do you want to continue?",
572 "Face tags '%2' have at least one face tag child. "
573 "Removing it will also remove the children.\n"
574 "Do you want to continue?",
575 tagsWithChildrenCount, tagsWithChildren);
576
577 bool removeChildren = (QMessageBox::Yes == QMessageBox::warning(qApp->activeWindow(),
578 qApp->applicationName(), message,
579 QMessageBox::Yes | QMessageBox::Cancel));
580
581 if (!removeChildren)
582 {
583 return;
584 }
585 }
586
587 QString message;
588
589 if (!allAssignedItems.isEmpty())
590 {
591 message = i18np("Face tag '%2' is assigned to at least one item. "
592 "Do you want to continue?",
593 "Face tags '%2' are assigned to at least one item. "
594 "Do you want to continue?",
595 tagsWithImagesCount, tagsWithImages);
596 }
597 else
598 {
599 message = i18np("Remove face tag?", "Remove face tags?", tags.size());
600 }
601
602 bool removeFaceTag = (QMessageBox::Yes == QMessageBox::warning(qApp->activeWindow(),
603 qApp->applicationName(), message,
604 QMessageBox::Yes | QMessageBox::Cancel));
605
606 if (removeFaceTag)
607 {
608 // Now we ask the user if we should also remove the tags from the images.
609
610 QString msg = i18np("Remove the tag corresponding to this face tag from the images?",
611 "Remove the %1 tags corresponding to this face tags from the images?",
612 allPersonTagsToDelete.size());
613
614 bool removeTagFromImages = (QMessageBox::Yes == QMessageBox::warning(qApp->activeWindow(),
615 qApp->applicationName(), msg,
616 QMessageBox::Yes | QMessageBox::No));
617
618 MetadataHub metadataHub;
619
620 // remove the face region from images and unassign the tag if wished
621
622 foreach (const qlonglong& imageId, allAssignedItems)
623 {
624 foreach (TAlbum* const tagToRemove, allPersonTagsToDelete)
625 {
626 ItemTagPair imageTagAssociation(imageId, tagToRemove->id());
627
628 if (imageTagAssociation.isAssigned())
629 {
630 imageTagAssociation.removeProperties(ImageTagPropertyName::autodetectedFace());
631 imageTagAssociation.removeProperties(ImageTagPropertyName::tagRegion());
632
633 if (removeTagFromImages)
634 {
635 imageTagAssociation.unAssignTag();
636
637 // Load the current metadata and sync the tags
638
639 ItemInfo info(imageId);
640
641 if (!info.isNull())
642 {
643 metadataHub.load(info);
644
645 if (!metadataHub.writeToMetadata(info))
646 {
647 qCWarning(DIGIKAM_GENERAL_LOG) << "Tags in image not changed:" << info.filePath();
648 }
649 }
650 }
651 }
652 }
653 }
654
655 foreach (TAlbum* const tAlbum, allPersonTagsToDelete)
656 {
657 TagProperties props(tAlbum->id());
658
659 // Delete TagPropertyName::person() and TagPropertyName::faceEngineName()
660 // fetch the UUID to delete the identity from facesdb
661
662 props.removeProperties(TagPropertyName::person());
663 props.removeProperties(TagPropertyName::faceEngineName());
664 QString uuid = props.value(TagPropertyName::faceEngineUuid());
665 qCDebug(DIGIKAM_GENERAL_LOG) << "Remove person tag properties for tag "
666 << tAlbum->title() << " with uuid " << uuid;
667
668 if (!uuid.isEmpty())
669 {
670 // Delete the UUID
671
672 props.removeProperties(TagPropertyName::faceEngineUuid());
673
674 // delete the faces db identity with this uuid.
675
676 FaceDbAccess access;
677 access.db()->deleteIdentity(uuid);
678 }
679
680 // reset tag icon
681
682 QString errMsg;
683 AlbumManager::instance()->updateTAlbumIcon(tAlbum, tAlbum->standardIconName(), 0, errMsg);
684 }
685 }
686 }
687
slotMultipleFaceTagDel()688 void TagModificationHelper::slotMultipleFaceTagDel()
689 {
690 QList<TAlbum*> lst = boundMultipleTags(sender());
691 qCDebug(DIGIKAM_GENERAL_LOG) << lst.size();
692 slotMultipleFaceTagDel(lst);
693 }
694
slotTagToFaceTag(TAlbum * tAlbum)695 void TagModificationHelper::slotTagToFaceTag(TAlbum* tAlbum)
696 {
697 if (!tAlbum)
698 {
699 return;
700 }
701
702 if (!FaceTags::isPerson(tAlbum->id()))
703 {
704 FaceTags::ensureIsPerson(tAlbum->id());
705
706 // reset tag icon
707
708 QString errMsg;
709 AlbumManager::instance()->updateTAlbumIcon(tAlbum, tAlbum->standardIconName(), 0, errMsg);
710 }
711 }
712
slotTagToFaceTag()713 void TagModificationHelper::slotTagToFaceTag()
714 {
715 slotTagToFaceTag(boundTag(sender()));
716 }
717
slotMultipleTagsToFaceTags(const QList<TAlbum * > & tags)718 void TagModificationHelper::slotMultipleTagsToFaceTags(const QList<TAlbum*>& tags)
719 {
720 foreach (TAlbum* const selectedTag, tags)
721 {
722 slotTagToFaceTag(selectedTag);
723 }
724 }
725
slotMultipleTagsToFaceTags()726 void TagModificationHelper::slotMultipleTagsToFaceTags()
727 {
728 QList<TAlbum*> lst = boundMultipleTags(sender());
729 qCDebug(DIGIKAM_GENERAL_LOG) << lst.size();
730 slotMultipleTagsToFaceTags(lst);
731 }
732
getFaceTags(TAlbum * rootTag)733 QList<TAlbum*> TagModificationHelper::getFaceTags(TAlbum* rootTag)
734 {
735 if (!rootTag)
736 {
737 return QList<TAlbum*>();
738 }
739
740 QList<TAlbum*> tags;
741 tags.append(rootTag);
742
743 return getFaceTags(tags).values();
744 }
745
getFaceTags(const QList<TAlbum * > & tags)746 QSet<TAlbum*> TagModificationHelper::getFaceTags(const QList<TAlbum*>& tags)
747 {
748 QSet<TAlbum*> faceTags;
749
750 foreach (TAlbum* const tAlbum, tags)
751 {
752 if (FaceTags::isPerson(tAlbum->id()))
753 {
754 faceTags.insert(tAlbum);
755 }
756
757 AlbumPointer<TAlbum> tag(tAlbum);
758 AlbumIterator iter(tag);
759
760 // Get all child tags which have the person property.
761
762 while (iter.current())
763 {
764 Album* const album = iter.current();
765 TAlbum* const tAlbum2 = dynamic_cast<TAlbum*>(album);
766
767 // Make sure that no nullp pointer dereference is done.
768 // though while(iter.current()) already tests for the current
769 // album being true, i.e. > 0 , i.e. non-null
770
771 if (tAlbum2 && FaceTags::isPerson(tAlbum2->id()))
772 {
773 faceTags.insert(tAlbum2);
774 }
775
776 ++iter;
777 }
778 }
779
780 return faceTags;
781 }
782
783 } // namespace Digikam
784