File indexing completed on 2025-04-27 03:58:34

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2021-04-18
0007  * Description : ExifTool metadata widget.
0008  *
0009  * SPDX-FileCopyrightText: 2021-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0010  *
0011  * SPDX-License-Identifier: GPL-2.0-or-later
0012  *
0013  * ============================================================ */
0014 
0015 #include "exiftoolwidget.h"
0016 
0017 // Qt includes
0018 
0019 #include <QLabel>
0020 #include <QGridLayout>
0021 #include <QPushButton>
0022 #include <QToolButton>
0023 #include <QMenu>
0024 #include <QMimeData>
0025 #include <QApplication>
0026 #include <QPrinter>
0027 #include <QPrintDialog>
0028 #include <QTextDocument>
0029 #include <QClipboard>
0030 #include <QStyle>
0031 #include <QPointer>
0032 #include <QFile>
0033 #include <QTimer>
0034 #include <QStandardPaths>
0035 #include <QTextStream>
0036 #include <QActionGroup>
0037 
0038 // KDE includes
0039 
0040 #include <klocalizedstring.h>
0041 
0042 // Local includes
0043 
0044 #include "exiftoollistviewgroup.h"
0045 #include "exiftoollistviewitem.h"
0046 #include "exiftoollistview.h"
0047 #include "exiftoolerrorview.h"
0048 #include "exiftoolloadingview.h"
0049 #include "searchtextbar.h"
0050 #include "dfiledialog.h"
0051 
0052 namespace Digikam
0053 {
0054 
0055 namespace
0056 {
0057 
0058 /**
0059  * Standard ExifTool entry list from the less important to the most important for photograph.
0060  */
0061 static const char* StandardExifToolEntryList[] =
0062 {
0063     "File",
0064     "Composite",
0065     "EXIF",
0066     "IPTC",
0067     "XMP",
0068     "Makernotes",
0069     "ICC Profile",
0070     "JFIF",
0071     "-1"
0072 };
0073 
0074 } // namespace
0075 
0076 class Q_DECL_HIDDEN ExifToolWidget::Private
0077 {
0078 public:
0079 
0080     enum ViewMode
0081     {
0082         LoadingView = 0,
0083         MetadataView,
0084         ErrorView
0085     };
0086 
0087 public:
0088 
0089     explicit Private()
0090         : noneAction      (nullptr),
0091           photoAction     (nullptr),
0092           customAction    (nullptr),
0093           settingsAction  (nullptr),
0094           metadataView    (nullptr),
0095           loadingView     (nullptr),
0096           view            (nullptr),
0097           errorView       (nullptr),
0098           searchBar       (nullptr),
0099           filterBtn       (nullptr),
0100           toolBtn         (nullptr),
0101           saveMetadata    (nullptr),
0102           printMetadata   (nullptr),
0103           copy2ClipBoard  (nullptr),
0104           optionsMenu     (nullptr),
0105           preLoadingTimer (nullptr)
0106     {
0107         for (int i = 0 ; QLatin1String(StandardExifToolEntryList[i]) != QLatin1String("-1") ; ++i)
0108         {
0109             keysFilter << QLatin1String(StandardExifToolEntryList[i]);
0110         }
0111     }
0112 
0113     QAction*             noneAction;
0114     QAction*             photoAction;
0115     QAction*             customAction;
0116     QAction*             settingsAction;
0117 
0118     QWidget*             metadataView;
0119     ExifToolLoadingView* loadingView;
0120     ExifToolListView*    view;
0121     ExifToolErrorView*   errorView;
0122     SearchTextBar*       searchBar;
0123 
0124     QString              fileName;
0125 
0126     QToolButton*         filterBtn;
0127     QToolButton*         toolBtn;
0128 
0129     QStringList          tagsFilter;
0130     QStringList          keysFilter;
0131 
0132     QAction*             saveMetadata;
0133     QAction*             printMetadata;
0134     QAction*             copy2ClipBoard;
0135 
0136     QMenu*               optionsMenu;
0137     QTimer*              preLoadingTimer;   ///< To prevent flicker effect with loading view with short loading time.
0138 };
0139 
0140 ExifToolWidget::ExifToolWidget(QWidget* const parent)
0141     : QStackedWidget(parent),
0142       d             (new Private)
0143 {
0144     setAttribute(Qt::WA_DeleteOnClose);
0145     setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
0146 
0147     // -----------------------------------------------------------------
0148 
0149     d->filterBtn      = new QToolButton(this);
0150     d->filterBtn->setToolTip(i18nc("@info: metadata view", "Tags filter options"));
0151     d->filterBtn->setIcon(QIcon::fromTheme(QLatin1String("view-filter")));
0152     d->filterBtn->setPopupMode(QToolButton::InstantPopup);
0153     d->filterBtn->setWhatsThis(i18nc("@info: metadata view", "Apply tags filter over metadata."));
0154 
0155     d->optionsMenu                  = new QMenu(d->filterBtn);
0156     QActionGroup* const filterGroup = new QActionGroup(this);
0157 
0158     d->noneAction     = d->optionsMenu->addAction(i18nc("@action: metadata view", "No filter"));
0159     d->noneAction->setCheckable(true);
0160     filterGroup->addAction(d->noneAction);
0161     d->photoAction    = d->optionsMenu->addAction(i18nc("@action: metadata view", "Photograph"));
0162     d->photoAction->setCheckable(true);
0163     filterGroup->addAction(d->photoAction);
0164     d->customAction   = d->optionsMenu->addAction(i18nc("@action: metadata view", "Custom"));
0165     d->customAction->setCheckable(true);
0166     filterGroup->addAction(d->customAction);
0167     d->optionsMenu->addSeparator();
0168     d->settingsAction = d->optionsMenu->addAction(i18nc("@action: metadata view", "Settings"));
0169     d->settingsAction->setCheckable(false);
0170 
0171     filterGroup->setExclusive(true);
0172     d->filterBtn->setMenu(d->optionsMenu);
0173 
0174     // -----------------------------------------------------------------
0175 
0176     const int spacing        = qMin(QApplication::style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing),
0177                              QApplication::style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing));
0178     d->metadataView          = new QWidget(this);
0179     QGridLayout* const grid2 = new QGridLayout(d->metadataView);
0180 
0181     d->toolBtn               = new QToolButton(this);
0182     d->toolBtn->setToolTip(i18nc("@info: metadata view", "Tools"));
0183     d->toolBtn->setIcon(QIcon::fromTheme(QLatin1String("system-run")));
0184     d->toolBtn->setPopupMode(QToolButton::InstantPopup);
0185     d->toolBtn->setWhatsThis(i18nc("@info: metadata view", "Run tool over metadata tags."));
0186 
0187     QMenu* const toolMenu    = new QMenu(d->toolBtn);
0188     d->saveMetadata          = toolMenu->addAction(i18nc("@action:inmenu", "Save in file"));
0189     d->printMetadata         = toolMenu->addAction(i18nc("@action:inmenu", "Print"));
0190     d->copy2ClipBoard        = toolMenu->addAction(i18nc("@action:inmenu", "Copy to Clipboard"));
0191     d->toolBtn->setMenu(toolMenu);
0192 
0193     d->preLoadingTimer       = new QTimer(this);
0194     d->preLoadingTimer->setInterval(2000);
0195     d->preLoadingTimer->setSingleShot(true);
0196     d->loadingView           = new ExifToolLoadingView(this);
0197 
0198     d->view                  = new ExifToolListView(d->metadataView);
0199     d->searchBar             = new SearchTextBar(d->metadataView, QLatin1String("ExifToolSearchBar"));
0200 
0201     grid2->addWidget(d->filterBtn, 0, 0, 1, 1);
0202     grid2->addWidget(d->searchBar, 0, 1, 1, 3);
0203     grid2->addWidget(d->toolBtn,   0, 4, 1, 1);
0204     grid2->addWidget(d->view,      1, 0, 1, 5);
0205     grid2->setColumnStretch(2, 10);
0206     grid2->setRowStretch(1, 10);
0207     grid2->setContentsMargins(spacing, spacing, spacing, spacing);
0208     grid2->setSpacing(0);
0209 
0210     // ---
0211 
0212     d->errorView             = new ExifToolErrorView(this);
0213 
0214     insertWidget(Private::LoadingView,  d->loadingView);
0215     insertWidget(Private::MetadataView, d->metadataView);
0216     insertWidget(Private::ErrorView,    d->errorView);
0217 
0218     setCurrentIndex(Private::MetadataView);
0219 
0220     setup();
0221 }
0222 
0223 ExifToolWidget::~ExifToolWidget()
0224 {
0225     delete d;
0226 }
0227 
0228 void ExifToolWidget::loadFromUrl(const QUrl& url)
0229 {
0230     d->fileName = url.fileName();
0231 
0232     d->preLoadingTimer->start();
0233     d->view->loadFromUrl(url);
0234 }
0235 
0236 void ExifToolWidget::slotLoadingResult(bool ok)
0237 {
0238     d->preLoadingTimer->stop();
0239 
0240     if (ok)
0241     {
0242         buildView();
0243         SearchTextSettings settings = d->searchBar->searchTextSettings();
0244 
0245         if (!settings.text.isEmpty())
0246         {
0247             d->view->slotSearchTextChanged(settings);
0248         }
0249 
0250         setCurrentIndex(Private::MetadataView);
0251     }
0252     else
0253     {
0254         d->errorView->setErrorText(i18nc("@info: error message",
0255                                    "Cannot load data\n"
0256                                    "from %1\n"
0257                                    "with ExifTool.\n\n"
0258                                    "%2",
0259                                    d->fileName,
0260                                    d->view->errorString()));
0261 
0262         setCurrentIndex(Private::ErrorView);
0263     }
0264 
0265     d->loadingView->setBusy(false);
0266     d->toolBtn->setEnabled(ok);
0267 }
0268 
0269 void ExifToolWidget::slotPreLoadingTimerDone()
0270 {
0271     setCurrentIndex(Private::LoadingView);
0272     d->loadingView->setBusy(true);
0273 }
0274 
0275 void ExifToolWidget::setup()
0276 {
0277     connect(d->optionsMenu, SIGNAL(triggered(QAction*)),
0278             this, SLOT(slotFilterChanged(QAction*)));
0279 
0280     connect(d->view, SIGNAL(signalLoadingResult(bool)),
0281             this, SLOT(slotLoadingResult(bool)));
0282 
0283     connect(d->preLoadingTimer, SIGNAL(timeout()),
0284             this, SLOT(slotPreLoadingTimerDone()));
0285 
0286     connect(d->errorView, SIGNAL(signalSetupExifTool()),
0287             this, SIGNAL(signalSetupExifTool()));
0288 
0289     connect(d->copy2ClipBoard, SIGNAL(triggered(bool)),
0290             this, SLOT(slotCopy2Clipboard()));
0291 
0292     connect(d->printMetadata, SIGNAL(triggered(bool)),
0293             this, SLOT(slotPrintMetadata()));
0294 
0295     connect(d->saveMetadata, SIGNAL(triggered(bool)),
0296             this, SLOT(slotSaveMetadataToFile()));
0297 
0298     connect(d->searchBar, SIGNAL(signalSearchTextSettings(SearchTextSettings)),
0299             d->view, SLOT(slotSearchTextChanged(SearchTextSettings)));
0300 
0301     connect(d->view, SIGNAL(signalTextFilterMatch(bool)),
0302             d->searchBar, SLOT(slotSearchResult(bool)));
0303 }
0304 
0305 QString ExifToolWidget::metadataToText() const
0306 {
0307     QString textmetadata  = i18nc("@info: metadata to text", "File name: %1 (%2)", d->fileName, QLatin1String("ExifTool"));
0308     int i                 = 0;
0309     QTreeWidgetItem* item = nullptr;
0310 
0311     do
0312     {
0313         item                                = d->view->topLevelItem(i);
0314         ExifToolListViewGroup* const lvItem = dynamic_cast<ExifToolListViewGroup*>(item);
0315 
0316         if (lvItem)
0317         {
0318             textmetadata.append(QLatin1String("\n\n>>> "));
0319             textmetadata.append(lvItem->text(0));
0320             textmetadata.append(QLatin1String(" <<<\n\n"));
0321 
0322             int j                  = 0;
0323             QTreeWidgetItem* child = nullptr;
0324 
0325             do
0326             {
0327                 child = lvItem->child(j);
0328 
0329                 if (child)
0330                 {
0331                     ExifToolListViewItem* const lvItem2 = dynamic_cast<ExifToolListViewItem*>(child);
0332 
0333                     if (lvItem2)
0334                     {
0335                         textmetadata.append(lvItem2->text(0));
0336                         textmetadata.append(QLatin1String(" : "));
0337                         textmetadata.append(lvItem2->text(1));
0338                         textmetadata.append(QLatin1Char('\n'));
0339                     }
0340                 }
0341 
0342                 ++j;
0343             }
0344             while (child);
0345         }
0346 
0347         ++i;
0348     }
0349     while (item);
0350 
0351     return textmetadata;
0352 }
0353 
0354 void ExifToolWidget::slotFilterChanged(QAction* action)
0355 {
0356     if      (action == d->settingsAction)
0357     {
0358         Q_EMIT signalSetupMetadataFilters();
0359     }
0360     else if ((action == d->noneAction)  ||
0361              (action == d->photoAction) ||
0362              (action == d->customAction))
0363     {
0364         buildView();
0365     }
0366 }
0367 
0368 void ExifToolWidget::slotCopy2Clipboard()
0369 {
0370     QMimeData* const mimeData = new QMimeData();
0371     mimeData->setText(metadataToText());
0372     QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard);
0373 }
0374 
0375 void ExifToolWidget::slotPrintMetadata()
0376 {
0377     QPrinter printer;
0378     printer.setFullPage(true);
0379 
0380     QPointer<QPrintDialog> dialog = new QPrintDialog(&printer, qApp->activeWindow());
0381 
0382     if (dialog->exec())
0383     {
0384         QTextDocument doc;
0385         doc.setPlainText(metadataToText());
0386         QFont font(QApplication::font());
0387         font.setPointSize(10);                // we define 10pt to be a nice base size for printing.
0388         doc.setDefaultFont(font);
0389         doc.print(&printer);
0390     }
0391 
0392     delete dialog;
0393 }
0394 
0395 void ExifToolWidget::slotSaveMetadataToFile()
0396 {
0397     QPointer<DFileDialog> fileSaveDialog = new DFileDialog(this, i18nc("@title:window", "Save ExifTool Information"),
0398                                                            QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation));
0399     fileSaveDialog->setAcceptMode(QFileDialog::AcceptSave);
0400     fileSaveDialog->setFileMode(QFileDialog::AnyFile);
0401     fileSaveDialog->selectFile(QString::fromUtf8("%1.txt").arg(d->fileName));
0402     fileSaveDialog->setNameFilter(QLatin1String("*.txt"));
0403     fileSaveDialog->exec();
0404 
0405     // Check for cancel.
0406 
0407     if (!fileSaveDialog->hasAcceptedUrls())
0408     {
0409         delete fileSaveDialog;
0410         return;
0411     }
0412 
0413     QUrl url = fileSaveDialog->selectedUrls().first();
0414     delete fileSaveDialog;
0415 
0416     QFile file(url.toLocalFile());
0417 
0418     if (!file.open(QIODevice::WriteOnly))
0419     {
0420         return;
0421     }
0422 
0423     QTextStream stream(&file);
0424     stream << metadataToText();
0425     file.close();
0426 }
0427 
0428 QString ExifToolWidget::getCurrentItemKey() const
0429 {
0430     return d->view->getCurrentItemKey();
0431 }
0432 
0433 void ExifToolWidget::setCurrentItemByKey(const QString& itemKey)
0434 {
0435     d->view->setCurrentItemByKey(itemKey);
0436 }
0437 
0438 QStringList ExifToolWidget::getTagsFilter() const
0439 {
0440     return d->tagsFilter;
0441 }
0442 
0443 void ExifToolWidget::setTagsFilter(const QStringList& list)
0444 {
0445     d->tagsFilter = list;
0446 
0447     if (d->tagsFilter.isEmpty())
0448     {
0449         d->customAction->setEnabled(false);
0450 
0451         if (getMode() == CUSTOM)
0452         {
0453             d->noneAction->setChecked(true);
0454         }
0455     }
0456     else
0457     {
0458         d->customAction->setEnabled(true);
0459     }
0460 
0461     buildView();
0462 }
0463 
0464 void ExifToolWidget::setMode(int mode)
0465 {
0466     if      (mode == NONE)
0467     {
0468         d->noneAction->setChecked(true);
0469     }
0470     else if (mode == PHOTO)
0471     {
0472         d->photoAction->setChecked(true);
0473     }
0474     else
0475     {
0476         d->customAction->setChecked(true);
0477     }
0478 
0479     buildView();
0480 }
0481 
0482 int ExifToolWidget::getMode() const
0483 {
0484     if      (d->noneAction->isChecked())
0485     {
0486         return NONE;
0487     }
0488     else if (d->photoAction->isChecked())
0489     {
0490         return PHOTO;
0491     }
0492 
0493     return CUSTOM;
0494 }
0495 
0496 void ExifToolWidget::buildView()
0497 {
0498     switch (getMode())
0499     {
0500         case CUSTOM:
0501         {
0502             d->view->setGroupList(getTagsFilter());
0503             break;
0504         }
0505 
0506         case PHOTO:
0507         {
0508             d->view->setGroupList(QStringList() << QLatin1String("FULL"), d->keysFilter);
0509             break;
0510         }
0511 
0512         default: // NONE
0513         {
0514             d->view->setGroupList(QStringList());
0515             break;
0516         }
0517     }
0518 
0519     d->view->slotSearchTextChanged(d->searchBar->searchTextSettings());
0520 }
0521 
0522 } // namespace Digikam
0523 
0524 #include "moc_exiftoolwidget.cpp"