File indexing completed on 2025-04-27 03:57:26

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2003-03-09
0007  * Description : Captions, Tags, and Rating properties editor
0008  *
0009  * SPDX-FileCopyrightText: 2003-2005 by Renchi Raju <renchi dot raju at gmail dot com>
0010  * SPDX-FileCopyrightText: 2003-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0011  * SPDX-FileCopyrightText: 2006-2011 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
0012  * SPDX-FileCopyrightText: 2009-2011 by Andi Clemens <andi dot clemens at gmail dot com>
0013  * SPDX-FileCopyrightText: 2009-2011 by Johannes Wienke <languitar at semipol dot de>
0014  * SPDX-FileCopyrightText: 2015      by Veaceslav Munteanu <veaceslav dot munteanu90 at gmail dot com>
0015  *
0016  * SPDX-License-Identifier: GPL-2.0-or-later
0017  *
0018  * ============================================================ */
0019 
0020 #include "itemdescedittab_p.h"
0021 
0022 namespace Digikam
0023 {
0024 
0025 ItemDescEditTab::ItemDescEditTab(QWidget* const parent)
0026     : DVBox(parent),
0027       d    (new Private(this))
0028 {
0029     d->hub                 = new DisjointMetadata;
0030 
0031     setContentsMargins(QMargins());
0032     setSpacing(d->spacing);
0033 
0034     d->tabWidget           = new QTabWidget(this);
0035 
0036     d->metadataChangeTimer = new QTimer(this);
0037     d->metadataChangeTimer->setSingleShot(true);
0038     d->metadataChangeTimer->setInterval(250);
0039 
0040     // Captions/Date/Rating view -----------------------------------
0041 
0042     initDescriptionView();
0043 
0044     // Tags view ---------------------------------------------------
0045 
0046     initTagsView();
0047 
0048     // Information Management View ---------------------------------
0049 
0050     initInformationView();
0051 
0052     // Initialize --------------------------------------------------
0053 
0054     d->setupEventFilters();
0055 
0056     updateRecentTags();
0057 
0058     // Setup signals/slots connctions -------------------------------
0059 
0060     d->setupConnections();
0061 }
0062 
0063 ItemDescEditTab::~ItemDescEditTab()
0064 {
0065     delete d->hub;
0066     delete d;
0067 }
0068 
0069 void ItemDescEditTab::setCurrentTab(int index)
0070 {
0071     d->tabWidget->setCurrentIndex(index);
0072 }
0073 
0074 int ItemDescEditTab::currentTab() const
0075 {
0076     return d->tabWidget->currentIndex();
0077 }
0078 
0079 void ItemDescEditTab::readSettings(KConfigGroup& group)
0080 {
0081     setCurrentTab(group.readEntry(QLatin1String("ItemDescEdit Tab"), (int)DESCRIPTIONS));
0082     d->titleEdit->setCurrentLanguageCode(group.readEntry(QLatin1String("ItemDescEditTab TitleLang"), QString()));
0083     d->captionsEdit->setCurrentLanguageCode(group.readEntry(QLatin1String("ItemDescEditTab CaptionsLang"), QString()));
0084 
0085     d->templateViewer->readSettings(group);
0086 
0087     d->tagCheckView->setConfigGroup(group);
0088     d->tagCheckView->setEntryPrefix(QLatin1String("ItemDescEditTab TagCheckView"));
0089     d->tagCheckView->loadState();
0090     d->tagsSearchBar->setConfigGroup(group);
0091     d->tagsSearchBar->setEntryPrefix(QLatin1String("ItemDescEditTab SearchBar"));
0092     d->tagsSearchBar->loadState();
0093 }
0094 
0095 void ItemDescEditTab::writeSettings(KConfigGroup& group)
0096 {
0097     group.writeEntry(QLatin1String("ItemDescEdit Tab"),             currentTab());
0098     group.writeEntry(QLatin1String("ItemDescEditTab TitleLang"),    d->titleEdit->currentLanguageCode());
0099     group.writeEntry(QLatin1String("ItemDescEditTab CaptionsLang"), d->captionsEdit->currentLanguageCode());
0100 
0101     d->templateViewer->writeSettings(group);
0102 
0103     d->tagCheckView->saveState();
0104     d->tagsSearchBar->saveState();
0105 }
0106 
0107 void ItemDescEditTab::setItem(const ItemInfo& info)
0108 {
0109     slotChangingItems();
0110     ItemInfoList list;
0111 
0112     if (!info.isNull())
0113     {
0114         list << info;
0115     }
0116 
0117     d->setInfos(list);
0118 }
0119 
0120 void ItemDescEditTab::setItems(const ItemInfoList& infos)
0121 {
0122     slotChangingItems();
0123     d->setInfos(infos);
0124 }
0125 
0126 bool ItemDescEditTab::eventFilter(QObject* o, QEvent* e)
0127 {
0128     if (e->type() == QEvent::KeyPress)
0129     {
0130         QKeyEvent* const k = static_cast<QKeyEvent*>(e);
0131 
0132         if ((k->key() == Qt::Key_Enter) || (k->key() == Qt::Key_Return))
0133         {
0134             if      (k->modifiers() == Qt::ControlModifier)
0135             {
0136                 d->lastSelectedWidget = qobject_cast<QWidget*>(o);
0137 
0138                 Q_EMIT signalNextItem();
0139 
0140                 return true;
0141             }
0142             else if (k->modifiers() == Qt::ShiftModifier)
0143             {
0144                 d->lastSelectedWidget = qobject_cast<QWidget*>(o);
0145 
0146                 Q_EMIT signalPrevItem();
0147 
0148                 return true;
0149             }
0150         }
0151 
0152         if (k->key() == Qt::Key_PageUp)
0153         {
0154             d->lastSelectedWidget = qobject_cast<QWidget*>(o);
0155 
0156             Q_EMIT signalPrevItem();
0157 
0158             return true;
0159         }
0160 
0161         if (k->key() == Qt::Key_PageDown)
0162         {
0163             d->lastSelectedWidget = qobject_cast<QWidget*>(o);
0164 
0165             Q_EMIT signalNextItem();
0166 
0167             return true;
0168         }
0169 
0170         if ((d->newTagEdit == o) &&
0171             !d->newTagEdit->completer()->popup()->isVisible())
0172         {
0173             if (k->key() == Qt::Key_Up)
0174             {
0175                 d->lastSelectedWidget = qobject_cast<QWidget*>(o);
0176 
0177                 Q_EMIT signalPrevItem();
0178 
0179                 return true;
0180             }
0181 
0182             if (k->key() == Qt::Key_Down)
0183             {
0184                 d->lastSelectedWidget = qobject_cast<QWidget*>(o);
0185 
0186                 Q_EMIT signalNextItem();
0187 
0188                 return true;
0189             }
0190         }
0191     }
0192 
0193     return DVBox::eventFilter(o, e);
0194 }
0195 
0196 bool ItemDescEditTab::isModified() const
0197 {
0198     return d->modified;
0199 }
0200 
0201 void ItemDescEditTab::slotChangingItems()
0202 {
0203     if (!d->modified)
0204     {
0205         return;
0206     }
0207 
0208     if (d->currInfos.isEmpty())
0209     {
0210         return;
0211     }
0212 
0213     if (!ApplicationSettings::instance()->getApplySidebarChangesDirectly())
0214     {
0215         // Open dialog via queued connection out-of-scope, see bug 302311
0216 
0217         DisjointMetadata* const hub2 = new DisjointMetadata();
0218         hub2->setDataFields(d->hub->dataFields());
0219 
0220         Q_EMIT signalAskToApplyChanges(d->currInfos, hub2);
0221 
0222         d->reset();
0223     }
0224     else
0225     {
0226         slotApplyAllChanges();
0227     }
0228 }
0229 
0230 void ItemDescEditTab::slotAskToApplyChanges(const QList<ItemInfo>& infos, DisjointMetadata* hub)
0231 {
0232     int changedFields = 0;
0233 
0234     if (hub->titlesChanged())
0235     {
0236         ++changedFields;
0237     }
0238 
0239     if (hub->commentsChanged())
0240     {
0241         ++changedFields;
0242     }
0243 
0244     if (hub->dateTimeChanged())
0245     {
0246         ++changedFields;
0247     }
0248 
0249     if (hub->ratingChanged())
0250     {
0251         ++changedFields;
0252     }
0253 
0254     if (hub->pickLabelChanged())
0255     {
0256         ++changedFields;
0257     }
0258 
0259     if (hub->colorLabelChanged())
0260     {
0261         ++changedFields;
0262     }
0263 
0264     if (hub->tagsChanged())
0265     {
0266         ++changedFields;
0267     }
0268 
0269     QString text;
0270 
0271     if (changedFields == 1)
0272     {
0273         if      (hub->commentsChanged())
0274         {
0275             text = i18ncp("@info", "You have edited the image caption. ",
0276                           "You have edited the captions of %1 images. ",
0277                           infos.count());
0278         }
0279         else if (hub->titlesChanged())
0280         {
0281             text = i18ncp("@info", "You have edited the image title. ",
0282                           "You have edited the titles of %1 images. ",
0283                           infos.count());
0284         }
0285         else if (hub->dateTimeChanged())
0286         {
0287             text = i18ncp("@info", "You have edited the date of the image. ",
0288                           "You have edited the date of %1 images. ",
0289                           infos.count());
0290         }
0291         else if (hub->pickLabelChanged())
0292         {
0293             text = i18ncp("@info", "You have edited the pick label of the image. ",
0294                           "You have edited the pick label of %1 images. ",
0295                           infos.count());
0296         }
0297         else if (hub->colorLabelChanged())
0298         {
0299             text = i18ncp("@info", "You have edited the color label of the image. ",
0300                           "You have edited the color label of %1 images. ",
0301                           infos.count());
0302         }
0303         else if (hub->ratingChanged())
0304         {
0305             text = i18ncp("@info", "You have edited the rating of the image. ",
0306                           "You have edited the rating of %1 images. ",
0307                           infos.count());
0308         }
0309         else if (hub->tagsChanged())
0310         {
0311             text = i18ncp("@info", "You have edited the tags of the image. ",
0312                           "You have edited the tags of %1 images. ",
0313                           infos.count());
0314         }
0315 
0316         text += i18nc("@info", "Do you want to apply your changes?");
0317     }
0318     else
0319     {
0320         text = i18ncp("@info", "<p>You have edited the metadata of the image: </p>",
0321                       "<p>You have edited the metadata of %1 images: </p>",
0322                       infos.count());
0323 
0324         text += QLatin1String("<p><ul>");
0325 
0326         if (hub->titlesChanged())
0327         {
0328             text += i18nc("@info", "<li>title</li>");
0329         }
0330 
0331         if (hub->commentsChanged())
0332         {
0333             text += i18nc("@info", "<li>caption</li>");
0334         }
0335 
0336         if (hub->dateTimeChanged())
0337         {
0338             text += i18nc("@info", "<li>date</li>");
0339         }
0340 
0341         if (hub->pickLabelChanged())
0342         {
0343             text += i18nc("@info", "<li>pick label</li>");
0344         }
0345 
0346         if (hub->colorLabelChanged())
0347         {
0348             text += i18nc("@info", "<li>color label</li>");
0349         }
0350 
0351         if (hub->ratingChanged())
0352         {
0353             text += i18nc("@info", "<li>rating</li>");
0354         }
0355 
0356         if (hub->tagsChanged())
0357         {
0358             text += i18nc("@info", "<li>tags</li>");
0359         }
0360 
0361         text += QLatin1String("</ul></p>");
0362 
0363         text += i18nc("@info", "<p>Do you want to apply your changes?</p>");
0364     }
0365 
0366     QCheckBox* const alwaysCBox  = new QCheckBox(i18nc("@action", "Always apply changes without confirmation"));
0367 
0368     QPointer<QMessageBox> msgBox = new QMessageBox(QMessageBox::Information,
0369                                                    i18nc("@title:window", "Apply Changes?"),
0370                                                    text,
0371                                                    QMessageBox::Yes | QMessageBox::No,
0372                                                    qApp->activeWindow());
0373     msgBox->setCheckBox(alwaysCBox);
0374     msgBox->setDefaultButton(QMessageBox::No);
0375     msgBox->setEscapeButton(QMessageBox::No);
0376 
0377     // Pop-up a message in desktop notification manager
0378 
0379     DNotificationWrapper(QString(), i18nc("@info", "Apply changes?"),
0380                          DigikamApp::instance(), DigikamApp::instance()->windowTitle());
0381 
0382     int returnCode   = msgBox->exec();
0383     bool alwaysApply = msgBox->checkBox()->isChecked();
0384     delete msgBox;
0385 
0386     if (alwaysApply)
0387     {
0388         ApplicationSettings::instance()->setApplySidebarChangesDirectly(true);
0389     }
0390 
0391     if (returnCode == QMessageBox::No)
0392     {
0393         delete hub;
0394 
0395         return;
0396     }
0397 
0398     // otherwise apply:
0399 
0400     FileActionMngr::instance()->applyMetadata(infos, hub);
0401 }
0402 
0403 void ItemDescEditTab::slotApplyAllChanges()
0404 {
0405     if (!d->modified)
0406     {
0407         return;
0408     }
0409 
0410     if (d->currInfos.isEmpty())
0411     {
0412         return;
0413     }
0414 
0415     FileActionMngr::instance()->applyMetadata(d->currInfos, *d->hub);
0416     d->reset();
0417 }
0418 
0419 void ItemDescEditTab::slotRevertAllChanges()
0420 {
0421     if (!d->modified)
0422     {
0423         return;
0424     }
0425 
0426     if (d->currInfos.isEmpty())
0427     {
0428         return;
0429     }
0430 
0431     d->setInfos(d->currInfos);
0432 }
0433 
0434 void ItemDescEditTab::slotReadFromFileMetadataToDatabase()
0435 {
0436     d->initProgressIndicator();
0437 
0438     Q_EMIT signalProgressMessageChanged(i18nc("@info", "Reading metadata from files. Please wait..."));
0439 
0440     d->ignoreItemAttributesWatch = true;
0441     int i                        = 0;
0442 
0443     ScanController::instance()->suspendCollectionScan();
0444 
0445     CollectionScanner scanner;
0446 
0447     Q_FOREACH (const ItemInfo& info, d->currInfos)
0448     {
0449         scanner.scanFile(info, CollectionScanner::CleanScan);
0450 
0451         Q_EMIT signalProgressValueChanged(i++ / (float)d->currInfos.count());
0452 
0453         qApp->processEvents();
0454     }
0455 
0456     ScanController::instance()->resumeCollectionScan();
0457     d->ignoreItemAttributesWatch = false;
0458 
0459     Q_EMIT signalProgressFinished();
0460 
0461     // reload everything
0462 
0463     d->setInfos(d->currInfos);
0464 }
0465 
0466 void ItemDescEditTab::slotWriteToFileMetadataFromDatabase()
0467 {
0468     d->initProgressIndicator();
0469 
0470     Q_EMIT signalProgressMessageChanged(i18nc("@info", "Writing metadata to files. Please wait..."));
0471 
0472     int i = 0;
0473 
0474     Q_FOREACH (const ItemInfo& info, d->currInfos)
0475     {
0476         MetadataHub hub;
0477 
0478         // read in from database
0479 
0480         hub.load(info);
0481 
0482         // write out to file DMetadata
0483 
0484         ScanController::FileMetadataWrite writeScope(info);
0485         writeScope.changed(hub.writeToMetadata(info, MetadataHub::WRITE_ALL));
0486 
0487         Q_EMIT signalProgressValueChanged(i++ / (float)d->currInfos.count());
0488 
0489         qApp->processEvents();
0490     }
0491 
0492     Q_EMIT signalProgressFinished();
0493 }
0494 
0495 void ItemDescEditTab::slotModified()
0496 {
0497     d->modified = true;
0498     d->applyBtn->setEnabled(true);
0499     d->revertBtn->setEnabled(true);
0500 
0501     if (d->currInfos.size() == 1)
0502     {
0503         d->applyToAllVersionsButton->setEnabled(true);
0504     }
0505 }
0506 
0507 void ItemDescEditTab::slotMoreMenu()
0508 {
0509     d->moreMenu->clear();
0510 
0511     if (d->singleSelection())
0512     {
0513         d->moreMenu->addAction(i18nc("@action", "Read metadata from file to database"),
0514                                this, SLOT(slotReadFromFileMetadataToDatabase()));
0515         QAction* const writeAction = d->moreMenu->addAction(i18nc("@action", "Write metadata to each file"),
0516                                                             this, SLOT(slotWriteToFileMetadataFromDatabase()));
0517 
0518         // we do not need a "Write to file" action here because the apply button will do just that
0519         // if selection is a single file.
0520         // Adding the option will confuse users: Does the apply button not write to file?
0521         // Removing the option will confuse users: There is not option to write to file! (not visible in single selection)
0522         // Disabling will confuse users: Why is it disabled?
0523 
0524         writeAction->setEnabled(false);
0525     }
0526     else
0527     {
0528         // We need to make clear that this action is different from the Apply button,
0529         // which saves the same changes to all files. These batch operations operate on each single file.
0530 
0531         d->moreMenu->addAction(i18nc("@action", "Read metadata from each file to database"),
0532                                this, SLOT(slotReadFromFileMetadataToDatabase()));
0533 
0534         d->moreMenu->addAction(i18nc("@action", "Write metadata to each file"),
0535                                this, SLOT(slotWriteToFileMetadataFromDatabase()));
0536 
0537         d->moreMenu->addSeparator();
0538 
0539         d->moreMenu->addAction(i18nc("@action", "Unify tags of selected items"),
0540                                this, SLOT(slotUnifyPartiallyTags()));
0541     }
0542 }
0543 
0544 void ItemDescEditTab::slotImagesChanged(int albumId)
0545 {
0546     if (d->ignoreItemAttributesWatch || d->modified)
0547     {
0548         return;
0549     }
0550 
0551     Album* const a = AlbumManager::instance()->findAlbum(albumId);
0552 
0553     if (d->currInfos.isEmpty() || !a || a->isRoot() || (a->type() != Album::TAG))
0554     {
0555         return;
0556     }
0557 
0558     d->setInfos(d->currInfos);
0559 }
0560 
0561 void ItemDescEditTab::slotReloadForMetadataChange()
0562 {
0563     // NOTE: What to do if d->modified? Reloading is no option.
0564     // It may be a little change the user wants to ignore, or a large conflict.
0565 
0566     if (d->currInfos.isEmpty() || d->modified)
0567     {
0568         d->resetMetadataChangeInfo();
0569 
0570         return;
0571     }
0572 
0573     if (d->singleSelection())
0574     {
0575         if (d->metadataChangeIds.contains(d->currInfos.first().id()))
0576         {
0577             d->setInfos(d->currInfos);
0578         }
0579     }
0580     else
0581     {
0582         // if image id is in our list, update
0583 
0584         Q_FOREACH (const ItemInfo& info, d->currInfos)
0585         {
0586             if (d->metadataChangeIds.contains(info.id()))
0587             {   // cppcheck-suppress useStlAlgorithm
0588                 d->setInfos(d->currInfos);
0589                 break;
0590             }
0591         }
0592     }
0593 }
0594 
0595 void ItemDescEditTab::slotApplyChangesToAllVersions()
0596 {
0597     if (!d->modified)
0598     {
0599         return;
0600     }
0601 
0602     if (d->currInfos.isEmpty())
0603     {
0604         return;
0605     }
0606 
0607     QSet<qlonglong>                     tmpSet;
0608     QList<QPair<qlonglong, qlonglong> > relations;
0609 
0610     Q_FOREACH (const ItemInfo& info, d->currInfos)
0611     {
0612         // Collect all ids in all image's relations.
0613 
0614         relations.append(info.relationCloud());
0615     }
0616 
0617     if (relations.isEmpty())
0618     {
0619         slotApplyAllChanges();
0620         return;
0621     }
0622 
0623     for (int i = 0 ; i < relations.size() ; ++i)
0624     {
0625         // Use QSet to prevent duplicates.
0626 
0627         tmpSet.insert(relations.at(i).first);
0628         tmpSet.insert(relations.at(i).second);
0629     }
0630 
0631     FileActionMngr::instance()->applyMetadata(ItemInfoList(tmpSet.values()), *d->hub);
0632 
0633     d->modified = false;
0634     d->hub->resetChanged();
0635     d->applyBtn->setEnabled(false);
0636     d->revertBtn->setEnabled(false);
0637     d->applyToAllVersionsButton->setEnabled(false);
0638 }
0639 
0640 } // namespace Digikam
0641 
0642 #include "moc_itemdescedittab.cpp"