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"