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 ¤t : 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"