File indexing completed on 2024-04-28 15:40:15

0001 // SPDX-FileCopyrightText: 2003-2020 The KPhotoAlbum Development Team
0002 // SPDX-FileCopyrightText: 2021-2023 Johannes Zarl-Zierl <johannes@zarl-zierl.at>
0003 //
0004 // SPDX-License-Identifier: GPL-2.0-or-later
0005 
0006 #include "Window.h"
0007 
0008 #include "AutoStackImages.h"
0009 #include "BreadcrumbViewer.h"
0010 #include "DeleteDialog.h"
0011 #include "DirtyIndicator.h"
0012 #include "DuplicateMerger/DuplicateMerger.h"
0013 #include "ExternalPopup.h"
0014 #include "FeatureDialog.h"
0015 #include "ImageCounter.h"
0016 #include "InvalidDateFinder.h"
0017 #include "Logging.h"
0018 #include "Options.h"
0019 #include "SearchBar.h"
0020 #include "SplashScreen.h"
0021 #include "StatisticsDialog.h"
0022 #include "StatusBar.h"
0023 #include "TokenEditor.h"
0024 #include "UpdateVideoThumbnail.h"
0025 #include "WelcomeDialog.h"
0026 
0027 #include <AnnotationDialog/Dialog.h>
0028 #include <BackgroundJobs/SearchForVideosWithoutLengthInfo.h>
0029 #include <BackgroundJobs/SearchForVideosWithoutVideoThumbnailsJob.h>
0030 #include <BackgroundTaskManager/JobManager.h>
0031 #include <Browser/BrowserWidget.h>
0032 #include <DB/CategoryCollection.h>
0033 #include <DB/ImageDB.h>
0034 #include <DB/ImageInfo.h>
0035 #include <DB/MD5.h>
0036 #include <DB/MD5Map.h>
0037 #include <DB/NewImageFinder.h>
0038 #include <DateBar/DateBarWidget.h>
0039 #include <Exif/InfoDialog.h>
0040 #include <Exif/ReReadDialog.h>
0041 #include <HTMLGenerator/HTMLDialog.h>
0042 #include <ImageManager/AsyncLoader.h>
0043 #include <ImageManager/ThumbnailBuilder.h>
0044 #include <ImportExport/Export.h>
0045 #include <ImportExport/Import.h>
0046 #include <Settings/SettingsDialog.h>
0047 #include <ThumbnailView/FilterWidget.h>
0048 #include <ThumbnailView/ThumbnailFacade.h>
0049 #include <ThumbnailView/enums.h>
0050 #include <Utilities/DemoUtil.h>
0051 #include <Utilities/List.h>
0052 #include <Utilities/ShowBusyCursor.h>
0053 #include <Viewer/ViewerWidget.h>
0054 #include <kpabase/FileExtensions.h>
0055 #include <kpabase/FileNameUtil.h>
0056 #include <kpabase/Logging.h>
0057 #include <kpabase/SettingsData.h>
0058 #include <kpabase/UIDelegate.h>
0059 #include <kpabase/config-kpa-marble.h>
0060 #include <kpabase/config-kpa-plugins.h>
0061 #include <kpaexif/Database.h>
0062 #include <kpaexif/Info.h>
0063 #include <kpathumbnails/ThumbnailCache.h>
0064 
0065 #ifdef KF5Purpose_FOUND
0066 #include <Plugins/PurposeMenu.h>
0067 #endif
0068 #ifdef HAVE_MARBLE
0069 #include <Map/MapView.h>
0070 #endif
0071 #ifdef KPA_ENABLE_REMOTECONTROL
0072 #include <RemoteControl/RemoteInterface.h>
0073 #endif
0074 
0075 #include <stdexcept>
0076 #ifdef HAVE_STDLIB_H
0077 #include <stdlib.h>
0078 #endif
0079 
0080 #include <kconfigwidgets_version.h>
0081 #include <kio_version.h> // for #if KIO_VERSION...
0082 #include <kwidgetsaddons_version.h>
0083 
0084 #include <KActionCollection>
0085 #include <KActionMenu>
0086 #include <KColorSchemeManager>
0087 #if KCONFIGWIDGETS_VERSION >= QT_VERSION_CHECK(5, 107, 0)
0088 #include <KColorSchemeMenu>
0089 #endif
0090 #include <KConfigGroup>
0091 #include <KEditToolBar>
0092 #include <KIconLoader>
0093 #include <KLocalizedString>
0094 #include <KMessageBox>
0095 #include <KPasswordDialog>
0096 #include <KProcess>
0097 #include <KSharedConfig>
0098 #include <KShortcutsDialog>
0099 #include <KStandardAction>
0100 #include <KToggleAction>
0101 #include <QApplication>
0102 #include <QClipboard>
0103 #include <QCloseEvent>
0104 #include <QContextMenuEvent>
0105 #include <QCursor>
0106 #include <QDesktopServices>
0107 #include <QDir>
0108 #include <QElapsedTimer>
0109 #include <QFrame>
0110 #include <QInputDialog>
0111 #include <QLayout>
0112 #include <QLoggingCategory>
0113 #include <QMenu>
0114 #include <QMessageBox>
0115 #include <QMimeData>
0116 #include <QMoveEvent>
0117 #include <QObject>
0118 #include <QPixmapCache>
0119 #include <QProgressDialog>
0120 #include <QResizeEvent>
0121 #include <QStackedWidget>
0122 #include <QTimer>
0123 #include <QVBoxLayout>
0124 #include <functional>
0125 #include <ktip.h>
0126 
0127 using namespace DB;
0128 
0129 MainWindow::Window *MainWindow::Window::s_instance = nullptr;
0130 
0131 MainWindow::Window::Window(QWidget *parent)
0132     : KXmlGuiWindow(parent)
0133     , m_annotationDialog(nullptr)
0134     , m_deleteDialog(nullptr)
0135     , m_htmlDialog(nullptr)
0136     , m_tokenEditor(nullptr)
0137 #ifdef HAVE_MARBLE
0138     , m_positionBrowser(nullptr)
0139 #endif
0140 {
0141     // propagate palette changes to subwindows:
0142     setAttribute(Qt::WA_WindowPropagation);
0143     qCDebug(MainWindowLog) << "Using icon theme: " << QIcon::themeName();
0144     qCDebug(MainWindowLog) << "Icon search paths: " << QIcon::themeSearchPaths();
0145     QElapsedTimer timer;
0146     timer.start();
0147     SplashScreen::instance()->message(i18n("Loading Database"));
0148     s_instance = this;
0149 
0150     bool gotConfigFile = load();
0151     if (!gotConfigFile)
0152         throw 0;
0153     qCInfo(TimingLog) << "MainWindow: Loading Database: " << timer.restart() << "ms.";
0154     SplashScreen::instance()->message(i18n("Loading Main Window"));
0155 
0156     QWidget *top = new QWidget(this);
0157     QVBoxLayout *lay = new QVBoxLayout(top);
0158     lay->setSpacing(2);
0159     lay->setContentsMargins(2, 2, 2, 2);
0160     setCentralWidget(top);
0161 
0162     m_stack = new QStackedWidget(top);
0163     lay->addWidget(m_stack, 1);
0164 
0165     m_dateBar = new DateBar::DateBarWidget(top);
0166     lay->addWidget(m_dateBar);
0167 
0168     m_dateBarLine = new QFrame(top);
0169     m_dateBarLine->setFrameStyle(QFrame::HLine | QFrame::Plain);
0170     m_dateBarLine->setLineWidth(0);
0171     m_dateBarLine->setMidLineWidth(0);
0172     m_dateBarLine->setForegroundRole(QPalette::Window);
0173     lay->addWidget(m_dateBarLine);
0174 
0175     setHistogramVisibilty(Settings::SettingsData::instance()->showHistogram());
0176 
0177     m_browser = new Browser::BrowserWidget(m_stack);
0178     Q_ASSERT(m_thumbnailCache);
0179     m_thumbnailView = new ThumbnailView::ThumbnailFacade(m_thumbnailCache);
0180 
0181     m_stack->addWidget(m_browser);
0182     m_stack->addWidget(m_thumbnailView->gui());
0183     m_stack->setCurrentWidget(m_browser);
0184 
0185     m_settingsDialog = nullptr;
0186     qCInfo(TimingLog) << "MainWindow: Loading MainWindow: " << timer.restart() << "ms.";
0187     setupMenuBar();
0188     qCInfo(TimingLog) << "MainWindow: setupMenuBar: " << timer.restart() << "ms.";
0189     createSearchBar();
0190     qCInfo(TimingLog) << "MainWindow: createSearchBar: " << timer.restart() << "ms.";
0191     setupStatusBar();
0192     qCInfo(TimingLog) << "MainWindow: setupStatusBar: " << timer.restart() << "ms.";
0193 
0194     setTabOrder(m_searchBar, m_thumbnailView->gui());
0195     setTabOrder(m_thumbnailView->gui(), m_dateBar);
0196 
0197     // Misc
0198     m_autoSaveTimer = new QTimer(this);
0199     connect(m_autoSaveTimer, &QTimer::timeout, this, &Window::slotAutoSave);
0200     startAutoSaveTimer();
0201 
0202     connect(m_browser, &Browser::BrowserWidget::showingOverview,
0203             this, &Window::showBrowser);
0204     connect(m_browser, &Browser::BrowserWidget::pathChanged,
0205             m_statusBar->mp_pathIndicator, &BreadcrumbViewer::setBreadcrumbs);
0206     connect(m_statusBar->mp_pathIndicator, &BreadcrumbViewer::widenToBreadcrumb,
0207             m_browser, &Browser::BrowserWidget::widenToBreadcrumb);
0208     connect(m_browser, &Browser::BrowserWidget::pathChanged,
0209             this, QOverload<const Browser::BreadcrumbList &>::of(&Window::updateDateBar));
0210     connect(m_browser, &Browser::BrowserWidget::showSearch, m_searchBar, QOverload<>::of(&QWidget::setFocus));
0211 
0212     connect(m_dateBar, &DateBar::DateBarWidget::dateSelected,
0213             m_thumbnailView, &ThumbnailView::ThumbnailFacade::gotoDate);
0214     connect(m_dateBar, &DateBar::DateBarWidget::toolTipInfo, this, &Window::showDateBarTip);
0215     connect(Settings::SettingsData::instance(), &Settings::SettingsData::histogramSizeChanged,
0216             m_dateBar, &DateBar::DateBarWidget::setHistogramBarSize);
0217     connect(Settings::SettingsData::instance(), &Settings::SettingsData::actualThumbnailSizeChanged,
0218             this, &Window::slotThumbnailSizeChanged);
0219 
0220     connect(m_dateBar, &DateBar::DateBarWidget::dateRangeChange, this, &Window::setDateRange);
0221     connect(m_dateBar, &DateBar::DateBarWidget::dateRangeCleared, this, &Window::clearDateRange);
0222     connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::currentDateChanged,
0223             m_dateBar, &DateBar::DateBarWidget::setDate);
0224 
0225     connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::showImage, this, &Window::showImage);
0226     connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::showSelection, this, QOverload<>::of(&Window::slotView));
0227     connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::showSearch, m_searchBar, QOverload<>::of(&QWidget::setFocus));
0228 
0229     connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::fileIdUnderCursorChanged, this, &Window::slotSetFileName);
0230     connect(DB::ImageDB::instance(), &DB::ImageDB::totalChanged, this, QOverload<>::of(&Window::updateDateBar));
0231     connect(DB::ImageDB::instance()->categoryCollection(), &DB::CategoryCollection::categoryCollectionChanged,
0232             this, &Window::slotOptionGroupChanged);
0233     connect(m_browser, &Browser::BrowserWidget::imageCount, m_statusBar->mp_partial, &ImageCounter::showBrowserMatches);
0234     connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::filterChanged, this, &Window::slotFilterChanged);
0235     connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::selectionChanged, this, &Window::updateContextMenuFromSelectionSize);
0236 
0237     checkIfVideoThumbnailerIsInstalled();
0238     executeStartupActions();
0239 
0240     qCInfo(TimingLog) << "MainWindow: executeStartupActions " << timer.restart() << "ms.";
0241     QTimer::singleShot(0, this, &Window::delayedInit);
0242     updateContextMenuFromSelectionSize(0);
0243 
0244     // Automatically save toolbar settings
0245     setAutoSaveSettings();
0246 
0247     m_copyLinkEngine = new CopyLinkEngine(this);
0248 
0249     qCInfo(TimingLog) << "MainWindow: misc setup time: " << timer.restart() << "ms.";
0250 }
0251 
0252 MainWindow::Window::~Window()
0253 {
0254     DB::ImageDB::deleteInstance();
0255     delete m_thumbnailCache;
0256 }
0257 
0258 void MainWindow::Window::delayedInit()
0259 {
0260     QElapsedTimer timer;
0261     timer.start();
0262     SplashScreen *splash = SplashScreen::instance();
0263     setupPluginMenu();
0264     qCInfo(TimingLog) << "MainWindow: setupPluginMenu: " << timer.restart() << "ms.";
0265 
0266     if (Settings::SettingsData::instance()->searchForImagesOnStart() || Options::the()->searchForImagesOnStart()) {
0267         splash->message(i18n("Searching for New Files"));
0268         qApp->processEvents();
0269         DB::NewImageFinder().findImages();
0270         qCInfo(TimingLog) << "MainWindow: Search for New Files: " << timer.restart() << "ms.";
0271     }
0272 
0273     splash->done();
0274     show();
0275     updateDateBar();
0276     qCInfo(TimingLog) << "MainWindow: MainWindow.show():" << timer.restart() << "ms.";
0277 
0278     QUrl importUrl = Options::the()->importFile();
0279     if (importUrl.isValid()) {
0280         // I need to do this in delayed init to get the import window on top of the normal window
0281         ImportExport::Import::imageImport(importUrl);
0282         qCInfo(TimingLog) << "MainWindow: imageImport:" << timer.restart() << "ms.";
0283     } else {
0284 #if KCONFIGWIDGETS_VERSION < QT_VERSION_CHECK(5, 83, 0)
0285         // I need to postpone this otherwise the tip dialog will not get focus on start up
0286         KTipDialog::showTip(this);
0287 #endif
0288     }
0289 
0290     qCInfo(TimingLog) << "MainWindow: Loading Exif DB:" << timer.restart() << "ms.";
0291 
0292 #ifdef KPA_ENABLE_REMOTECONTROL
0293     if (!Options::the()->listen().isNull())
0294         RemoteControl::RemoteInterface::instance().listen(Options::the()->listen());
0295     else if (Settings::SettingsData::instance()->listenForAndroidDevicesOnStartup())
0296         RemoteControl::RemoteInterface::instance().listen();
0297 #endif
0298 }
0299 
0300 bool MainWindow::Window::slotExit()
0301 {
0302     bool deleteDemoDB = false;
0303     if (Options::the()->demoMode()) {
0304         const QString question = i18n("<p><b>Delete Your Temporary Demo Database</b></p>"
0305                                       "<p>I hope you enjoyed the KPhotoAlbum demo. The demo database was created in the "
0306                                       "folder <tt>/tmp</tt>, should it be deleted now? If you do not delete it, it will waste disk space; "
0307                                       "on the other hand, if you want to come back and try the demo again, you "
0308                                       "might want to keep it around with the changes you made through this session.</p>");
0309         const QString title = i18nc("@title", "Delete Demo Database");
0310 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0)
0311         const auto answer = KMessageBox::questionTwoActionsCancel(widget(),
0312                                                                   question,
0313                                                                   title,
0314                                                                   KStandardGuiItem::del(),
0315                                                                   KStandardGuiItem::save(),
0316                                                                   KStandardGuiItem::cancel(),
0317                                                                   QString::fromLatin1("deleteDemoDatabase2"));
0318         if (answer == KMessageBox::Cancel)
0319             return false;
0320         else if (answer == KMessageBox::PrimaryAction) {
0321 #else
0322         const auto answer = KMessageBox::questionYesNoCancel(this, question, title,
0323                                                              KStandardGuiItem::yes(), KStandardGuiItem::no(), KStandardGuiItem::cancel(),
0324                                                              QString::fromLatin1("deleteDemoDatabase"));
0325         if (answer == KMessageBox::Cancel)
0326             return false;
0327         else if (answer == KMessageBox::Yes) {
0328 #endif
0329             deleteDemoDB = true;
0330         } else {
0331             slotSave();
0332         }
0333     } else if (m_statusBar->mp_dirtyIndicator->isSaveDirty()) {
0334         const QString question = i18n("Do you want to save the changes?");
0335         const QString title = i18nc("@title", "Save Changes?");
0336 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0)
0337         const auto answer = KMessageBox::questionTwoActionsCancel(widget(),
0338                                                                   question,
0339                                                                   title,
0340                                                                   KStandardGuiItem::save(),
0341                                                                   KStandardGuiItem::dontSave(),
0342                                                                   KStandardGuiItem::cancel());
0343         constexpr auto REPLY_SAVE = KMessageBox::PrimaryAction;
0344         constexpr auto REPLY_DONTSAVE = KMessageBox::SecondaryAction;
0345 #else
0346         const auto answer = KMessageBox::questionYesNoCancel(this, question, title);
0347         constexpr auto REPLY_SAVE = KMessageBox::Yes;
0348         constexpr auto REPLY_DONTSAVE = KMessageBox::No;
0349 #endif
0350         if (answer == KMessageBox::Cancel) {
0351             return false;
0352         }
0353         if (answer == REPLY_SAVE) {
0354             slotSave();
0355         }
0356         if (answer == REPLY_DONTSAVE) {
0357             QDir().remove(Settings::SettingsData::instance()->imageDirectory() + QString::fromLatin1(".#index.xml"));
0358         }
0359     }
0360 
0361     ImageManager::AsyncLoader::instance()->requestExit();
0362     // Always flush any remaining thumbnails
0363     thumbnailCache()->save();
0364     if (deleteDemoDB)
0365         Utilities::deleteDemo();
0366     qApp->quit();
0367     return true;
0368 }
0369 
0370 void MainWindow::Window::slotOptions()
0371 {
0372     if (!m_settingsDialog) {
0373         m_settingsDialog = new Settings::SettingsDialog(this);
0374         // lambda expression because because reloadThumbnails has default parameters:
0375         connect(m_settingsDialog, &Settings::SettingsDialog::changed, this, [=]() { this->reloadThumbnails(); });
0376         connect(m_settingsDialog, &Settings::SettingsDialog::changed, this, &Window::startAutoSaveTimer);
0377         connect(m_settingsDialog, &Settings::SettingsDialog::changed, m_browser, &Browser::BrowserWidget::reload);
0378     }
0379     m_settingsDialog->show();
0380 }
0381 
0382 void MainWindow::Window::slotCreateImageStack()
0383 {
0384     const DB::FileNameList list = selected();
0385     if (list.size() < 2) {
0386         // it doesn't make sense to make a stack from one image, does it?
0387         return;
0388     }
0389 
0390     bool ok = DB::ImageDB::instance()->stack(list);
0391     if (!ok) {
0392         const QString question = i18n("Some of the selected images already belong to a stack. "
0393                                       "Do you want to remove them from their stacks and create a "
0394                                       "completely new one?");
0395         const QString title = i18nc("@title", "Stacking problem");
0396 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0)
0397         const auto answer = KMessageBox::questionTwoActions(widget(),
0398                                                             question,
0399                                                             title,
0400                                                             KStandardGuiItem::ok(),
0401                                                             KStandardGuiItem::cancel());
0402         if (answer == KMessageBox::ButtonCode::PrimaryAction) {
0403 #else
0404         const auto answer = KMessageBox::questionYesNo(this, question, title);
0405         if (answer == KMessageBox::Yes) {
0406 #endif
0407             DB::ImageDB::instance()->unstack(list);
0408             if (!DB::ImageDB::instance()->stack(list)) {
0409                 KMessageBox::error(this,
0410                                    i18n("Unknown error, stack creation failed."),
0411                                    i18n("Stacking Error"));
0412                 return;
0413             }
0414         } else {
0415             return;
0416         }
0417     }
0418 
0419     DirtyIndicator::markDirty();
0420     // The current item might have just became invisible
0421     m_thumbnailView->setCurrentItem(list.at(0));
0422     m_thumbnailView->updateDisplayModel();
0423 }
0424 
0425 /** @short Make the selected image the head of a stack
0426  *
0427  * The whole point of image stacking is to group images together and then select
0428  * one of them as the "most important". This function is (maybe just a
0429  * temporary) way of promoting a selected image to the "head" of a stack it
0430  * belongs to. In future, it might get replaced by a Ligtroom-like interface.
0431  * */
0432 void MainWindow::Window::slotSetStackHead()
0433 {
0434     const DB::FileNameList list = selected();
0435     if (list.size() != 1) {
0436         // this should be checked by enabling/disabling of QActions
0437         return;
0438     }
0439 
0440     setStackHead(*list.begin());
0441 }
0442 
0443 void MainWindow::Window::setStackHead(const DB::FileName &image)
0444 {
0445     const auto info = DB::ImageDB::instance()->info(image);
0446     if (!info->isStacked())
0447         return;
0448 
0449     unsigned int oldOrder = info->stackOrder();
0450 
0451     const DB::FileNameList others = DB::ImageDB::instance()->getStackFor(image);
0452     for (const DB::FileName &current : others) {
0453         const auto currentInfo = DB::ImageDB::instance()->info(current);
0454         if (current == image) {
0455             currentInfo->setStackOrder(1);
0456         } else if (currentInfo->stackOrder() < oldOrder) {
0457             currentInfo->setStackOrder(currentInfo->stackOrder() + 1);
0458         }
0459     }
0460 
0461     DirtyIndicator::markDirty();
0462     m_thumbnailView->updateDisplayModel();
0463 }
0464 
0465 void MainWindow::Window::slotUnStackImages()
0466 {
0467     const DB::FileNameList &list = selected();
0468     if (list.isEmpty())
0469         return;
0470 
0471     DB::ImageDB::instance()->unstack(list);
0472     DirtyIndicator::markDirty();
0473     m_thumbnailView->updateDisplayModel();
0474 }
0475 
0476 void MainWindow::Window::slotConfigureAllImages()
0477 {
0478     configureImages(false);
0479 }
0480 
0481 void MainWindow::Window::slotConfigureImagesOneAtATime()
0482 {
0483     configureImages(true);
0484 }
0485 
0486 void MainWindow::Window::configureImages(bool oneAtATime)
0487 {
0488     const DB::FileNameList &list = selected();
0489     if (list.isEmpty()) {
0490         KMessageBox::error(this, i18n("No item is selected."), i18n("No Selection"));
0491     } else {
0492         DB::ImageInfoList images;
0493         for (const DB::FileName &fileName : list) {
0494             auto info = DB::ImageDB::instance()->info(fileName);
0495             images.append(info);
0496         }
0497         configureImages(images, oneAtATime);
0498     }
0499 }
0500 
0501 void MainWindow::Window::configureImages(const DB::ImageInfoList &list, bool oneAtATime)
0502 {
0503     s_instance->configImages(list, oneAtATime);
0504 }
0505 
0506 void MainWindow::Window::configImages(const DB::ImageInfoList &list, bool oneAtATime)
0507 {
0508     createAnnotationDialog();
0509     if (m_annotationDialog->configure(list, oneAtATime) == QDialog::Rejected)
0510         return;
0511 
0512     reloadThumbnails(ThumbnailView::MaintainSelection);
0513 }
0514 
0515 void MainWindow::Window::slotSearch()
0516 {
0517     createAnnotationDialog();
0518     DB::ImageSearchInfo searchInfo = m_annotationDialog->search();
0519     if (!searchInfo.isNull())
0520         m_browser->addSearch(searchInfo);
0521 }
0522 
0523 void MainWindow::Window::createAnnotationDialog()
0524 {
0525     Utilities::ShowBusyCursor dummy;
0526     if (!m_annotationDialog.isNull())
0527         return;
0528 
0529     m_annotationDialog = new AnnotationDialog::Dialog(nullptr);
0530     connect(m_annotationDialog.data(), &AnnotationDialog::Dialog::imageRotated, this, &Window::slotImageRotated);
0531 }
0532 
0533 void MainWindow::Window::slotSave()
0534 {
0535     Utilities::ShowBusyCursor dummy;
0536     m_statusBar->showMessage(i18n("Saving..."), 5000);
0537     DB::ImageDB::instance()->save(Settings::SettingsData::instance()->imageDirectory() + QString::fromLatin1("index.xml"), false);
0538     thumbnailCache()->save();
0539     m_statusBar->mp_dirtyIndicator->saved();
0540     QDir().remove(Settings::SettingsData::instance()->imageDirectory() + QString::fromLatin1(".#index.xml"));
0541     m_statusBar->showMessage(i18n("Saving... Done"), 5000);
0542 }
0543 
0544 void MainWindow::Window::slotDeleteSelected()
0545 {
0546     if (!m_deleteDialog)
0547         m_deleteDialog = new DeleteDialog(this);
0548     if (m_deleteDialog->exec(selected()) != QDialog::Accepted)
0549         return;
0550 
0551     DirtyIndicator::markDirty();
0552 }
0553 
0554 void MainWindow::Window::slotCopySelectedURLs()
0555 {
0556     QList<QUrl> urls;
0557     int urlcount = 0;
0558     const auto selectedFiles = selected();
0559     for (const DB::FileName &fileName : selectedFiles) {
0560         urls.append(QUrl::fromLocalFile(fileName.absolute()));
0561         urlcount++;
0562     }
0563     if (urlcount == 1)
0564         m_paste->setEnabled(true);
0565     else
0566         m_paste->setEnabled(false);
0567     QMimeData *mimeData = new QMimeData;
0568     mimeData->setUrls(urls);
0569 
0570     QApplication::clipboard()->setMimeData(mimeData);
0571 }
0572 
0573 void MainWindow::Window::slotPasteInformation()
0574 {
0575     const QMimeData *mimeData = QApplication::clipboard()->mimeData();
0576 
0577     if (!mimeData->hasUrls() || mimeData->urls().length() != 1)
0578         return;
0579 
0580     const QUrl url = mimeData->urls().constFirst();
0581     if (!url.isLocalFile())
0582         return;
0583 
0584     const DB::FileName fileName = DB::FileName::fromAbsolutePath(url.toLocalFile());
0585     // fail silent if there is no file.
0586     if (fileName.isNull())
0587         return;
0588 
0589     const DB::ImageInfoPtr originalInfo = DB::ImageDB::instance()->info(fileName);
0590     // fail silent if there is no info for the file.
0591     if (!originalInfo)
0592         return;
0593 
0594     const auto selectedFiles = selected();
0595     for (const DB::FileName &newFile : selectedFiles) {
0596         auto newInfo = DB::ImageDB::instance()->info(newFile);
0597         newInfo->copyExtraData(*originalInfo, false);
0598     }
0599     DirtyIndicator::markDirty();
0600 }
0601 
0602 void MainWindow::Window::slotReReadExifInfo()
0603 {
0604     DB::FileNameList files = selectedOnDisk();
0605     static Exif::ReReadDialog *dialog = nullptr;
0606     if (!dialog)
0607         dialog = new Exif::ReReadDialog(this);
0608     if (dialog->exec(files) == QDialog::Accepted)
0609         DirtyIndicator::markDirty();
0610 }
0611 
0612 void MainWindow::Window::slotAutoStackImages()
0613 {
0614     const DB::FileNameList list = selected();
0615     if (list.isEmpty()) {
0616         KMessageBox::error(this, i18n("No item is selected."), i18n("No Selection"));
0617         return;
0618     }
0619     QPointer<MainWindow::AutoStackImages> stacker = new AutoStackImages(this, list);
0620     if (stacker->exec() == QDialog::Accepted)
0621         showThumbNails();
0622     delete stacker;
0623 }
0624 
0625 /**
0626  * In thumbnail mode, return a list of files that are selected.
0627  * Otherwise, return all images in the current scope/context.
0628  */
0629 DB::FileNameList MainWindow::Window::selected(ThumbnailView::SelectionMode mode) const
0630 {
0631     if (m_thumbnailView->gui() == m_stack->currentWidget())
0632         return m_thumbnailView->selection(mode);
0633     else
0634         // return all images in the current scope (parameter false: include images not on disk)
0635         return DB::ImageDB::instance()->currentScope(false);
0636 }
0637 
0638 void MainWindow::Window::slotViewNewWindow()
0639 {
0640     slotView(false, false);
0641 }
0642 
0643 /*
0644  * Returns a list of files that are both selected and on disk. If there are no
0645  * selected files, returns all files form current context that are on disk.
0646  * Note: On some setups (NFS), this can be a very time-consuming method!
0647  * */
0648 DB::FileNameList MainWindow::Window::selectedOnDisk()
0649 {
0650     const DB::FileNameList list = selected(ThumbnailView::NoExpandCollapsedStacks);
0651 
0652     DB::FileNameList listOnDisk;
0653     for (const DB::FileName &fileName : list) {
0654         if (DB::ImageInfo::imageOnDisk(fileName))
0655             listOnDisk.append(fileName);
0656     }
0657 
0658     return listOnDisk;
0659 }
0660 
0661 void MainWindow::Window::slotView(bool reuse, bool slideShow, bool random)
0662 {
0663     launchViewer(selected(ThumbnailView::NoExpandCollapsedStacks), reuse, slideShow, random);
0664 }
0665 
0666 void MainWindow::Window::slotView()
0667 {
0668     slotView(true, false, false);
0669 }
0670 
0671 void MainWindow::Window::launchViewer(const DB::FileNameList &inputMediaList, bool reuse, bool slideShow, bool random)
0672 {
0673     DB::FileNameList mediaList = inputMediaList;
0674     int seek = -1;
0675     if (mediaList.isEmpty()) {
0676         mediaList = m_thumbnailView->imageList(ThumbnailView::ViewOrder);
0677     } else if (mediaList.size() == 1) {
0678         // we fake it so it appears the user has selected all images
0679         // and magically scrolls to the originally selected one
0680         const DB::FileName first = mediaList.at(0);
0681         mediaList = m_thumbnailView->imageList(ThumbnailView::ViewOrder);
0682         seek = mediaList.indexOf(first);
0683     }
0684 
0685     if (mediaList.isEmpty())
0686         mediaList = DB::ImageDB::instance()->currentScope(false);
0687 
0688     if (mediaList.isEmpty()) {
0689         KMessageBox::error(this, i18n("There are no images to be shown."));
0690         return;
0691     }
0692 
0693     if (random) {
0694         mediaList = DB::FileNameList(Utilities::shuffleList(mediaList));
0695     }
0696 
0697     using Viewer::ViewerWidget;
0698     ViewerWidget *viewer;
0699     if (reuse && ViewerWidget::latest()) {
0700         viewer = ViewerWidget::latest();
0701         viewer->raise();
0702         viewer->activateWindow();
0703     } else {
0704         viewer = new ViewerWidget(ViewerWidget::UsageType::FullFeaturedViewer);
0705         viewer->setCopyLinkEngine(m_copyLinkEngine);
0706     }
0707 
0708     connect(viewer, &Viewer::ViewerWidget::soughtTo, m_thumbnailView, &ThumbnailView::ThumbnailFacade::changeSingleSelection);
0709     connect(viewer, &Viewer::ViewerWidget::imageRotated, this, &Window::slotImageRotated);
0710 
0711     viewer->show(slideShow);
0712     viewer->load(mediaList, seek < 0 ? 0 : seek);
0713     viewer->raise();
0714 }
0715 
0716 void MainWindow::Window::slotSortByDateAndTime()
0717 {
0718     DB::ImageDB::instance()->sortAndMergeBackIn(selected());
0719     showThumbNails(DB::ImageDB::instance()->search(Browser::BrowserWidget::instance()->currentContext()).files());
0720     DirtyIndicator::markDirty();
0721 }
0722 
0723 void MainWindow::Window::slotSortAllByDateAndTime()
0724 {
0725     DB::ImageDB::instance()->sortAndMergeBackIn(DB::ImageDB::instance()->files());
0726     if (m_thumbnailView->gui() == m_stack->currentWidget())
0727         showThumbNails(DB::ImageDB::instance()->search(Browser::BrowserWidget::instance()->currentContext()).files());
0728     DirtyIndicator::markDirty();
0729 }
0730 
0731 QString MainWindow::Window::welcome()
0732 {
0733     QString configFileName;
0734     QPointer<MainWindow::WelcomeDialog> dialog = new WelcomeDialog(this);
0735     // exit if the user dismissed the welcome dialog
0736     if (!dialog->exec()) {
0737         qApp->quit();
0738     }
0739     configFileName = dialog->configFileName();
0740     delete dialog;
0741     return configFileName;
0742 }
0743 
0744 bool MainWindow::Window::event(QEvent *event)
0745 {
0746     if (event->type() == QEvent::PaletteChange) {
0747         // KColorSchemeManager sets a dynamic property when activating a scheme:
0748         const QString schemePath = qApp->property("KDE_COLOR_SCHEME_PATH").toString();
0749         qCInfo(MainWindowLog) << "Color Scheme changed to " << (schemePath.isEmpty() ? QString::fromLatin1("system default") : schemePath);
0750         Settings::SettingsData::instance()->setColorScheme(schemePath);
0751     }
0752     return KXmlGuiWindow::event(event);
0753 }
0754 
0755 void MainWindow::Window::closeEvent(QCloseEvent *e)
0756 {
0757     bool quit = true;
0758     quit = slotExit();
0759     // If I made it here, then the user canceled
0760     if (!quit)
0761         e->ignore();
0762     else
0763         e->setAccepted(true);
0764 }
0765 
0766 void MainWindow::Window::slotLimitToSelected()
0767 {
0768     const auto selectedList = selected();
0769     if (selectedList.isEmpty()) {
0770         QMessageBox::information(this, i18n("Limit View to Selection"),
0771                                  i18n("No images are selected!"));
0772         return;
0773     }
0774     Utilities::ShowBusyCursor dummy;
0775     showThumbNails(selectedList);
0776 }
0777 
0778 void MainWindow::Window::setupMenuBar()
0779 {
0780     // File menu
0781     KStandardAction::save(this, &Window::slotSave, actionCollection());
0782     KStandardAction::quit(this, &Window::slotExit, actionCollection());
0783     m_generateHtml = actionCollection()->addAction(QString::fromLatin1("exportHTML"));
0784     m_generateHtml->setText(i18n("Generate HTML..."));
0785     connect(m_generateHtml, &QAction::triggered, this, &Window::slotExportToHTML);
0786 
0787     QAction *a = actionCollection()->addAction(QString::fromLatin1("import"), this, &Window::slotImport);
0788     a->setText(i18n("Import..."));
0789 
0790     a = actionCollection()->addAction(QString::fromLatin1("export"), this, &Window::slotExport);
0791     a->setText(i18n("Export/Copy Images..."));
0792 
0793     // Go menu
0794     a = KStandardAction::back(m_browser, &Browser::BrowserWidget::back, actionCollection());
0795     connect(m_browser, &Browser::BrowserWidget::canGoBack, a, &QAction::setEnabled);
0796     a->setEnabled(false);
0797 
0798     a = KStandardAction::forward(m_browser, &Browser::BrowserWidget::forward, actionCollection());
0799     connect(m_browser, &Browser::BrowserWidget::canGoForward, a, &QAction::setEnabled);
0800     a->setEnabled(false);
0801 
0802     a = KStandardAction::home(m_browser, &Browser::BrowserWidget::home, actionCollection());
0803     actionCollection()->setDefaultShortcut(a, Qt::CTRL + Qt::Key_Home);
0804     connect(a, &QAction::triggered, m_dateBar, &DateBar::DateBarWidget::clearSelection);
0805 
0806     KStandardAction::redisplay(m_browser, &Browser::BrowserWidget::go, actionCollection());
0807 
0808     // The Edit menu
0809     m_copy = KStandardAction::copy(this, &Window::slotCopySelectedURLs, actionCollection());
0810     m_paste = KStandardAction::paste(this, &Window::slotPasteInformation, actionCollection());
0811     m_paste->setEnabled(false);
0812     m_selectAll = KStandardAction::selectAll(m_thumbnailView, &ThumbnailView::ThumbnailFacade::selectAll, actionCollection());
0813     m_clearSelection = KStandardAction::deselect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::clearSelection, actionCollection());
0814     m_clearSelection->setEnabled(false);
0815     KStandardAction::find(this, &Window::slotSearch, actionCollection());
0816 
0817     m_deleteSelected = actionCollection()->addAction(QString::fromLatin1("deleteSelected"));
0818     m_deleteSelected->setText(i18nc("Delete selected images", "Delete Selected"));
0819     m_deleteSelected->setIcon(QIcon::fromTheme(QString::fromLatin1("edit-delete")));
0820     actionCollection()->setDefaultShortcut(m_deleteSelected, Qt::Key_Delete);
0821     connect(m_deleteSelected, &QAction::triggered, this, &Window::slotDeleteSelected);
0822 
0823     a = actionCollection()->addAction(QString::fromLatin1("removeTokens"), this, &Window::slotRemoveTokens);
0824     a->setText(i18n("Remove Tokens..."));
0825 
0826     a = actionCollection()->addAction(QString::fromLatin1("showListOfFiles"), this, &Window::slotShowListOfFiles);
0827     a->setText(i18n("Open List of Files..."));
0828 
0829     m_configOneAtATime = actionCollection()->addAction(QString::fromLatin1("oneProp"), this, &Window::slotConfigureImagesOneAtATime);
0830     m_configOneAtATime->setText(i18n("Annotate Individual Items"));
0831     actionCollection()->setDefaultShortcut(m_configOneAtATime, Qt::CTRL + Qt::Key_1);
0832 
0833     m_configAllSimultaniously = actionCollection()->addAction(QString::fromLatin1("allProp"), this, &Window::slotConfigureAllImages);
0834     m_configAllSimultaniously->setText(i18n("Annotate Multiple Items at a Time"));
0835     actionCollection()->setDefaultShortcut(m_configAllSimultaniously, Qt::CTRL + Qt::Key_2);
0836 
0837     m_createImageStack = actionCollection()->addAction(QString::fromLatin1("createImageStack"), this, &Window::slotCreateImageStack);
0838     m_createImageStack->setText(i18n("Merge Images into a Stack"));
0839     actionCollection()->setDefaultShortcut(m_createImageStack, Qt::CTRL + Qt::Key_3);
0840 
0841     m_unStackImages = actionCollection()->addAction(QString::fromLatin1("unStackImages"), this, &Window::slotUnStackImages);
0842     m_unStackImages->setText(i18n("Remove Images from Stack"));
0843 
0844     m_setStackHead = actionCollection()->addAction(QString::fromLatin1("setStackHead"), this, &Window::slotSetStackHead);
0845     m_setStackHead->setText(i18n("Set as First Image in Stack"));
0846     actionCollection()->setDefaultShortcut(m_setStackHead, Qt::CTRL + Qt::Key_4);
0847 
0848     m_rotLeft = actionCollection()->addAction(QString::fromLatin1("rotateLeft"), this, &Window::slotRotateSelectedLeft);
0849     m_rotLeft->setText(i18n("Rotate counterclockwise"));
0850     actionCollection()->setDefaultShortcut(m_rotLeft, Qt::Key_7);
0851 
0852     m_rotRight = actionCollection()->addAction(QString::fromLatin1("rotateRight"), this, &Window::slotRotateSelectedRight);
0853     m_rotRight->setText(i18n("Rotate clockwise"));
0854     actionCollection()->setDefaultShortcut(m_rotRight, Qt::Key_9);
0855 
0856     // The View menu
0857     m_view = actionCollection()->addAction(QString::fromLatin1("viewImages"), this, qOverload<>(&Window::slotView));
0858     m_view->setText(i18n("View"));
0859 
0860     m_viewInNewWindow = actionCollection()->addAction(QString::fromLatin1("viewImagesNewWindow"), this, &Window::slotViewNewWindow);
0861     m_viewInNewWindow->setText(i18n("View (In New Window)"));
0862 
0863     m_runSlideShow = actionCollection()->addAction(QString::fromLatin1("runSlideShow"), this, &Window::slotRunSlideShow);
0864     m_runSlideShow->setText(i18n("Run Slide Show"));
0865     m_runSlideShow->setIcon(QIcon::fromTheme(QString::fromLatin1("view-presentation")));
0866     actionCollection()->setDefaultShortcut(m_runSlideShow, Qt::CTRL + Qt::Key_R);
0867 
0868     m_runRandomSlideShow = actionCollection()->addAction(QString::fromLatin1("runRandomizedSlideShow"), this, &Window::slotRunRandomizedSlideShow);
0869     m_runRandomSlideShow->setText(i18n("Run Randomized Slide Show"));
0870 
0871     a = actionCollection()->addAction(QString::fromLatin1("collapseAllStacks"),
0872                                       m_thumbnailView, &ThumbnailView::ThumbnailFacade::collapseAllStacks);
0873     connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::collapseAllStacksEnabled, a, &QAction::setEnabled);
0874     a->setEnabled(false);
0875     a->setText(i18n("Collapse all stacks"));
0876 
0877     a = actionCollection()->addAction(QString::fromLatin1("expandAllStacks"),
0878                                       m_thumbnailView, &ThumbnailView::ThumbnailFacade::expandAllStacks);
0879     connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::expandAllStacksEnabled, a, &QAction::setEnabled);
0880     a->setEnabled(false);
0881     a->setText(i18n("Expand all stacks"));
0882 
0883     QActionGroup *grp = new QActionGroup(this);
0884 
0885     a = actionCollection()->add<KToggleAction>(QString::fromLatin1("orderIncr"), this, &Window::slotOrderIncr);
0886     a->setText(i18n("Show &Oldest First"));
0887     a->setActionGroup(grp);
0888     a->setChecked(!Settings::SettingsData::instance()->showNewestThumbnailFirst());
0889 
0890     a = actionCollection()->add<KToggleAction>(QString::fromLatin1("orderDecr"), this, &Window::slotOrderDecr);
0891     a->setText(i18n("Show &Newest First"));
0892     a->setActionGroup(grp);
0893     a->setChecked(Settings::SettingsData::instance()->showNewestThumbnailFirst());
0894 
0895     m_sortByDateAndTime = actionCollection()->addAction(QString::fromLatin1("sortImages"), this, &Window::slotSortByDateAndTime);
0896     m_sortByDateAndTime->setText(i18n("Sort Selected by Date && Time"));
0897 
0898     m_limitToMarked = actionCollection()->addAction(QString::fromLatin1("limitToMarked"), this, &Window::slotLimitToSelected);
0899     m_limitToMarked->setText(i18n("Limit View to Selection"));
0900 
0901     m_jumpToContext = actionCollection()->addAction(QString::fromLatin1("jumpToContext"), this, &Window::slotJumpToContext);
0902     m_jumpToContext->setText(i18n("Jump to Context"));
0903     actionCollection()->setDefaultShortcut(m_jumpToContext, Qt::CTRL + Qt::Key_J);
0904     m_jumpToContext->setIcon(QIcon::fromTheme(QString::fromLatin1("kphotoalbum"))); // icon suggestion: go-jump (don't know the exact meaning though, so I didn't replace it right away
0905 
0906     m_lock = actionCollection()->addAction(QString::fromLatin1("lockToDefaultScope"), this, &Window::lockToDefaultScope);
0907     m_lock->setIcon(QIcon::fromTheme(QLatin1String("lock")));
0908     m_lock->setText(i18n("Lock Images"));
0909 
0910     m_unlock = actionCollection()->addAction(QString::fromLatin1("unlockFromDefaultScope"), this, &Window::unlockFromDefaultScope);
0911     m_unlock->setIcon(QIcon::fromTheme(QLatin1String("unlock")));
0912     m_unlock->setText(i18n("Unlock"));
0913 
0914     m_setDefaultPos = actionCollection()->addAction(QString::fromLatin1("setDefaultScopePositive"), this, &Window::setDefaultScopePositive);
0915     m_setDefaultPos->setText(i18n("Lock Away All Other Items"));
0916 
0917     m_setDefaultNeg = actionCollection()->addAction(QString::fromLatin1("setDefaultScopeNegative"), this, &Window::setDefaultScopeNegative);
0918     m_setDefaultNeg->setText(i18n("Lock Away Current Set of Items"));
0919 
0920     m_viewMenu = actionCollection()->add<KActionMenu>(QString::fromLatin1("configureView"));
0921     m_viewMenu->setText(i18n("Configure Current View"));
0922     m_viewMenu->setIcon(QIcon::fromTheme(QString::fromLatin1("view-list-details")));
0923 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 77, 0)
0924     m_viewMenu->setPopupMode(QToolButton::InstantPopup);
0925 #else
0926     m_viewMenu->setDelayed(false);
0927 #endif
0928 
0929     QActionGroup *viewGrp = new QActionGroup(this);
0930     viewGrp->setExclusive(true);
0931 
0932     m_smallListView = actionCollection()->add<KToggleAction>(QString::fromLatin1("smallListView"), m_browser, &Browser::BrowserWidget::slotSmallListView);
0933     m_smallListView->setText(i18n("Tree"));
0934     m_viewMenu->addAction(m_smallListView);
0935     m_smallListView->setActionGroup(viewGrp);
0936 
0937     m_largeListView = actionCollection()->add<KToggleAction>(QString::fromLatin1("largelistview"), m_browser, &Browser::BrowserWidget::slotLargeListView);
0938     m_largeListView->setText(i18n("Tree with User Icons"));
0939     m_viewMenu->addAction(m_largeListView);
0940     m_largeListView->setActionGroup(viewGrp);
0941 
0942     m_largeIconView = actionCollection()->add<KToggleAction>(QString::fromLatin1("largeiconview"), m_browser, &Browser::BrowserWidget::slotLargeIconView);
0943     m_largeIconView->setText(i18n("Icons"));
0944     m_viewMenu->addAction(m_largeIconView);
0945     m_largeIconView->setActionGroup(viewGrp);
0946 
0947     m_viewMenu->addSeparator();
0948 
0949     m_sortViewNaturally = actionCollection()->add<KToggleAction>(QString::fromLatin1("sortViewNaturally"), m_browser, &Browser::BrowserWidget::slotSortViewNaturally);
0950     m_sortViewNaturally->setText(i18n("Use natural sort order"));
0951     m_sortViewNaturally->setChecked(Settings::SettingsData::instance()->browserUseNaturalSortOrder());
0952     m_browser->slotSortViewNaturally(m_sortViewNaturally->isChecked());
0953     connect(m_sortViewNaturally, &QAction::toggled, Settings::SettingsData::instance(), &Settings::SettingsData::setBrowserUseNaturalSortOrder);
0954     m_viewMenu->addAction(m_sortViewNaturally);
0955 
0956     connect(m_browser, &Browser::BrowserWidget::isViewChangeable, viewGrp, &QActionGroup::setEnabled);
0957     connect(m_browser, &Browser::BrowserWidget::currentViewTypeChanged, this, &Window::slotUpdateViewMenu);
0958 
0959     a = actionCollection()->add<KToggleAction>(QString::fromLatin1("showToolTipOnImages"));
0960     a->setText(i18n("Show Tooltips in Thumbnails Window"));
0961     actionCollection()->setDefaultShortcut(a, Qt::CTRL + Qt::Key_T);
0962     connect(a, &QAction::toggled, m_thumbnailView, &ThumbnailView::ThumbnailFacade::showToolTipsOnImages);
0963 
0964     a = actionCollection()->add<KToggleAction>(QString::fromLatin1("showLabelBelowThumbnail"));
0965     a->setText(i18n("Show Labels in Thumbnails Window"));
0966     a->setIcon(QIcon::fromTheme(QString::fromLatin1("label")));
0967     a->setChecked(Settings::SettingsData::instance()->displayLabels());
0968     connect(a, &QAction::toggled, this, [this](bool doShow) {
0969         Settings::SettingsData::instance()->setDisplayLabels(doShow);
0970         reloadThumbnails();
0971     });
0972     connect(Settings::SettingsData::instance(), &Settings::SettingsData::displayLabelsChanged, a, &QAction::setChecked);
0973 
0974     a = actionCollection()->add<KToggleAction>(QString::fromLatin1("showCategoryBelowThumbnail"));
0975     a->setText(i18n("Show Categories in Thumbnails Window"));
0976     a->setIcon(QIcon::fromTheme(QString::fromLatin1("category")));
0977     a->setChecked(Settings::SettingsData::instance()->displayCategories());
0978     connect(a, &QAction::toggled, this, [this](bool doShow) {
0979         Settings::SettingsData::instance()->setDisplayCategories(doShow);
0980         reloadThumbnails();
0981     });
0982     connect(Settings::SettingsData::instance(), &Settings::SettingsData::displayCategoriesChanged, a, &QAction::setChecked);
0983 
0984     KColorSchemeManager *schemes = new KColorSchemeManager(this);
0985     const QString schemePath = Settings::SettingsData::instance()->colorScheme();
0986     const auto schemeCfg = KSharedConfig::openConfig(schemePath);
0987     const QString activeSchemeName = schemeCfg->group("General").readEntry("Name", QFileInfo(schemePath).baseName());
0988 #if KCONFIGWIDGETS_VERSION >= QT_VERSION_CHECK(5, 107, 0)
0989     const auto activeSchemeIndex = schemes->indexForScheme(activeSchemeName);
0990     if (activeSchemeIndex.isValid())
0991         schemes->activateScheme(activeSchemeIndex);
0992     m_colorSchemeMenu = KColorSchemeMenu::createMenu(schemes, this);
0993 #else
0994     m_colorSchemeMenu = schemes->createSchemeSelectionMenu(activeSchemeName, this);
0995 #endif
0996     m_colorSchemeMenu->setText(i18n("Choose Color Scheme"));
0997     m_colorSchemeMenu->setIcon(QIcon::fromTheme(QString::fromLatin1("color")));
0998 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 77, 0)
0999     m_colorSchemeMenu->setPopupMode(QToolButton::InstantPopup);
1000 #else
1001     m_colorSchemeMenu->setDelayed(false);
1002 #endif
1003     actionCollection()->addAction(QString::fromLatin1("colorScheme"), m_colorSchemeMenu);
1004 
1005     // Maintenance
1006     a = actionCollection()->addAction(QString::fromLatin1("findUnavailableImages"), this, &Window::slotShowNotOnDisk);
1007     a->setText(i18n("Display Images and Videos Not on Disk"));
1008 
1009     a = actionCollection()->addAction(QString::fromLatin1("findImagesWithInvalidDate"), this, &Window::slotShowImagesWithInvalidDate);
1010     a->setText(i18n("Display Images and Videos with Incomplete Dates..."));
1011 
1012     a = actionCollection()->addAction(QLatin1String("mergeDuplicates"), this, &Window::mergeDuplicates);
1013     a->setText(i18n("Merge duplicates"));
1014     a = actionCollection()->addAction(QString::fromLatin1("rebuildMD5s"), this, &Window::slotRecalcCheckSums);
1015     a->setText(i18n("Refresh Selected Thumbnails and Checksums"));
1016 
1017     a = actionCollection()->addAction(QString::fromLatin1("rescan"), this, &Window::slotRescan);
1018     a->setIcon(QIcon::fromTheme(QString::fromLatin1("document-import")));
1019     a->setText(i18n("Rescan for Images and Videos"));
1020 
1021     QAction *recreateExif = actionCollection()->addAction(QString::fromLatin1("recreateExifDB"), this, &Window::slotRecreateExifDB);
1022     recreateExif->setText(i18n("Recreate Exif Search Database"));
1023 
1024     QAction *rereadExif = actionCollection()->addAction(QString::fromLatin1("reReadExifInfo"), this, &Window::slotReReadExifInfo);
1025     rereadExif->setText(i18n("Read Exif Info from Files..."));
1026 
1027     m_sortAllByDateAndTime = actionCollection()->addAction(QString::fromLatin1("sortAllImages"), this, &Window::slotSortAllByDateAndTime);
1028     m_sortAllByDateAndTime->setText(i18n("Sort All by Date && Time"));
1029     m_sortAllByDateAndTime->setEnabled(true);
1030 
1031     m_AutoStackImages = actionCollection()->addAction(QString::fromLatin1("autoStack"), this, &Window::slotAutoStackImages);
1032     m_AutoStackImages->setText(i18n("Automatically Stack Selected Images..."));
1033 
1034     a = actionCollection()->addAction(QString::fromLatin1("buildThumbs"), this, &Window::slotBuildThumbnails);
1035     a->setText(i18n("Build Thumbnails"));
1036 
1037     a = actionCollection()->addAction(QString::fromLatin1("statistics"), this, &Window::slotStatistics);
1038     a->setText(i18n("Statistics..."));
1039 
1040     m_markUntagged = actionCollection()->addAction(QString::fromUtf8("markUntagged"),
1041                                                    this, &Window::slotMarkUntagged);
1042     m_markUntagged->setText(i18n("Mark As Untagged"));
1043 
1044     // The Settings menu
1045     KStandardAction::preferences(this, &Window::slotOptions, actionCollection());
1046     // the default configureShortcuts impl in XMLGuiFactory that is available via setupGUI
1047     // does not work for us because we need to add our own (non-XMLGui) actionCollections:
1048     KStandardAction::keyBindings(this, &Window::configureShortcuts, actionCollection());
1049 
1050     a = actionCollection()->addAction(QString::fromLatin1("readdAllMessages"), this, &Window::slotReenableMessages);
1051     a->setText(i18n("Enable All Messages"));
1052 
1053     // The help menu
1054 #if KCONFIGWIDGETS_VERSION < QT_VERSION_CHECK(5, 83, 0)
1055     KStandardAction::tipOfDay(this, &Window::showTipOfDay, actionCollection());
1056 #endif
1057 
1058     a = actionCollection()->addAction(QString::fromLatin1("runDemo"), this, &Window::runDemo);
1059     a->setText(i18n("Run KPhotoAlbum Demo"));
1060 
1061     a = actionCollection()->addAction(QString::fromLatin1("features"), this, &Window::showFeatures);
1062     a->setText(i18n("KPhotoAlbum Feature Status"));
1063 
1064     a = actionCollection()->addAction(QString::fromLatin1("showVideo"), this, &Window::showVideos);
1065     a->setText(i18n("Show Demo Videos"));
1066 
1067     // Context menu actions
1068     m_showExifDialog = actionCollection()->addAction(QString::fromLatin1("showExifInfo"), this, &Window::slotShowExifInfo);
1069     m_showExifDialog->setText(i18n("Show Exif Info and file metadata"));
1070 
1071     m_recreateThumbnails = actionCollection()->addAction(QString::fromLatin1("recreateThumbnails"), this, &Window::slotRecalcCheckSums);
1072     m_recreateThumbnails->setText(i18n("Refresh Selected Thumbnails and Checksums"));
1073 
1074     m_useNextVideoThumbnail = actionCollection()->addAction(QString::fromLatin1("useNextVideoThumbnail"), this, &Window::useNextVideoThumbnail);
1075     m_useNextVideoThumbnail->setText(i18n("Use next video thumbnail"));
1076     actionCollection()->setDefaultShortcut(m_useNextVideoThumbnail, Qt::CTRL + Qt::Key_Plus);
1077 
1078     m_usePreviousVideoThumbnail = actionCollection()->addAction(QString::fromLatin1("usePreviousVideoThumbnail"), this, &Window::usePreviousVideoThumbnail);
1079     m_usePreviousVideoThumbnail->setText(i18n("Use previous video thumbnail"));
1080     actionCollection()->setDefaultShortcut(m_usePreviousVideoThumbnail, Qt::CTRL + Qt::Key_Minus);
1081 
1082     m_copyAction = actionCollection()->addAction(QStringLiteral("copyImagesTo"), this, std::bind(&Window::triggerCopyLinkAction, this, CopyLinkEngine::Copy));
1083     m_copyAction->setText(i18np("Copy image to ...", "Copy images to ...", 1));
1084     actionCollection()->setDefaultShortcut(m_copyAction, Qt::Key_F7);
1085 
1086     m_linkAction = actionCollection()->addAction(QStringLiteral("linkImagesTo"), this, std::bind(&Window::triggerCopyLinkAction, this, CopyLinkEngine::Link));
1087     m_linkAction->setText(i18np("Link image to ...", "Link images to ...", 1));
1088     actionCollection()->setDefaultShortcut(m_linkAction, Qt::SHIFT + Qt::Key_F7);
1089 
1090     setupGUI(KXmlGuiWindow::ToolBar | Create | Save);
1091 }
1092 
1093 void MainWindow::Window::slotExportToHTML()
1094 {
1095     if (!m_htmlDialog)
1096         m_htmlDialog = new HTMLGenerator::HTMLDialog(this);
1097     m_htmlDialog->exec(selectedOnDisk());
1098 }
1099 
1100 void MainWindow::Window::startAutoSaveTimer()
1101 {
1102     int i = Settings::SettingsData::instance()->autoSave();
1103     m_autoSaveTimer->stop();
1104     if (i != 0) {
1105         m_autoSaveTimer->start(i * 1000 * 60);
1106     }
1107 }
1108 
1109 void MainWindow::Window::slotAutoSave()
1110 {
1111     if (m_statusBar->mp_dirtyIndicator->isAutoSaveDirty()) {
1112         Utilities::ShowBusyCursor dummy;
1113         m_statusBar->showMessage(i18n("Auto saving...."));
1114         DB::ImageDB::instance()->save(Settings::SettingsData::instance()->imageDirectory() + QString::fromLatin1(".#index.xml"), true);
1115         thumbnailCache()->save();
1116         m_statusBar->showMessage(i18n("Auto saving.... Done"), 5000);
1117         m_statusBar->mp_dirtyIndicator->autoSaved();
1118     }
1119 }
1120 
1121 void MainWindow::Window::showThumbNails()
1122 {
1123     m_statusBar->showThumbnailSlider();
1124     reloadThumbnails(ThumbnailView::ClearSelection);
1125     m_stack->setCurrentWidget(m_thumbnailView->gui());
1126     m_thumbnailView->gui()->setFocus();
1127     updateStates(true);
1128 }
1129 
1130 void MainWindow::Window::showBrowser()
1131 {
1132     m_statusBar->clearMessage();
1133     m_statusBar->hideThumbnailSlider();
1134     m_stack->setCurrentWidget(m_browser);
1135     m_browser->setFocus();
1136     updateContextMenuFromSelectionSize(0);
1137     updateStates(false);
1138 }
1139 
1140 void MainWindow::Window::slotOptionGroupChanged()
1141 {
1142     // deleting the dialog would close it anyways, but I feel that we should not
1143     // depend on this behaviour lest we want it to crash:
1144     if (m_annotationDialog) {
1145         m_annotationDialog->close();
1146         m_annotationDialog->deleteLater();
1147     }
1148 
1149     m_annotationDialog = nullptr;
1150     DirtyIndicator::markDirty();
1151 }
1152 
1153 void MainWindow::Window::slotFilterChanged()
1154 {
1155     m_statusBar->mp_partial->showBrowserMatches(m_thumbnailView->imageList(ThumbnailView::ViewOrder).size());
1156 }
1157 
1158 void MainWindow::Window::showTipOfDay()
1159 {
1160     // deprecated without in-program replacement
1161 #if KCONFIGWIDGETS_VERSION < QT_VERSION_CHECK(5, 83, 0)
1162     KTipDialog::showTip(this, QString(), true);
1163 #endif
1164 }
1165 
1166 void MainWindow::Window::runDemo()
1167 {
1168     KProcess *process = new KProcess;
1169     *process << qApp->applicationFilePath() << QLatin1String("--demo");
1170     process->startDetached();
1171 }
1172 
1173 bool MainWindow::Window::load()
1174 {
1175     // Let first try to find a config file.
1176     QString configFile;
1177 
1178     QUrl dbFileUrl = Options::the()->dbFile();
1179     if (!dbFileUrl.isEmpty() && dbFileUrl.isLocalFile()) {
1180         configFile = dbFileUrl.toLocalFile();
1181     } else if (Options::the()->demoMode()) {
1182         configFile = Utilities::setupDemo();
1183     } else {
1184         bool showWelcome = false;
1185         KConfigGroup config = KSharedConfig::openConfig()->group(QString::fromUtf8("General"));
1186         if (config.hasKey(QString::fromLatin1("imageDBFile"))) {
1187             configFile = config.readEntry<QString>(QString::fromLatin1("imageDBFile"), QString());
1188             if (!QFileInfo::exists(configFile))
1189                 showWelcome = true;
1190         } else
1191             showWelcome = true;
1192 
1193         if (showWelcome) {
1194             SplashScreen::instance()->hide();
1195             configFile = welcome();
1196         }
1197     }
1198     if (configFile.isNull())
1199         return false;
1200 
1201     if (configFile.startsWith(QString::fromLatin1("~")))
1202         configFile = QDir::home().path() + QString::fromLatin1("/") + configFile.mid(1);
1203 
1204     // To avoid a race conditions where both the image loader thread creates an instance of
1205     // Settings, and where the main thread crates an instance, we better get it created now.
1206     Settings::SettingsData::setup(QFileInfo(configFile).absolutePath(), *this);
1207 
1208     if (Settings::SettingsData::instance()->showSplashScreen()) {
1209         SplashScreen::instance()->show();
1210         qApp->processEvents();
1211     }
1212 
1213     // Doing some validation on user provided index file
1214     if (Options::the()->dbFile().isValid()) {
1215         QFileInfo fi(configFile);
1216 
1217         if (!fi.dir().exists()) {
1218             KMessageBox::error(this, i18n("<p>Could not open given index.xml, as the provided folder does not exist.<br />%1</p>", fi.absolutePath()));
1219             return false;
1220         }
1221 
1222         // We use index.xml as the XML backend, thus we want to test for exactly it
1223         fi.setFile(QString::fromLatin1("%1/index.xml").arg(fi.dir().absolutePath()));
1224         if (!fi.exists()) {
1225             const QString question = i18n("<p>Given index file does not exist, do you want to create following?"
1226                                           "<br />%1/index.xml</p>",
1227                                           fi.absolutePath());
1228             const QString title = i18nc("@title", "Create database");
1229 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0)
1230             const auto answer = KMessageBox::questionTwoActions(this, question,
1231                                                                 title,
1232                                                                 KGuiItem(i18nc("@action:button", "Create")),
1233                                                                 KStandardGuiItem::cancel());
1234             if (answer != KMessageBox::ButtonCode::PrimaryAction) {
1235 #else
1236             const auto answer = KMessageBox::questionYesNo(this, question);
1237             if (answer != KMessageBox::Yes) {
1238 #endif
1239                 return false;
1240             }
1241         }
1242         configFile = fi.absoluteFilePath();
1243     }
1244     DB::ImageDB::setupXMLDB(configFile, *this);
1245 
1246     const QString thumbnailDirectory = QDir(Settings::SettingsData::instance()->imageDirectory()).absoluteFilePath(ImageManager::defaultThumbnailDirectory());
1247     m_thumbnailCache = new ImageManager::ThumbnailCache { thumbnailDirectory };
1248     // thumbnail size from cache overrides config value:
1249     Settings::SettingsData::instance()->setThumbnailSize(m_thumbnailCache->thumbnailSize());
1250 
1251     // some sanity checks:
1252     if (!DB::ImageDB::instance()->untaggedCategoryFeatureConfigured()
1253         && !(Settings::SettingsData::instance()->untaggedCategory().isEmpty()
1254              && Settings::SettingsData::instance()->untaggedTag().isEmpty())
1255         && !Options::the()->demoMode()) {
1256         KMessageBox::error(this, i18n("<p>You have configured a tag for untagged images, but either the tag itself "
1257                                       "or its category does not exist in the database.</p>"
1258                                       "<p>Please review your untagged tag setting under "
1259                                       "<interface>Settings|Configure KPhotoAlbum...|Categories</interface></p>"));
1260     }
1261 
1262     return true;
1263 }
1264 
1265 void MainWindow::Window::contextMenuEvent(QContextMenuEvent *e)
1266 {
1267     if (m_stack->currentWidget() == m_thumbnailView->gui()) {
1268         QMenu menu(this);
1269         menu.addAction(m_configOneAtATime);
1270         menu.addAction(m_configAllSimultaniously);
1271         menu.addSeparator();
1272         menu.addAction(m_createImageStack);
1273         menu.addAction(m_unStackImages);
1274         menu.addAction(m_setStackHead);
1275         menu.addSeparator();
1276         menu.addAction(m_runSlideShow);
1277         menu.addAction(m_runRandomSlideShow);
1278         menu.addAction(m_showExifDialog);
1279 
1280         menu.addSeparator();
1281         menu.addAction(m_rotLeft);
1282         menu.addAction(m_rotRight);
1283         menu.addAction(m_recreateThumbnails);
1284         menu.addAction(m_useNextVideoThumbnail);
1285         menu.addAction(m_usePreviousVideoThumbnail);
1286         m_useNextVideoThumbnail->setEnabled(anyVideosSelected());
1287         m_usePreviousVideoThumbnail->setEnabled(anyVideosSelected());
1288         menu.addSeparator();
1289 
1290         menu.addAction(m_view);
1291         menu.addAction(m_viewInNewWindow);
1292 
1293         // "Invoke external program"
1294 
1295         ExternalPopup externalCommands { &menu };
1296         const DB::ImageInfoPtr info = DB::ImageDB::instance()->info(m_thumbnailView->mediaIdUnderCursor());
1297 
1298         const auto selection = selected();
1299         const auto selectionCount = selected().count();
1300 
1301         externalCommands.populate(info, selection);
1302         auto *externalCommandsAction = menu.addMenu(&externalCommands);
1303         if (!info && selectionCount == 0) {
1304             externalCommandsAction->setEnabled(false);
1305         }
1306 
1307         // "Copy to ..." / "Link to ..."
1308 
1309         menu.addSeparator();
1310 
1311         m_copyAction->setText(i18np("Copy image to ...", "Copy images to ...", selectionCount));
1312         menu.addAction(m_copyAction);
1313         m_copyAction->setEnabled(selectionCount > 0);
1314 
1315         m_linkAction->setText(i18np("Link image to ...", "Link images to ...", selectionCount));
1316         menu.addAction(m_linkAction);
1317         m_linkAction->setEnabled(selectionCount > 0);
1318 
1319         menu.exec(QCursor::pos());
1320     }
1321     e->setAccepted(true);
1322 }
1323 
1324 void MainWindow::Window::triggerCopyLinkAction(CopyLinkEngine::Action action)
1325 {
1326     QStringList selection;
1327 
1328     if (m_thumbnailView->gui() == m_stack->currentWidget()) {
1329         selection = selected().toStringList(DB::AbsolutePath);
1330     }
1331 
1332     if (selection.isEmpty()) {
1333         KMessageBox::error(this, i18n("No item is selected."), i18n("No Selection"));
1334         return;
1335     }
1336 
1337     QList<QUrl> selectedFiles;
1338     for (const QString &path : qAsConst(selection)) {
1339         selectedFiles.append(QUrl::fromLocalFile(path));
1340     }
1341 
1342     m_copyLinkEngine->selectTarget(this, selectedFiles, action);
1343 }
1344 
1345 void MainWindow::Window::setDefaultScopePositive()
1346 {
1347     Settings::SettingsData::instance()->setCurrentLock(m_browser->currentContext().getLockData(), false);
1348 }
1349 
1350 void MainWindow::Window::setDefaultScopeNegative()
1351 {
1352     Settings::SettingsData::instance()->setCurrentLock(m_browser->currentContext().getLockData(), true);
1353 }
1354 
1355 void MainWindow::Window::lockToDefaultScope()
1356 {
1357     setLocked(true, false);
1358 }
1359 
1360 void MainWindow::Window::unlockFromDefaultScope()
1361 {
1362     setLocked(false, false);
1363 }
1364 
1365 void MainWindow::Window::setLocked(bool locked, bool force, bool recount)
1366 {
1367     m_statusBar->setLocked(locked);
1368     Settings::SettingsData::instance()->setLocked(locked, force);
1369 
1370     m_lock->setEnabled(!locked);
1371     m_unlock->setEnabled(locked);
1372     m_setDefaultPos->setEnabled(!locked);
1373     m_setDefaultNeg->setEnabled(!locked);
1374     if (recount)
1375         m_browser->reload();
1376 }
1377 
1378 void MainWindow::Window::configureShortcuts()
1379 {
1380     Viewer::ViewerWidget *viewer = new Viewer::ViewerWidget; // Do not show, this is only used to get a key configuration
1381     KShortcutsDialog *dialog = new KShortcutsDialog();
1382     dialog->addCollection(actionCollection(), i18n("General"));
1383     dialog->addCollection(viewer->actions(), i18n("Viewer"));
1384     dialog->addCollection(m_dateBar->actions(), i18nc("I.e. the timeline histogram widget", "Date Bar"));
1385 
1386     createAnnotationDialog();
1387     dialog->addCollection(m_annotationDialog->actions(), i18n("Annotation Dialog"));
1388     dialog->addCollection(m_filterWidget->actions(), i18n("Thumbnail View"));
1389 
1390     dialog->configure();
1391 
1392     delete dialog;
1393     delete viewer;
1394 }
1395 
1396 void MainWindow::Window::slotSetFileName(const DB::FileName &fileName)
1397 {
1398     ImageInfoPtr info;
1399 
1400     if (fileName.isNull())
1401         m_statusBar->clearMessage();
1402     else {
1403         info = DB::ImageDB::instance()->info(fileName);
1404         if (info)
1405             m_statusBar->showMessage(fileName.absolute(), 4000);
1406     }
1407 }
1408 
1409 void MainWindow::Window::updateContextMenuFromSelectionSize(int selectionSize)
1410 {
1411     m_configAllSimultaniously->setEnabled(selectionSize > 1);
1412     m_configOneAtATime->setEnabled(selectionSize >= 1);
1413     m_createImageStack->setEnabled(selectionSize > 1);
1414     m_unStackImages->setEnabled(selectionSize >= 1);
1415     m_setStackHead->setEnabled(selectionSize == 1); // FIXME: do we want to check if it's stacked here?
1416     m_sortByDateAndTime->setEnabled(selectionSize > 1);
1417     m_recreateThumbnails->setEnabled(selectionSize >= 1);
1418     m_rotLeft->setEnabled(selectionSize >= 1);
1419     m_rotRight->setEnabled(selectionSize >= 1);
1420     m_AutoStackImages->setEnabled(selectionSize > 1);
1421     m_markUntagged->setEnabled(selectionSize >= 1);
1422     m_statusBar->mp_selected->setSelectionCount(selectionSize);
1423     m_clearSelection->setEnabled(selectionSize > 0);
1424 }
1425 
1426 void MainWindow::Window::rotateSelected(int angle)
1427 {
1428     const DB::FileNameList list = selected();
1429     if (list.isEmpty()) {
1430         KMessageBox::error(this, i18n("No item is selected."),
1431                            i18n("No Selection"));
1432     } else {
1433         for (const DB::FileName &fileName : list) {
1434             DB::ImageDB::instance()->info(fileName)->rotate(angle);
1435             thumbnailCache()->removeThumbnail(fileName);
1436         }
1437         m_statusBar->mp_dirtyIndicator->markDirty();
1438     }
1439 }
1440 
1441 void MainWindow::Window::slotRotateSelectedLeft()
1442 {
1443     rotateSelected(-90);
1444     reloadThumbnails();
1445 }
1446 
1447 void MainWindow::Window::slotRotateSelectedRight()
1448 {
1449     rotateSelected(90);
1450     reloadThumbnails();
1451 }
1452 
1453 void MainWindow::Window::reloadThumbnails(ThumbnailView::SelectionUpdateMethod method)
1454 {
1455     m_thumbnailView->reload(method);
1456     updateContextMenuFromSelectionSize(m_thumbnailView->selection().size());
1457 }
1458 
1459 void MainWindow::Window::slotUpdateViewMenu(DB::Category::ViewType type)
1460 {
1461     // the view group takes care of deselecting the other items
1462     if (type == DB::Category::TreeView)
1463         m_smallListView->setChecked(true);
1464     else if (type == DB::Category::ThumbedTreeView)
1465         m_largeListView->setChecked(true);
1466     else if (type == DB::Category::ThumbedIconView)
1467         m_largeIconView->setChecked(true);
1468 }
1469 
1470 void MainWindow::Window::slotShowNotOnDisk()
1471 {
1472     DB::FileNameList notOnDisk;
1473     const auto allImages = DB::ImageDB::instance()->files();
1474     for (const DB::FileName &fileName : allImages) {
1475         if (!fileName.exists())
1476             notOnDisk.append(fileName);
1477     }
1478 
1479     showThumbNails(notOnDisk);
1480 }
1481 
1482 void MainWindow::Window::slotShowImagesWithChangedMD5Sum()
1483 {
1484 #ifdef DOES_STILL_NOT_WORK_IN_KPA4
1485     Utilities::ShowBusyCursor dummy;
1486     StringSet changed = DB::ImageDB::instance()->imagesWithMD5Changed();
1487     showThumbNails(changed.toList());
1488 #else // DOES_STILL_NOT_WORK_IN_KPA4
1489     qFatal("Code commented out in MainWindow::Window::slotShowImagesWithChangedMD5Sum");
1490 #endif // DOES_STILL_NOT_WORK_IN_KPA4
1491 }
1492 
1493 void MainWindow::Window::updateStates(bool thumbNailView)
1494 {
1495     m_selectAll->setEnabled(thumbNailView);
1496     m_deleteSelected->setEnabled(thumbNailView);
1497     m_limitToMarked->setEnabled(thumbNailView);
1498     m_jumpToContext->setEnabled(thumbNailView);
1499 }
1500 
1501 void MainWindow::Window::slotRunSlideShow()
1502 {
1503     slotView(true, true);
1504 }
1505 
1506 void MainWindow::Window::slotRunRandomizedSlideShow()
1507 {
1508     slotView(true, true, true);
1509 }
1510 
1511 MainWindow::Window *MainWindow::Window::theMainWindow()
1512 {
1513     Q_ASSERT(s_instance);
1514     return s_instance;
1515 }
1516 
1517 ImageManager::ThumbnailCache *MainWindow::Window::thumbnailCache() const
1518 {
1519     return m_thumbnailCache;
1520 }
1521 
1522 void MainWindow::Window::slotImport()
1523 {
1524     ImportExport::Import::imageImport();
1525 }
1526 
1527 void MainWindow::Window::slotExport()
1528 {
1529     ImportExport::Export::imageExport(selectedOnDisk());
1530 }
1531 
1532 void MainWindow::Window::slotReenableMessages()
1533 {
1534     const QString question = i18n("<p>Really enable all message boxes where you previously "
1535                                   "checked the do-not-show-again check box?</p>");
1536     const QString title = i18nc("@title", "Reset hidden dialogs");
1537 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0)
1538     const auto answer = KMessageBox::questionTwoActions(this, question,
1539                                                         title,
1540                                                         KStandardGuiItem::reset(),
1541                                                         KStandardGuiItem::cancel());
1542     if (answer == KMessageBox::ButtonCode::PrimaryAction) {
1543 #else
1544     const auto answer = KMessageBox::questionYesNo(this, question);
1545     if (answer == KMessageBox::Yes) {
1546 #endif
1547         KMessageBox::enableAllMessages();
1548     }
1549 }
1550 
1551 void MainWindow::Window::setupPluginMenu()
1552 {
1553     QMenu *menu = findChild<QMenu *>(QString::fromLatin1("plugins"));
1554     if (!menu) {
1555         KMessageBox::error(this, i18n("<p>KPhotoAlbum hit an internal error (missing plug-in menu in MainWindow::Window::setupPluginMenu). This indicate that you forgot to do a make install. If you did compile KPhotoAlbum yourself, then please run make install. If not, please report this as a bug.</p><p>KPhotoAlbum will continue execution, but it is not entirely unlikely that it will crash later on due to the missing make install.</p>"), i18n("Internal Error"));
1556         return; // This is no good, but lets try and continue.
1557     }
1558 
1559 #ifdef KF5Purpose_FOUND
1560     Plugins::PurposeMenu *purposeMenu = new Plugins::PurposeMenu(menu);
1561     connect(m_thumbnailView, &ThumbnailView::ThumbnailFacade::selectionChanged,
1562             purposeMenu, &Plugins::PurposeMenu::slotSelectionChanged);
1563     connect(purposeMenu, &Plugins::PurposeMenu::imageShared, this, [this](QUrl shareLocation) {
1564         QString message;
1565         if (shareLocation.isValid()) {
1566             message = i18n("Successfully shared image(s). Copying location to clipboard...");
1567             QGuiApplication::clipboard()->setText(shareLocation.toString());
1568         } else {
1569             message = i18n("Successfully shared image(s).");
1570         }
1571         m_statusBar->showMessage(message);
1572     });
1573     connect(purposeMenu, &Plugins::PurposeMenu::imageSharingFailed, this, [this](QString errorMessage) {
1574         QString message = i18n("Image sharing failed with message: %1", errorMessage);
1575         m_statusBar->showMessage(message);
1576     });
1577 #endif
1578 }
1579 
1580 void MainWindow::Window::setPluginMenuState(const char *name, const QList<QAction *> &actions)
1581 {
1582     QMenu *menu = findChild<QMenu *>(QString::fromLatin1(name));
1583     if (menu)
1584         menu->setEnabled(actions.count() != 0);
1585 }
1586 
1587 void MainWindow::Window::slotImagesChanged(const QList<QUrl> &urls)
1588 {
1589     for (QList<QUrl>::ConstIterator it = urls.begin(); it != urls.end(); ++it) {
1590         DB::FileName fileName = DB::FileName::fromAbsolutePath((*it).path());
1591         if (!fileName.isNull()) {
1592             // Plugins may report images outsite of the photodatabase
1593             // This seems to be the case with the border image plugin, which reports the destination image
1594             thumbnailCache()->removeThumbnail(fileName);
1595             // update MD5sum:
1596             MD5 md5sum = MD5Sum(fileName);
1597             DB::ImageDB::instance()->info(fileName)->setMD5Sum(md5sum);
1598         }
1599     }
1600     m_statusBar->mp_dirtyIndicator->markDirty();
1601     reloadThumbnails(ThumbnailView::MaintainSelection);
1602 }
1603 
1604 DB::ImageSearchInfo MainWindow::Window::currentContext()
1605 {
1606     return m_browser->currentContext();
1607 }
1608 
1609 QString MainWindow::Window::currentBrowseCategory() const
1610 {
1611     return m_browser->currentCategory();
1612 }
1613 
1614 void MainWindow::Window::resizeEvent(QResizeEvent *)
1615 {
1616     if (Settings::SettingsData::ready() && isVisible())
1617         Settings::SettingsData::instance()->setWindowGeometry(Settings::MainWindow, geometry());
1618 }
1619 
1620 void MainWindow::Window::moveEvent(QMoveEvent *)
1621 {
1622     if (Settings::SettingsData::ready() && isVisible())
1623         Settings::SettingsData::instance()->setWindowGeometry(Settings::MainWindow, geometry());
1624 }
1625 
1626 void MainWindow::Window::slotRemoveTokens()
1627 {
1628     if (!m_tokenEditor)
1629         m_tokenEditor = new TokenEditor(this);
1630     m_tokenEditor->show();
1631     connect(m_tokenEditor, &TokenEditor::finished, m_browser, &Browser::BrowserWidget::go);
1632 }
1633 
1634 void MainWindow::Window::slotShowListOfFiles()
1635 {
1636     QStringList list = QInputDialog::getMultiLineText(this,
1637                                                       i18n("Open List of Files"),
1638                                                       i18n("You can open a set of files in KPhotoAlbum's database by listing the files here."))
1639                            .split(QChar::fromLatin1('\n'), Qt::SkipEmptyParts);
1640     if (list.isEmpty())
1641         return;
1642 
1643     DB::FileNameList out;
1644     for (QStringList::const_iterator it = list.constBegin(); it != list.constEnd(); ++it) {
1645         const DB::FileName fileName = Utilities::fileNameFromUserData(*it);
1646         // the file has to already be part of the database
1647         if (DB::ImageDB::instance()->info(fileName))
1648             out.append(fileName);
1649     }
1650 
1651     if (out.isEmpty())
1652         KMessageBox::information(this, i18n("No images matching your input were found."), i18n("No Matches"));
1653     else
1654         showThumbNails(out);
1655 }
1656 
1657 void MainWindow::Window::updateDateBar(const Browser::BreadcrumbList &path)
1658 {
1659     static QString lastPath = QString::fromLatin1("ThisStringShouldNeverBeSeenSoWeUseItAsInitialContent");
1660     if (path.toString() != lastPath)
1661         updateDateBar();
1662     lastPath = path.toString();
1663 }
1664 
1665 void MainWindow::Window::updateDateBar()
1666 {
1667     const auto imageList = DB::ImageDB::instance()->search(Browser::BrowserWidget::instance()->currentContext(), DB::SearchOption::AllowRangeMatch);
1668     m_dateBar->setImageCollection(imageList);
1669 }
1670 
1671 void MainWindow::Window::slotShowImagesWithInvalidDate()
1672 {
1673     QPointer<InvalidDateFinder> finder = new InvalidDateFinder(this);
1674     if (finder->exec() == QDialog::Accepted)
1675         showThumbNails();
1676     delete finder;
1677 }
1678 
1679 void MainWindow::Window::showDateBarTip(const QString &msg)
1680 {
1681     m_statusBar->showMessage(msg, 3000);
1682 }
1683 
1684 void MainWindow::Window::slotJumpToContext()
1685 {
1686     const DB::FileName fileName = m_thumbnailView->currentItem();
1687     if (!fileName.isNull()) {
1688         m_browser->addImageView(fileName);
1689     }
1690 }
1691 
1692 void MainWindow::Window::setDateRange(const DB::ImageDate &range)
1693 {
1694     DB::ImageDB::instance()->setDateRange(range, m_dateBar->includeFuzzyCounts());
1695     m_statusBar->mp_partial->showBrowserMatches(this->selected().size());
1696     m_browser->reload();
1697     reloadThumbnails(ThumbnailView::MaintainSelection);
1698 }
1699 
1700 void MainWindow::Window::clearDateRange()
1701 {
1702     DB::ImageDB::instance()->clearDateRange();
1703     m_browser->reload();
1704     reloadThumbnails(ThumbnailView::MaintainSelection);
1705 }
1706 
1707 void MainWindow::Window::showThumbNails(const FileNameList &items)
1708 {
1709     m_thumbnailView->setImageList(items);
1710     m_statusBar->mp_partial->setMatchCount(items.size());
1711     showThumbNails();
1712 }
1713 
1714 void MainWindow::Window::slotRecalcCheckSums()
1715 {
1716     auto images = selected();
1717 
1718     const auto db = DB::ImageDB::instance();
1719     if (images.isEmpty()) {
1720         images = db->files();
1721         // avoid lookups
1722         db->md5Map()->clear();
1723     }
1724 
1725     DB::NewImageFinder().calculateMD5sums(images, db->md5Map());
1726 }
1727 
1728 void MainWindow::Window::slotRescan()
1729 {
1730     NewImageFinder().findImages();
1731 }
1732 
1733 void MainWindow::Window::slotShowExifInfo()
1734 {
1735     DB::FileNameList items = selectedOnDisk();
1736     if (!items.isEmpty()) {
1737         Exif::InfoDialog *exifDialog = new Exif::InfoDialog(items.at(0), this);
1738         exifDialog->show();
1739     }
1740 }
1741 
1742 void MainWindow::Window::showFeatures()
1743 {
1744     FeatureDialog dialog(this);
1745     dialog.exec();
1746 }
1747 
1748 void MainWindow::Window::showImage(const DB::FileName &fileName)
1749 {
1750     launchViewer(DB::FileNameList() << fileName, true, false, false);
1751 }
1752 
1753 void MainWindow::Window::slotBuildThumbnails()
1754 {
1755     ImageManager::ThumbnailBuilder::instance()->buildAll(ImageManager::StartNow);
1756 }
1757 
1758 void MainWindow::Window::slotBuildThumbnailsIfWanted()
1759 {
1760     if (!Settings::SettingsData::instance()->incrementalThumbnails())
1761         ImageManager::ThumbnailBuilder::instance()->buildAll(ImageManager::StartDelayed);
1762 }
1763 
1764 void MainWindow::Window::slotOrderIncr()
1765 {
1766     m_thumbnailView->setSortDirection(ThumbnailView::OldestFirst);
1767 }
1768 
1769 void MainWindow::Window::slotOrderDecr()
1770 {
1771     m_thumbnailView->setSortDirection(ThumbnailView::NewestFirst);
1772 }
1773 
1774 void MainWindow::Window::showVideos()
1775 {
1776     QDesktopServices::openUrl(QUrl(
1777         QStringLiteral("https://www.kphotoalbum.org/documentation/videos/")));
1778 }
1779 
1780 void MainWindow::Window::slotStatistics()
1781 {
1782     static StatisticsDialog *dialog = new StatisticsDialog(this);
1783     dialog->show();
1784 }
1785 
1786 void MainWindow::Window::slotMarkUntagged()
1787 {
1788     if (DB::ImageDB::instance()->untaggedCategoryFeatureConfigured()) {
1789         for (const DB::FileName &newFile : selected()) {
1790             DB::ImageDB::instance()->info(newFile)->addCategoryInfo(Settings::SettingsData::instance()->untaggedCategory(),
1791                                                                     Settings::SettingsData::instance()->untaggedTag());
1792         }
1793 
1794         DirtyIndicator::markDirty();
1795     } else {
1796         // Note: the same dialog text is used in
1797         // Browser::OverviewPage::activateUntaggedImagesAction(),
1798         // so if it is changed, be sure to also change it there!
1799         KMessageBox::information(this,
1800                                  i18n("<p>You have not yet configured which tag to use for indicating untagged images."
1801                                       "</p>"
1802                                       "<p>Please follow these steps to do so:"
1803                                       "<ul><li>In the menu bar choose <b>Settings</b></li>"
1804                                       "<li>From there choose <b>Configure KPhotoAlbum</b></li>"
1805                                       "<li>Now choose the <b>Categories</b> icon</li>"
1806                                       "<li>Now configure section <b>Untagged Images</b></li></ul></p>"),
1807                                  i18n("Feature has not been configured"));
1808     }
1809 }
1810 
1811 void MainWindow::Window::setupStatusBar()
1812 {
1813     m_statusBar = new MainWindow::StatusBar;
1814     setStatusBar(m_statusBar);
1815     setLocked(Settings::SettingsData::instance()->locked(), true, false);
1816     connect(m_statusBar, &StatusBar::thumbnailSettingsRequested, this, [this]() { this->slotOptions(); m_settingsDialog->activatePage(Settings::SettingsPage::ThumbnailsPage); });
1817 }
1818 
1819 void MainWindow::Window::slotRecreateExifDB()
1820 {
1821     const auto allImageFiles = DB::ImageDB::instance()->files(DB::MediaType::Image);
1822     DB::ProgressDialog<QProgressDialog> dialog;
1823     dialog.setModal(true);
1824     dialog.setLabelText(i18n("Rereading Exif information from all images"));
1825 
1826     DB::ImageDB::instance()->exifDB()->recreate(allImageFiles, dialog);
1827 }
1828 
1829 void MainWindow::Window::useNextVideoThumbnail()
1830 {
1831     UpdateVideoThumbnail::useNext(selected());
1832 }
1833 
1834 void MainWindow::Window::usePreviousVideoThumbnail()
1835 {
1836     UpdateVideoThumbnail::usePrevious(selected());
1837 }
1838 
1839 void MainWindow::Window::mergeDuplicates()
1840 {
1841     DuplicateMerger *merger = new DuplicateMerger;
1842     merger->show();
1843 }
1844 
1845 void MainWindow::Window::slotThumbnailSizeChanged()
1846 {
1847     QPixmapCache::clear();
1848 
1849     QString thumbnailSizeMsg = i18nc("@info:status",
1850                                      // xgettext:no-c-format
1851                                      "Thumbnail width: %1px (storage size: %2px)",
1852                                      Settings::SettingsData::instance()->actualThumbnailSize(),
1853                                      Settings::SettingsData::instance()->thumbnailSize());
1854     m_statusBar->showMessage(thumbnailSizeMsg, 4000);
1855 }
1856 
1857 void MainWindow::Window::createSearchBar()
1858 {
1859     // Set up the search tool bar
1860     m_searchBar = new SearchBar(this);
1861     m_searchBar->setLineEditEnabled(false);
1862     m_searchBar->setObjectName(QString::fromUtf8("searchBar"));
1863 
1864     connect(m_searchBar, &SearchBar::textChanged, m_browser, &Browser::BrowserWidget::slotLimitToMatch);
1865     connect(m_searchBar, &SearchBar::returnPressed, m_browser, &Browser::BrowserWidget::slotInvokeSeleted);
1866     connect(m_searchBar, &SearchBar::movementKeyPressed, m_browser, &Browser::BrowserWidget::scrollKeyPressed);
1867     connect(m_browser, &Browser::BrowserWidget::viewChanged, m_searchBar, &SearchBar::clear);
1868     connect(m_browser, &Browser::BrowserWidget::isSearchable, m_searchBar, &SearchBar::setLineEditEnabled);
1869 
1870     m_filterWidget = m_thumbnailView->createFilterWidget(this);
1871     addToolBar(m_filterWidget);
1872     m_filterWidget->setObjectName(QString::fromUtf8("filterBar"));
1873     connect(m_browser, &Browser::BrowserWidget::viewChanged, ThumbnailView::ThumbnailFacade::instance(), &ThumbnailView::ThumbnailFacade::clearFilter);
1874     connect(m_browser, &Browser::BrowserWidget::isFilterable, m_filterWidget, &ThumbnailView::FilterWidget::setEnabled);
1875     connect(m_searchBar, &SearchBar::textChanged, ThumbnailView::ThumbnailFacade::instance(), &ThumbnailView::ThumbnailFacade::setFreeformFilter);
1876     connect(m_searchBar, &SearchBar::cleared, ThumbnailView::ThumbnailFacade::instance(), &ThumbnailView::ThumbnailFacade::clearFilter);
1877 }
1878 
1879 void MainWindow::Window::executeStartupActions()
1880 {
1881     Q_ASSERT(m_thumbnailCache);
1882     new ImageManager::ThumbnailBuilder(m_statusBar, this, m_thumbnailCache);
1883     if (!Settings::SettingsData::instance()->incrementalThumbnails())
1884         ImageManager::ThumbnailBuilder::instance()->buildMissing();
1885     connect(m_thumbnailCache, &ImageManager::ThumbnailCache::cacheInvalidated,
1886             this, &Window::slotBuildThumbnailsIfWanted);
1887 
1888     if (!FeatureDialog::hasVideoThumbnailer()) {
1889         BackgroundTaskManager::JobManager::instance()->addJob(
1890             new BackgroundJobs::SearchForVideosWithoutLengthInfo);
1891 
1892         BackgroundTaskManager::JobManager::instance()->addJob(
1893             new BackgroundJobs::SearchForVideosWithoutVideoThumbnailsJob);
1894     }
1895 }
1896 
1897 void MainWindow::Window::checkIfVideoThumbnailerIsInstalled()
1898 {
1899     if (Options::the()->demoMode())
1900         return;
1901 
1902     if (!FeatureDialog::hasVideoThumbnailer()) {
1903         KMessageBox::information(this,
1904                                  i18n("<p>Unable to find ffmpeg on the system.</p>"
1905                                       "<p>Without it, KPhotoAlbum will not be able to display video thumbnails and video lengths. "
1906                                       "Please install the ffmpeg package</p>"),
1907                                  i18n("Video thumbnails are not available"), QString::fromLatin1("VideoThumbnailerNotInstalled"));
1908     }
1909 }
1910 
1911 bool MainWindow::Window::anyVideosSelected() const
1912 {
1913     const auto selectedFiles = selected();
1914     for (const DB::FileName &fileName : selectedFiles) {
1915         if (KPABase::isVideo(fileName))
1916             return true;
1917     }
1918     return false;
1919 }
1920 
1921 void MainWindow::Window::setHistogramVisibilty(bool visible) const
1922 {
1923     if (visible) {
1924         m_dateBar->show();
1925         m_dateBarLine->show();
1926     } else {
1927         m_dateBar->hide();
1928         m_dateBarLine->hide();
1929     }
1930 }
1931 
1932 void MainWindow::Window::slotImageRotated(const DB::FileName &fileName)
1933 {
1934     // An image has been rotated by the annotation dialog or the viewer.
1935     // We have to reload the respective thumbnail to get it in the right angle
1936     thumbnailCache()->removeThumbnail(fileName);
1937 }
1938 
1939 bool MainWindow::Window::dbIsDirty() const
1940 {
1941     return m_statusBar->mp_dirtyIndicator->isSaveDirty();
1942 }
1943 
1944 #ifdef HAVE_MARBLE
1945 void MainWindow::Window::showPositionBrowser()
1946 {
1947     auto positionBrowser = positionBrowserWidget();
1948     m_stack->setCurrentWidget(positionBrowser);
1949     updateStates(false);
1950 }
1951 
1952 Map::MapView *MainWindow::Window::positionBrowserWidget()
1953 {
1954     if (!m_positionBrowser) {
1955         m_positionBrowser = new Map::MapView(m_stack, Map::UsageType::InlineMapView);
1956         m_stack->addWidget(m_positionBrowser);
1957     }
1958     return m_positionBrowser;
1959 }
1960 #endif
1961 
1962 UserFeedback MainWindow::Window::askWarningContinueCancel(const QString &msg, const QString &title, const QString &dialogId)
1963 {
1964     auto answer = KMessageBox::warningContinueCancel(this, msg, title, KStandardGuiItem::cont(), KStandardGuiItem::cancel(), dialogId);
1965     return (answer == KMessageBox::Continue) ? UserFeedback::Confirm : UserFeedback::Deny;
1966 }
1967 
1968 UserFeedback MainWindow::Window::askQuestionYesNo(const QString &msg, const QString &title, const QString &dialogId)
1969 {
1970 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0)
1971     const auto answer = KMessageBox::questionTwoActions(this, msg,
1972                                                         title,
1973                                                         KStandardGuiItem::ok(),
1974                                                         KStandardGuiItem::cancel(),
1975                                                         dialogId);
1976     const UserFeedback value = (answer == KMessageBox::ButtonCode::PrimaryAction) ? UserFeedback::Confirm : UserFeedback::Deny;
1977 #else
1978     const auto answer = KMessageBox::questionYesNo(this, msg, title, KStandardGuiItem::yes(), KStandardGuiItem::no(), dialogId);
1979     const UserFeedback value = (answer == KMessageBox::Yes) ? UserFeedback::Confirm : UserFeedback::Deny;
1980 #endif
1981     return value;
1982 }
1983 
1984 void MainWindow::Window::showInformation(const QString &msg, const QString &title, const QString &dialogId)
1985 {
1986     KMessageBox::information(this, msg, title, dialogId);
1987 }
1988 
1989 void MainWindow::Window::showError(const QString &msg, const QString &title, const QString &)
1990 {
1991     KMessageBox::error(this, msg, title);
1992 }
1993 
1994 bool MainWindow::Window::isDialogDisabled(const QString &dialogId)
1995 {
1996     // Note(jzarl): there are different methods for different kinds of dialogs.
1997     // However, all these methods share exactly the same code in KMessageBox.
1998     // If that ever changes, we can still update our implementation - until then I won't just copy a stupid API...
1999     return !KMessageBox::shouldBeShownContinue(dialogId);
2000 }
2001 
2002 // vi:expandtab:tabstop=4 shiftwidth=4:
2003 
2004 #include "moc_Window.cpp"