File indexing completed on 2024-04-28 04:21:28

0001 // SPDX-FileCopyrightText: 2003 David Faure <faure@kde.org>
0002 // SPDX-FileCopyrightText: 2003-2005 Stephan Binner <binner@kde.org>
0003 // SPDX-FileCopyrightText: 2003-2007 Dirk Mueller <mueller@kde.org>
0004 // SPDX-FileCopyrightText: 2003-2023 Jesper K. Pedersen <jesper.pedersen@kdab.com>
0005 // SPDX-FileCopyrightText: 2004 Marc Mutz <mutz@kde.org>
0006 // SPDX-FileCopyrightText: 2006-2010 Tuomas Suutari <tuomas@nepnep.net>
0007 // SPDX-FileCopyrightText: 2007 Shawn Willden <shawn-kimdaba@willden.org>
0008 // SPDX-FileCopyrightText: 2007 Thiago Macieira <thiago@kde.org>
0009 // SPDX-FileCopyrightText: 2007-2008 Laurent Montel <montel@kde.org>
0010 // SPDX-FileCopyrightText: 2007-2010 Jan Kundrát <jkt@flaska.net>
0011 // SPDX-FileCopyrightText: 2008 Henner Zeller <h.zeller@acm.org>
0012 // SPDX-FileCopyrightText: 2008 Luboš Luňák <l.lunak@kde.org>
0013 // SPDX-FileCopyrightText: 2009, 2022 Yuri Chornoivan <yurchor@ukr.net>
0014 // SPDX-FileCopyrightText: 2009-2012 Miika Turkia <miika.turkia@gmail.com>
0015 // SPDX-FileCopyrightText: 2010 Wes Hardaker <kpa@capturedonearth.com>
0016 // SPDX-FileCopyrightText: 2013-2024 Johannes Zarl-Zierl <johannes@zarl-zierl.at>
0017 // SPDX-FileCopyrightText: 2014-2022 Tobias Leupold <tl@stonemx.de>
0018 // SPDX-FileCopyrightText: 2015-2020 Robert Krawitz <rlk@alum.mit.edu>
0019 // SPDX-FileCopyrightText: 2018 Antoni Bella Pérez <antonibella5@yahoo.com>
0020 // SPDX-FileCopyrightText: 2022 Friedrich W. H. Kossebau <kossebau@kde.org>
0021 //
0022 // SPDX-License-Identifier: GPL-2.0-or-later
0023 
0024 #include "ViewerWidget.h"
0025 #include <config-kpa-videobackends.h>
0026 
0027 #include "CategoryImageConfig.h"
0028 #include "CursorVisibilityHandler.h"
0029 #include "ImageDisplay.h"
0030 #include "InfoBox.h"
0031 #include "Logging.h"
0032 
0033 #if Phonon4Qt5_FOUND
0034 #include "PhononDisplay.h"
0035 #endif
0036 
0037 #if QtAV_FOUND
0038 #include "QtAVDisplay.h"
0039 #endif
0040 
0041 #include "TaggedArea.h"
0042 #include "TextDisplay.h"
0043 #include "TransientDisplay.h"
0044 
0045 #if LIBVLC_FOUND
0046 #include "VLCDisplay.h"
0047 #endif
0048 
0049 #include "AnnotationHandler.h"
0050 #include "VideoDisplay.h"
0051 #include "VideoShooter.h"
0052 #include "VisibleOptionsMenu.h"
0053 
0054 #include <DB/CategoryCollection.h>
0055 #include <DB/ImageDB.h>
0056 #include <Exif/InfoDialog.h>
0057 #include <MainWindow/CategoryImagePopup.h>
0058 #include <MainWindow/DeleteDialog.h>
0059 #include <MainWindow/DirtyIndicator.h>
0060 #include <MainWindow/ExternalPopup.h>
0061 #include <MainWindow/Window.h>
0062 #include <Settings/VideoPlayerSelectorDialog.h>
0063 #include <Utilities/DescriptionUtil.h>
0064 #include <kpabase/FileExtensions.h>
0065 #include <kpabase/SettingsData.h>
0066 #include <kpathumbnails/ThumbnailCache.h>
0067 
0068 #include <KActionCollection>
0069 #include <KColorScheme>
0070 #include <KIO/CopyJob>
0071 #include <KIconLoader>
0072 #include <KLocalizedString>
0073 #include <KMessageBox>
0074 #include <QAction>
0075 #include <QApplication>
0076 #include <QContextMenuEvent>
0077 #include <QDBusConnection>
0078 #include <QDBusMessage>
0079 #include <QDebug>
0080 #include <QDesktopWidget>
0081 #include <QElapsedTimer>
0082 #include <QEventLoop>
0083 #include <QFileDialog>
0084 #include <QFileInfo>
0085 #include <QKeyEvent>
0086 #include <QList>
0087 #include <QPushButton>
0088 #include <QResizeEvent>
0089 #include <QStackedWidget>
0090 #include <QTimeLine>
0091 #include <QTimer>
0092 #include <QWheelEvent>
0093 #include <qglobal.h>
0094 
0095 #include <QDesktopServices>
0096 #include <QInputDialog>
0097 #include <QMetaEnum>
0098 #include <functional>
0099 
0100 using namespace std::chrono_literals;
0101 
0102 Viewer::ViewerWidget *Viewer::ViewerWidget::s_latest = nullptr;
0103 
0104 Viewer::ViewerWidget *Viewer::ViewerWidget::latest()
0105 {
0106     return s_latest;
0107 }
0108 
0109 // Notice the parent is zero to allow other windows to come on top of it.
0110 Viewer::ViewerWidget::ViewerWidget(UsageType type)
0111     : QStackedWidget(nullptr)
0112     , m_crashSentinel(QString::fromUtf8("videoBackend"))
0113     , m_screenSaverCookie(-1)
0114     , m_current(0)
0115     , m_popup(nullptr)
0116     , m_showingFullScreen(false)
0117     , m_forward(true)
0118     , m_isRunningSlideShow(false)
0119     , m_videoPlayerStoppedManually(false)
0120     , m_type(type)
0121     , m_copyLinkEngine(nullptr)
0122     , m_annotationHandler(new AnnotationHandler(this))
0123 {
0124     if (type == UsageType::FullFeaturedViewer) {
0125         setWindowFlags(Qt::Window);
0126         setAttribute(Qt::WA_DeleteOnClose);
0127         s_latest = this;
0128     }
0129 
0130     m_display = m_imageDisplay = new ImageDisplay(this);
0131     addWidget(m_imageDisplay);
0132     m_cursorHandlerForImageDisplay = new CursorVisibilityHandler(m_imageDisplay);
0133 
0134     m_textDisplay = new TextDisplay(this);
0135     addWidget(m_textDisplay);
0136 
0137     createVideoViewer();
0138 
0139     connect(m_imageDisplay, &ImageDisplay::possibleChange, this, &ViewerWidget::updateCategoryConfig);
0140     connect(m_imageDisplay, &ImageDisplay::imageReady, this, &ViewerWidget::updateInfoBox);
0141     connect(m_imageDisplay, &ImageDisplay::imageZoomCaptionChanged, this, &ViewerWidget::setCaptionWithDetail);
0142     connect(m_imageDisplay, &ImageDisplay::viewGeometryChanged, this, &ViewerWidget::remapAreas);
0143 
0144     // This must not be added to the layout, as it is standing on top of
0145     // the ImageDisplay
0146     m_infoBox = new InfoBox(this);
0147     m_infoBox->hide();
0148 
0149     setupContextMenu();
0150 
0151     m_slideShowTimer = new QTimer(this);
0152     m_slideShowTimer->setSingleShot(true);
0153     m_slideShowPause = Settings::SettingsData::instance()->slideShowInterval() * 1000;
0154     connect(m_slideShowTimer, &QTimer::timeout, this, &ViewerWidget::slotSlideShowNextFromTimer);
0155     m_transientDisplay = new TransientDisplay(this);
0156     m_transientDisplay->hide();
0157 
0158     setFocusPolicy(Qt::StrongFocus);
0159 
0160     QTimer::singleShot(2000, this, &ViewerWidget::test);
0161 
0162     connect(DB::ImageDB::instance(), &DB::ImageDB::imagesDeleted, this, &ViewerWidget::slotRemoveDeletedImages);
0163 
0164     updatePalette();
0165     connect(Settings::SettingsData::instance(), &Settings::SettingsData::colorSchemeChanged, this, &ViewerWidget::updatePalette);
0166 
0167     connect(m_annotationHandler, &AnnotationHandler::requestToggleCategory,
0168             this, &Viewer::ViewerWidget::toggleTag);
0169     connect(m_annotationHandler, &AnnotationHandler::requestHelp,
0170             this, &Viewer::ViewerWidget::showAnnotationHelp);
0171 }
0172 
0173 void Viewer::ViewerWidget::setupContextMenu()
0174 {
0175     m_popup = new QMenu(this);
0176     m_actions = new KActionCollection(this);
0177 
0178     // we make unused features invisible to avoid uninitialized values all over the class
0179     const bool showFullFeatures = m_type == UsageType::FullFeaturedViewer;
0180 
0181     createAnnotationMenu();
0182     createSlideShowMenu();
0183     createZoomMenu();
0184     createRotateMenu();
0185     createSkipMenu();
0186     createShowContextMenu();
0187     createInvokeExternalMenu();
0188     createVideoMenu();
0189     createCategoryImageMenu();
0190     createFilterMenu();
0191 
0192     m_setStackHead = m_actions->addAction(QString::fromLatin1("viewer-set-stack-head"), this, &ViewerWidget::slotSetStackHead);
0193     m_setStackHead->setText(i18nc("@action:inmenu", "Set as First Image in Stack"));
0194     m_setStackHead->setVisible(showFullFeatures);
0195     m_actions->setDefaultShortcut(m_setStackHead, Qt::CTRL + Qt::Key_4);
0196     m_popup->addAction(m_setStackHead);
0197 
0198     m_showExifViewer = m_actions->addAction(QString::fromLatin1("viewer-show-exif-viewer"), this, &ViewerWidget::showExifViewer);
0199     m_showExifViewer->setText(i18n("Show Exif Info and file metadata"));
0200     m_popup->addAction(m_showExifViewer);
0201 
0202     m_popup->addSeparator();
0203 
0204     m_copyToAction = m_actions->addAction(QStringLiteral("viewer-copy-to"), this, std::bind(&ViewerWidget::triggerCopyLinkAction, this, MainWindow::CopyLinkEngine::Copy));
0205     m_copyToAction->setText(i18nc("@action:inmenu", "Copy image to ..."));
0206     m_copyToAction->setVisible(showFullFeatures);
0207     m_actions->setDefaultShortcut(m_copyToAction, Qt::Key_F7);
0208     m_popup->addAction(m_copyToAction);
0209 
0210     m_linkToAction = m_actions->addAction(QStringLiteral("viewer-link-to"), this, std::bind(&ViewerWidget::triggerCopyLinkAction, this, MainWindow::CopyLinkEngine::Link));
0211     m_linkToAction->setText(i18nc("@action:inmenu", "Link image to ..."));
0212     m_linkToAction->setVisible(showFullFeatures);
0213     m_actions->setDefaultShortcut(m_linkToAction, Qt::SHIFT + Qt::Key_F7);
0214     m_popup->addAction(m_linkToAction);
0215 
0216     m_popup->addSeparator();
0217 
0218     auto action = m_actions->addAction(QString::fromLatin1("viewer-close"), this, &ViewerWidget::close);
0219     action->setText(i18nc("@action:inmenu", "Close"));
0220     action->setShortcut(Qt::Key_Escape);
0221     action->setVisible(showFullFeatures);
0222     m_actions->setShortcutsConfigurable(action, false);
0223     m_popup->addAction(action);
0224 
0225     m_actions->readSettings();
0226 
0227     const auto actions = m_actions->actions();
0228     for (QAction *action : actions) {
0229         action->setShortcutContext(Qt::WindowShortcut);
0230         addAction(action);
0231     }
0232 }
0233 
0234 void Viewer::ViewerWidget::createShowContextMenu()
0235 {
0236     VisibleOptionsMenu *menu = new VisibleOptionsMenu(this, m_actions);
0237     connect(menu, &VisibleOptionsMenu::visibleOptionsChanged, this, &ViewerWidget::updateInfoBox);
0238     const bool showFullFeatures = m_type == UsageType::FullFeaturedViewer;
0239     m_popup->addMenu(menu)->setVisible(showFullFeatures);
0240 }
0241 
0242 void Viewer::ViewerWidget::inhibitScreenSaver(bool inhibit)
0243 {
0244     QDBusMessage message;
0245     if (inhibit) {
0246         message = QDBusMessage::createMethodCall(QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("/ScreenSaver"),
0247                                                  QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("Inhibit"));
0248 
0249         message << QString(QString::fromLatin1("KPhotoAlbum"));
0250         message << QString(QString::fromLatin1("Giving a slideshow"));
0251         QDBusMessage reply = QDBusConnection::sessionBus().call(message);
0252         if (reply.type() == QDBusMessage::ReplyMessage)
0253             m_screenSaverCookie = reply.arguments().constFirst().toInt();
0254     } else {
0255         if (m_screenSaverCookie != -1) {
0256             message = QDBusMessage::createMethodCall(QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("/ScreenSaver"),
0257                                                      QString::fromLatin1("org.freedesktop.ScreenSaver"), QString::fromLatin1("UnInhibit"));
0258             message << (uint)m_screenSaverCookie;
0259             QDBusConnection::sessionBus().send(message);
0260             m_screenSaverCookie = -1;
0261         }
0262     }
0263 }
0264 
0265 DB::FileName Viewer::ViewerWidget::currentFileName() const
0266 {
0267     return m_list.value(m_current);
0268 }
0269 
0270 void Viewer::ViewerWidget::createInvokeExternalMenu()
0271 {
0272     m_externalPopup = new MainWindow::ExternalPopup(m_popup);
0273     const bool showFullFeatures = m_type == UsageType::FullFeaturedViewer;
0274     m_popup->addMenu(m_externalPopup)->setVisible(showFullFeatures);
0275     connect(m_externalPopup, &MainWindow::ExternalPopup::aboutToShow, this, &ViewerWidget::populateExternalPopup);
0276 }
0277 
0278 void Viewer::ViewerWidget::createRotateMenu()
0279 {
0280     m_rotateMenu = new QMenu(m_popup);
0281     m_rotateMenu->setTitle(i18nc("@title:inmenu", "Rotate"));
0282 
0283     auto addRotateAction = [this](const QString &title, int angle, const QKeySequence &shortcut, const QString &actionName) {
0284         auto *action = new QAction(title);
0285         connect(action, &QAction::triggered, [this, angle] { rotate(angle); });
0286         action->setShortcut(shortcut);
0287         m_actions->setShortcutsConfigurable(action, false);
0288         m_actions->addAction(actionName, action);
0289         m_rotateMenu->addAction(action);
0290     };
0291 
0292     addRotateAction(i18nc("@action:inmenu", "Rotate clockwise"), 90, Qt::Key_9, QString::fromLatin1("viewer-rotate90"));
0293     addRotateAction(i18nc("@action:inmenu", "Flip Over"), 180, Qt::Key_8, QString::fromLatin1("viewer-rotate180"));
0294     addRotateAction(i18nc("@action:inmenu", "Rotate counterclockwise"), 270, Qt::Key_7, QString::fromLatin1("viewer-rotate270"));
0295     const bool showFullFeatures = m_type == UsageType::FullFeaturedViewer;
0296     // hide entries of hidden menus so that they can't be triggered via shortcut:
0297     for (auto &action : m_rotateMenu->actions())
0298         action->setVisible(showFullFeatures);
0299     m_popup->addMenu(m_rotateMenu)->setVisible(showFullFeatures);
0300 }
0301 
0302 void Viewer::ViewerWidget::createSkipMenu()
0303 {
0304     QMenu *popup = new QMenu(m_popup);
0305     popup->setTitle(i18nc("@title:inmenu As in 'skip 2 images'", "Skip"));
0306 
0307     QAction *action = m_actions->addAction(QString::fromLatin1("viewer-home"), this, &ViewerWidget::showFirst);
0308     action->setText(i18nc("@action:inmenu Go to first image", "First"));
0309     action->setShortcut(Qt::Key_Home);
0310     m_actions->setShortcutsConfigurable(action, false);
0311     popup->addAction(action);
0312     m_backwardActions.append(action);
0313 
0314     action = m_actions->addAction(QString::fromLatin1("viewer-end"), this, &ViewerWidget::showLast);
0315     action->setText(i18nc("@action:inmenu Go to last image", "Last"));
0316     action->setShortcut(Qt::Key_End);
0317     m_actions->setShortcutsConfigurable(action, false);
0318     popup->addAction(action);
0319     m_forwardActions.append(action);
0320 
0321     action = m_actions->addAction(QString::fromLatin1("viewer-next"), this, &ViewerWidget::showNext);
0322     action->setText(i18nc("@action:inmenu", "Show Next"));
0323     action->setShortcuts(QList<QKeySequence>() << Qt::Key_PageDown << Qt::Key_Space);
0324     popup->addAction(action);
0325     m_forwardActions.append(action);
0326 
0327     action = m_actions->addAction(QString::fromLatin1("viewer-next-10"), this, &ViewerWidget::showNext10);
0328     action->setText(i18nc("@action:inmenu", "Skip 10 Forward"));
0329     m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_PageDown);
0330     popup->addAction(action);
0331     m_forwardActions.append(action);
0332 
0333     action = m_actions->addAction(QString::fromLatin1("viewer-next-100"), this, &ViewerWidget::showNext100);
0334     action->setText(i18nc("@action:inmenu", "Skip 100 Forward"));
0335     m_actions->setDefaultShortcut(action, Qt::SHIFT + Qt::Key_PageDown);
0336     popup->addAction(action);
0337     m_forwardActions.append(action);
0338 
0339     action = m_actions->addAction(QString::fromLatin1("viewer-next-1000"), this, &ViewerWidget::showNext1000);
0340     action->setText(i18nc("@action:inmenu", "Skip 1000 Forward"));
0341     m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::SHIFT + Qt::Key_PageDown);
0342     popup->addAction(action);
0343     m_forwardActions.append(action);
0344 
0345     action = m_actions->addAction(QString::fromLatin1("viewer-prev"), this, &ViewerWidget::showPrev);
0346     action->setText(i18nc("@action:inmenu", "Show Previous"));
0347     action->setShortcuts(QList<QKeySequence>() << Qt::Key_PageUp << Qt::Key_Backspace);
0348     popup->addAction(action);
0349     m_backwardActions.append(action);
0350 
0351     action = m_actions->addAction(QString::fromLatin1("viewer-prev-10"), this, &ViewerWidget::showPrev10);
0352     action->setText(i18nc("@action:inmenu", "Skip 10 Backward"));
0353     m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_PageUp);
0354     popup->addAction(action);
0355     m_backwardActions.append(action);
0356 
0357     action = m_actions->addAction(QString::fromLatin1("viewer-prev-100"), this, &ViewerWidget::showPrev100);
0358     action->setText(i18nc("@action:inmenu", "Skip 100 Backward"));
0359     m_actions->setDefaultShortcut(action, Qt::SHIFT + Qt::Key_PageUp);
0360     popup->addAction(action);
0361     m_backwardActions.append(action);
0362 
0363     action = m_actions->addAction(QString::fromLatin1("viewer-prev-1000"), this, &ViewerWidget::showPrev1000);
0364     action->setText(i18nc("@action:inmenu", "Skip 1000 Backward"));
0365     m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::SHIFT + Qt::Key_PageUp);
0366     popup->addAction(action);
0367     m_backwardActions.append(action);
0368 
0369     action = m_actions->addAction(QString::fromLatin1("viewer-delete-current"), this, &ViewerWidget::deleteCurrent);
0370     action->setText(i18nc("@action:inmenu", "Delete Image"));
0371     m_actions->setDefaultShortcut(action, Qt::CTRL + Qt::Key_Delete);
0372     popup->addAction(action);
0373 
0374     action = m_actions->addAction(QString::fromLatin1("viewer-remove-current"), this, &ViewerWidget::removeCurrent);
0375     action->setText(i18nc("@action:inmenu", "Remove Image from Display List"));
0376     action->setShortcut(Qt::Key_Delete);
0377     m_actions->setShortcutsConfigurable(action, false);
0378     popup->addAction(action);
0379 
0380     const bool showFullFeatures = m_type == UsageType::FullFeaturedViewer;
0381     // hide entries of hidden menus so that they can't be triggered via shortcut:
0382     for (auto &action : popup->actions())
0383         action->setVisible(showFullFeatures);
0384     m_popup->addMenu(popup)->setVisible(showFullFeatures);
0385 }
0386 
0387 void Viewer::ViewerWidget::createZoomMenu()
0388 {
0389     QMenu *popup = new QMenu(m_popup);
0390     popup->setTitle(i18nc("@action:inmenu", "Zoom"));
0391 
0392     // PENDING(blackie) Only for image display?
0393     QAction *action = m_actions->addAction(QString::fromLatin1("viewer-zoom-in"), this, &ViewerWidget::zoomIn);
0394     action->setText(i18nc("@action:inmenu", "Zoom In"));
0395     action->setShortcut(Qt::Key_Plus);
0396     m_actions->setShortcutsConfigurable(action, false);
0397     popup->addAction(action);
0398 
0399     action = m_actions->addAction(QString::fromLatin1("viewer-zoom-out"), this, &ViewerWidget::zoomOut);
0400     action->setText(i18nc("@action:inmenu", "Zoom Out"));
0401     action->setShortcut(Qt::Key_Minus);
0402     m_actions->setShortcutsConfigurable(action, false);
0403     popup->addAction(action);
0404 
0405     action = m_actions->addAction(QString::fromLatin1("viewer-zoom-full"), this, &ViewerWidget::zoomFull);
0406     action->setText(i18nc("@action:inmenu", "Full View"));
0407     action->setShortcut(Qt::Key_Period);
0408     m_actions->setShortcutsConfigurable(action, false);
0409     popup->addAction(action);
0410 
0411     action = m_actions->addAction(QString::fromLatin1("viewer-zoom-pixel"), this, &ViewerWidget::zoomPixelForPixel);
0412     action->setText(i18nc("@action:inmenu", "Pixel for Pixel View"));
0413     action->setShortcut(Qt::Key_Equal);
0414     m_actions->setShortcutsConfigurable(action, false);
0415     popup->addAction(action);
0416 
0417     action = m_actions->addAction(QString::fromLatin1("viewer-toggle-fullscreen"), this, &ViewerWidget::toggleFullScreen);
0418     action->setText(i18nc("@action:inmenu", "Toggle Full Screen"));
0419     action->setShortcuts(QList<QKeySequence>() << Qt::Key_F11 << Qt::Key_Return);
0420     action->setVisible(m_type == UsageType::FullFeaturedViewer);
0421     popup->addAction(action);
0422 
0423     m_popup->addMenu(popup);
0424 }
0425 
0426 void Viewer::ViewerWidget::createSlideShowMenu()
0427 {
0428     QMenu *popup = new QMenu(m_popup);
0429     popup->setTitle(i18nc("@title:inmenu", "Slideshow"));
0430 
0431     m_startStopSlideShow = m_actions->addAction(QString::fromLatin1("viewer-start-stop-slideshow"), this, &ViewerWidget::slotStartStopSlideShow);
0432     m_startStopSlideShow->setText(i18nc("@action:inmenu", "Run Slideshow"));
0433     m_actions->setDefaultShortcut(m_startStopSlideShow, Qt::CTRL + Qt::Key_R);
0434     popup->addAction(m_startStopSlideShow);
0435 
0436     m_slideShowRunFaster = m_actions->addAction(QString::fromLatin1("viewer-run-faster"), this, &ViewerWidget::slotSlideShowFaster);
0437     m_slideShowRunFaster->setText(i18nc("@action:inmenu", "Run Faster"));
0438     m_actions->setDefaultShortcut(m_slideShowRunFaster, Qt::CTRL + Qt::Key_Plus); // if you change this, please update the info in Viewer::TransientDisplay
0439     popup->addAction(m_slideShowRunFaster);
0440 
0441     m_slideShowRunSlower = m_actions->addAction(QString::fromLatin1("viewer-run-slower"), this, &ViewerWidget::slotSlideShowSlower);
0442     m_slideShowRunSlower->setText(i18nc("@action:inmenu", "Run Slower"));
0443     m_actions->setDefaultShortcut(m_slideShowRunSlower, Qt::CTRL + Qt::Key_Minus); // if you change this, please update the info in Viewer::TransientDisplay
0444     popup->addAction(m_slideShowRunSlower);
0445 
0446     const bool showFullFeatures = m_type == UsageType::FullFeaturedViewer;
0447     // hide entries of hidden menus so that they can't be triggered via shortcut:
0448     for (auto &action : popup->actions())
0449         action->setVisible(showFullFeatures);
0450     m_popup->addMenu(popup)->setVisible(showFullFeatures);
0451 }
0452 
0453 void Viewer::ViewerWidget::load(const DB::FileNameList &list, int index)
0454 {
0455     m_list = list;
0456     m_imageDisplay->setImageList(list);
0457     m_current = index;
0458     load();
0459 }
0460 
0461 void Viewer::ViewerWidget::load()
0462 {
0463     const auto currentFile = currentFileName();
0464     if (currentFile.isNull())
0465         return;
0466 
0467     m_display->stop();
0468     const bool isReadable = QFileInfo(currentFile.absolute()).isReadable();
0469     const bool isVideo = isReadable && KPABase::isVideo(currentFile);
0470 
0471     m_crashSentinel.suspend();
0472     if (isReadable) {
0473         if (isVideo) {
0474             m_display = m_videoDisplay;
0475             m_crashSentinel.activate();
0476         } else
0477             m_display = m_imageDisplay;
0478     } else {
0479         m_display = m_textDisplay;
0480         m_textDisplay->setText(i18n("File not available"));
0481         updateInfoBox();
0482     }
0483 
0484     setCurrentWidget(m_display);
0485     m_infoBox->raise();
0486 
0487     updateContextMenuState(isVideo);
0488 
0489     Q_EMIT soughtTo(currentFile);
0490 
0491     bool ok = m_display->setImage(currentInfo(), m_forward);
0492     if (!ok) {
0493         close();
0494         return;
0495     }
0496 
0497     setCaptionWithDetail(QString());
0498 
0499     if (isVideo)
0500         updateCategoryConfig();
0501 
0502     if (m_isRunningSlideShow)
0503         m_slideShowTimer->start(m_slideShowPause);
0504 
0505     if (m_display == m_textDisplay)
0506         updateInfoBox();
0507 
0508     // Add all tagged areas
0509     setTaggedAreasFromImage();
0510 }
0511 
0512 void Viewer::ViewerWidget::setCaptionWithDetail(const QString &detail)
0513 {
0514     const auto currentFile = currentFileName();
0515     if (currentFile.isNull())
0516         return;
0517 
0518     setWindowTitle(i18nc("@title:window %1 is the filename, %2 its detail info", "%1 %2",
0519                          currentFile.absolute(),
0520                          detail));
0521 }
0522 
0523 void Viewer::ViewerWidget::slotRemoveDeletedImages(const DB::FileNameList &imageList)
0524 {
0525     const auto currentFile = currentFileName();
0526     for (const auto &filename : imageList) {
0527         m_list.removeAll(filename);
0528         m_removed.removeAll(filename);
0529     }
0530     if (m_list.isEmpty()) {
0531         close();
0532         return;
0533     }
0534 
0535     const int newIndex = m_list.indexOf(currentFile);
0536     if (newIndex == -1) {
0537         // find some sensible file to display in place of the deleted file
0538         if (m_current >= m_list.count()) {
0539             m_current = m_list.size();
0540             showPrev();
0541         } else {
0542             showNextN(0);
0543         }
0544     } else {
0545         m_current = newIndex;
0546         showNextN(0);
0547     }
0548 }
0549 
0550 void Viewer::ViewerWidget::contextMenuEvent(QContextMenuEvent *e)
0551 {
0552     if (m_display == m_videoDisplay) {
0553         if (m_videoDisplay->isPaused())
0554             m_playPause->setText(i18nc("@action:inmenu Start video playback", "Play"));
0555         else
0556             m_playPause->setText(i18nc("@action:inmenu Pause video playback", "Pause"));
0557 
0558         m_stop->setEnabled(m_videoDisplay->isPlaying());
0559     }
0560 
0561     m_popup->exec(e->globalPos());
0562     e->setAccepted(true);
0563 }
0564 
0565 void Viewer::ViewerWidget::showNextN(int n)
0566 {
0567     filterNone();
0568     if (m_display == m_videoDisplay) {
0569         m_videoPlayerStoppedManually = true;
0570         m_videoDisplay->stop();
0571     }
0572 
0573     if (m_current + n < (int)m_list.count()) {
0574         m_current += n;
0575         if (m_current >= (int)m_list.count())
0576             m_current = (int)m_list.count() - 1;
0577         m_forward = true;
0578         load();
0579     }
0580 }
0581 
0582 void Viewer::ViewerWidget::showNext()
0583 {
0584     showNextN(1);
0585 }
0586 
0587 void Viewer::ViewerWidget::removeCurrent()
0588 {
0589     removeOrDeleteCurrent(OnlyRemoveFromViewer);
0590 }
0591 
0592 void Viewer::ViewerWidget::deleteCurrent()
0593 {
0594     removeOrDeleteCurrent(RemoveImageFromDatabase);
0595 }
0596 
0597 void Viewer::ViewerWidget::removeOrDeleteCurrent(RemoveAction action)
0598 {
0599     const DB::FileName fileName = currentFileName();
0600     if (fileName.isNull())
0601         return;
0602 
0603     if (action == RemoveImageFromDatabase)
0604         m_removed.append(fileName);
0605     m_list.removeAll(fileName);
0606     if (m_list.isEmpty())
0607         close();
0608     if (m_current == m_list.count())
0609         showPrev();
0610     else
0611         showNextN(0);
0612 }
0613 
0614 void Viewer::ViewerWidget::setTagMode(TagMode tagMode)
0615 {
0616     m_tagMode = tagMode;
0617     m_addTagAction->setEnabled(tagMode == TagMode::Annotating);
0618     m_copyAction->setEnabled(tagMode == TagMode::Annotating);
0619     m_addDescriptionAction->setEnabled(tagMode == TagMode::Annotating);
0620 
0621     const auto tagModeText = [&] {
0622         switch (tagMode) {
0623         case TagMode::Locked:
0624             return i18n("locked");
0625         case TagMode::Annotating:
0626             return i18n("annotating");
0627         case TagMode::Tokenizing:
0628             return i18n("tokenizing");
0629         }
0630         return QString();
0631     }();
0632 
0633     m_transientDisplay->display(i18n("Change display mode to %1", tagModeText));
0634 }
0635 
0636 void Viewer::ViewerWidget::updateContextMenuState(bool isVideo)
0637 {
0638     const auto currentFile = currentFileName();
0639     if (currentFile.isNull())
0640         return;
0641 
0642     m_categoryImagePopup->setEnabled(!isVideo);
0643 
0644     m_showExifViewer->setEnabled(!isVideo);
0645     if (m_exifViewer)
0646         m_exifViewer->setImage(currentFile);
0647 
0648     for (QAction *videoAction : qAsConst(m_videoActions)) {
0649         videoAction->setVisible(isVideo);
0650     }
0651 
0652     // PENDING(blackie) This needs to be improved, so that it shows the actions only if there are that many images to jump.
0653     for (QList<QAction *>::const_iterator it = m_forwardActions.constBegin(); it != m_forwardActions.constEnd(); ++it)
0654         (*it)->setEnabled(m_current + 1 < (int)m_list.count());
0655     for (QList<QAction *>::const_iterator it = m_backwardActions.constBegin(); it != m_backwardActions.constEnd(); ++it)
0656         (*it)->setEnabled(m_current > 0);
0657 
0658     m_setStackHead->setEnabled(currentInfo()->isStacked());
0659     m_filterMenu->setEnabled(!isVideo);
0660 
0661     bool on = (m_list.count() > 1);
0662     m_startStopSlideShow->setEnabled(on);
0663     m_slideShowRunFaster->setEnabled(on);
0664     m_slideShowRunSlower->setEnabled(on);
0665 }
0666 
0667 namespace Viewer
0668 {
0669 class TemporarilyDisableCursorHandling
0670 {
0671 public:
0672     TemporarilyDisableCursorHandling(Viewer::ViewerWidget *viewer)
0673         : m_viewer(viewer)
0674     {
0675         viewer->m_cursorHandlerForImageDisplay->disableCursorHiding();
0676         viewer->m_cursorHandlerForVideoDisplay->disableCursorHiding();
0677     }
0678     ~TemporarilyDisableCursorHandling()
0679     {
0680         m_viewer->m_cursorHandlerForImageDisplay->enableCursorHiding();
0681         m_viewer->m_cursorHandlerForVideoDisplay->enableCursorHiding();
0682     }
0683 
0684 private:
0685     Viewer::ViewerWidget *m_viewer;
0686 };
0687 }
0688 
0689 void Viewer::ViewerWidget::showNext10()
0690 {
0691     showNextN(10);
0692 }
0693 
0694 void Viewer::ViewerWidget::showNext100()
0695 {
0696     showNextN(100);
0697 }
0698 
0699 void Viewer::ViewerWidget::showNext1000()
0700 {
0701     showNextN(1000);
0702 }
0703 
0704 void Viewer::ViewerWidget::showPrevN(int n)
0705 {
0706     if (m_display == m_videoDisplay)
0707         m_videoDisplay->stop();
0708 
0709     if (m_current > 0) {
0710         m_current -= n;
0711         if (m_current < 0)
0712             m_current = 0;
0713         m_forward = false;
0714         load();
0715     }
0716 }
0717 
0718 void Viewer::ViewerWidget::showPrev()
0719 {
0720     showPrevN(1);
0721 }
0722 
0723 void Viewer::ViewerWidget::showPrev10()
0724 {
0725     showPrevN(10);
0726 }
0727 
0728 void Viewer::ViewerWidget::showPrev100()
0729 {
0730     showPrevN(100);
0731 }
0732 
0733 void Viewer::ViewerWidget::showPrev1000()
0734 {
0735     showPrevN(1000);
0736 }
0737 
0738 void Viewer::ViewerWidget::rotate(int angle)
0739 {
0740     const auto current = currentInfo();
0741     if (current->isNull())
0742         return;
0743 
0744     current->rotate(angle);
0745     m_display->rotate(current);
0746     invalidateThumbnail();
0747     MainWindow::DirtyIndicator::markDirty();
0748     Q_EMIT imageRotated(currentFileName());
0749 }
0750 
0751 void Viewer::ViewerWidget::showFirst()
0752 {
0753     showPrevN(m_list.count());
0754 }
0755 
0756 void Viewer::ViewerWidget::showLast()
0757 {
0758     showNextN(m_list.count());
0759 }
0760 
0761 void Viewer::ViewerWidget::closeEvent(QCloseEvent *event)
0762 {
0763     if (!m_removed.isEmpty()) {
0764         MainWindow::DeleteDialog dialog(this);
0765         dialog.exec(m_removed);
0766     }
0767 
0768     m_slideShowTimer->stop();
0769     m_isRunningSlideShow = false;
0770     // give the video display time to do cleanup as long as the window handle is still valid:
0771     m_videoDisplay->stop();
0772     event->accept();
0773 }
0774 
0775 DB::ImageInfoPtr Viewer::ViewerWidget::currentInfo() const
0776 {
0777     const auto currentFile = currentFileName();
0778     if (currentFile.isNull())
0779         return {};
0780 
0781     return DB::ImageDB::instance()->info(currentFile);
0782 }
0783 
0784 void Viewer::ViewerWidget::updatePalette()
0785 {
0786     QPalette pal = palette();
0787     // if the scheme was set at startup from the scheme path (and not afterwards through KColorSchemeManager),
0788     // then KColorScheme would use the standard system scheme if we don't explicitly give a config:
0789     const auto schemeCfg = KSharedConfig::openConfig(Settings::SettingsData::instance()->colorScheme());
0790     KColorScheme::adjustBackground(pal, KColorScheme::NormalBackground, QPalette::Base, KColorScheme::Complementary, schemeCfg);
0791     KColorScheme::adjustForeground(pal, KColorScheme::NormalText, QPalette::Text, KColorScheme::Complementary, schemeCfg);
0792     setPalette(pal);
0793 }
0794 
0795 void Viewer::ViewerWidget::infoBoxMove()
0796 {
0797     QPoint p = mapFromGlobal(QCursor::pos());
0798     Settings::Position oldPos = Settings::SettingsData::instance()->infoBoxPosition();
0799     Settings::Position pos = oldPos;
0800     int x = m_display->mapFromParent(p).x();
0801     int y = m_display->mapFromParent(p).y();
0802     int w = m_display->width();
0803     int h = m_display->height();
0804 
0805     if (x < w / 3) {
0806         if (y < h / 3)
0807             pos = Settings::TopLeft;
0808         else if (y > h * 2 / 3)
0809             pos = Settings::BottomLeft;
0810         else
0811             pos = Settings::Left;
0812     } else if (x > w * 2 / 3) {
0813         if (y < h / 3)
0814             pos = Settings::TopRight;
0815         else if (y > h * 2 / 3)
0816             pos = Settings::BottomRight;
0817         else
0818             pos = Settings::Right;
0819     } else {
0820         if (y < h / 3)
0821             pos = Settings::Top;
0822         else if (y > h * 2 / 3)
0823             pos = Settings::Bottom;
0824     }
0825     if (pos != oldPos) {
0826         Settings::SettingsData::instance()->setInfoBoxPosition(pos);
0827         updateInfoBox();
0828     }
0829 }
0830 
0831 void Viewer::ViewerWidget::moveInfoBox()
0832 {
0833     m_infoBox->setSize();
0834     Settings::Position pos = Settings::SettingsData::instance()->infoBoxPosition();
0835 
0836     int lx = m_display->pos().x();
0837     int ly = m_display->pos().y();
0838     int lw = m_display->width();
0839     int lh = m_display->height();
0840 
0841     int bw = m_infoBox->width();
0842     int bh = m_infoBox->height();
0843 
0844     int bx, by;
0845     // x-coordinate
0846     if (pos == Settings::TopRight || pos == Settings::BottomRight || pos == Settings::Right)
0847         bx = lx + lw - 5 - bw;
0848     else if (pos == Settings::TopLeft || pos == Settings::BottomLeft || pos == Settings::Left)
0849         bx = lx + 5;
0850     else
0851         bx = lx + lw / 2 - bw / 2;
0852 
0853     // Y-coordinate
0854     if (pos == Settings::TopLeft || pos == Settings::TopRight || pos == Settings::Top)
0855         by = ly + 5;
0856     else if (pos == Settings::BottomLeft || pos == Settings::BottomRight || pos == Settings::Bottom)
0857         by = ly + lh - 5 - bh;
0858     else
0859         by = ly + lh / 2 - bh / 2;
0860 
0861     m_infoBox->move(bx, by);
0862 }
0863 
0864 void Viewer::ViewerWidget::resizeEvent(QResizeEvent *e)
0865 {
0866     moveInfoBox();
0867     QWidget::resizeEvent(e);
0868 }
0869 
0870 void Viewer::ViewerWidget::updateInfoBox()
0871 {
0872     if (currentInfo()) {
0873         QMap<int, QPair<QString, QString>> map;
0874         const QString text = Utilities::createInfoText(currentInfo(), &map);
0875 
0876         if (Settings::SettingsData::instance()->showInfoBox() && !text.isNull() && (m_type == UsageType::FullFeaturedViewer)) {
0877             m_infoBox->setInfo(text, map);
0878             m_infoBox->show();
0879         } else
0880             m_infoBox->hide();
0881 
0882         moveInfoBox();
0883     }
0884     m_infoBox->setSize();
0885 }
0886 
0887 Viewer::ViewerWidget::~ViewerWidget()
0888 {
0889     inhibitScreenSaver(false);
0890 
0891     if (s_latest == this)
0892         s_latest = nullptr;
0893 }
0894 
0895 void Viewer::ViewerWidget::toggleFullScreen()
0896 {
0897     setShowFullScreen(!m_showingFullScreen);
0898 }
0899 
0900 void Viewer::ViewerWidget::slotStartStopSlideShow()
0901 {
0902     bool wasRunningSlideShow = m_isRunningSlideShow;
0903     m_isRunningSlideShow = !m_isRunningSlideShow && m_list.count() != 1;
0904 
0905     if (wasRunningSlideShow) {
0906         m_startStopSlideShow->setText(i18nc("@action:inmenu", "Run Slideshow"));
0907         m_slideShowTimer->stop();
0908         if (m_list.count() != 1)
0909             m_transientDisplay->display(i18nc("OSD for slideshow", "Ending Slideshow"));
0910         inhibitScreenSaver(false);
0911     } else {
0912         m_startStopSlideShow->setText(i18nc("@action:inmenu", "Stop Slideshow"));
0913         if (currentInfo()->mediaType() != DB::Video)
0914             m_slideShowTimer->start(m_slideShowPause);
0915         const auto faster = m_actions->action(QString::fromLatin1("viewer-run-faster"))->shortcut().toString();
0916         const auto slower = m_actions->action(QString::fromLatin1("viewer-run-slower"))->shortcut().toString();
0917         m_transientDisplay->display(i18nc("OSD for slideshow", "Starting Slideshow<br/>%1 makes the slideshow faster<br/>%2 makes the slideshow slower",
0918                                           faster, slower),
0919                                     1500ms);
0920         inhibitScreenSaver(true);
0921     }
0922 }
0923 
0924 void Viewer::ViewerWidget::slotSlideShowNextFromTimer()
0925 {
0926     // Load the next images.
0927     QElapsedTimer timer;
0928     timer.start();
0929     if (m_display == m_imageDisplay)
0930         slotSlideShowNext();
0931 
0932     // ensure that there is a few milliseconds pause, so that an end slideshow keypress
0933     // can get through immediately, we don't want it to queue up behind a bunch of timer events,
0934     // which loaded a number of new images before the slideshow stops
0935     int ms = qMax(Q_INT64_C(200), m_slideShowPause - timer.elapsed());
0936     m_slideShowTimer->start(ms);
0937 }
0938 
0939 void Viewer::ViewerWidget::slotSlideShowNext()
0940 {
0941     m_forward = true;
0942     if (m_current + 1 < (int)m_list.count())
0943         m_current++;
0944     else
0945         m_current = 0;
0946 
0947     load();
0948 }
0949 
0950 void Viewer::ViewerWidget::slotSlideShowFaster()
0951 {
0952     changeSlideShowInterval(-500);
0953 }
0954 
0955 void Viewer::ViewerWidget::slotSlideShowSlower()
0956 {
0957     changeSlideShowInterval(+500);
0958 }
0959 
0960 void Viewer::ViewerWidget::changeSlideShowInterval(int delta)
0961 {
0962     if (m_list.count() == 1)
0963         return;
0964 
0965     m_slideShowPause += delta;
0966     m_slideShowPause = qMax(m_slideShowPause, 500);
0967     m_transientDisplay->display(i18nc("OSD for slideshow, num of seconds per image", "%1&nbsp;s", m_slideShowPause / 1000.0));
0968     if (m_slideShowTimer->isActive())
0969         m_slideShowTimer->start(m_slideShowPause);
0970 }
0971 
0972 void Viewer::ViewerWidget::editImage()
0973 {
0974     // don't block this method because the ViewerWidget may already be deleted once configureImages returns
0975     QTimer::singleShot(0, [&]() {
0976         DB::ImageInfoList list;
0977         list.append(currentInfo());
0978         MainWindow::Window::configureImages(list, true);
0979     });
0980 }
0981 
0982 void Viewer::ViewerWidget::filterNone()
0983 {
0984     if (m_display == m_imageDisplay) {
0985         m_imageDisplay->filterNone();
0986         m_filterMono->setChecked(false);
0987         m_filterBW->setChecked(false);
0988         m_filterContrastStretch->setChecked(false);
0989         m_filterHistogramEqualization->setChecked(false);
0990     }
0991 }
0992 
0993 void Viewer::ViewerWidget::filterSelected()
0994 {
0995     // The filters that drop bit depth below 32 should be the last ones
0996     // so that filters requiring more bit depth are processed first
0997     if (m_display == m_imageDisplay) {
0998         m_imageDisplay->filterNone();
0999         if (m_filterBW->isChecked())
1000             m_imageDisplay->filterBW();
1001         if (m_filterContrastStretch->isChecked())
1002             m_imageDisplay->filterContrastStretch();
1003         if (m_filterHistogramEqualization->isChecked())
1004             m_imageDisplay->filterHistogramEqualization();
1005         if (m_filterMono->isChecked())
1006             m_imageDisplay->filterMono();
1007     }
1008 }
1009 
1010 void Viewer::ViewerWidget::filterBW()
1011 {
1012     if (m_display == m_imageDisplay) {
1013         if (m_filterBW->isChecked())
1014             m_filterBW->setChecked(m_imageDisplay->filterBW());
1015         else
1016             filterSelected();
1017     }
1018 }
1019 
1020 void Viewer::ViewerWidget::filterContrastStretch()
1021 {
1022     if (m_display == m_imageDisplay) {
1023         if (m_filterContrastStretch->isChecked())
1024             m_filterContrastStretch->setChecked(m_imageDisplay->filterContrastStretch());
1025         else
1026             filterSelected();
1027     }
1028 }
1029 
1030 void Viewer::ViewerWidget::filterHistogramEqualization()
1031 {
1032     if (m_display == m_imageDisplay) {
1033         if (m_filterHistogramEqualization->isChecked())
1034             m_filterHistogramEqualization->setChecked(m_imageDisplay->filterHistogramEqualization());
1035         else
1036             filterSelected();
1037     }
1038 }
1039 
1040 void Viewer::ViewerWidget::filterMono()
1041 {
1042     if (m_display == m_imageDisplay) {
1043         if (m_filterMono->isChecked())
1044             m_filterMono->setChecked(m_imageDisplay->filterMono());
1045         else
1046             filterSelected();
1047     }
1048 }
1049 
1050 void Viewer::ViewerWidget::slotSetStackHead()
1051 {
1052     const auto currentFile = currentFileName();
1053     if (currentFile.isNull())
1054         return;
1055 
1056     MainWindow::Window::theMainWindow()->setStackHead(currentFile);
1057 }
1058 
1059 bool Viewer::ViewerWidget::showingFullScreen() const
1060 {
1061     return m_showingFullScreen;
1062 }
1063 
1064 void Viewer::ViewerWidget::setShowFullScreen(bool on)
1065 {
1066     if (on) {
1067         setWindowState(windowState() | Qt::WindowFullScreen); // set
1068         moveInfoBox();
1069     } else {
1070         // We need to size the image when going out of full screen, in case we started directly in full screen
1071         //
1072         setWindowState(windowState() & ~Qt::WindowFullScreen); // reset
1073         resize(Settings::SettingsData::instance()->viewerSize());
1074     }
1075     m_showingFullScreen = on;
1076 }
1077 
1078 void Viewer::ViewerWidget::updateCategoryConfig()
1079 {
1080     if (!CategoryImageConfig::instance()->isVisible())
1081         return;
1082 
1083     CategoryImageConfig::instance()->setCurrentImage(m_imageDisplay->currentViewAsThumbnail(), currentInfo());
1084 }
1085 
1086 void Viewer::ViewerWidget::populateExternalPopup()
1087 {
1088     m_externalPopup->populate(currentInfo(), m_list);
1089 }
1090 
1091 void Viewer::ViewerWidget::populateCategoryImagePopup()
1092 {
1093     const auto currentFile = currentFileName();
1094     if (currentFile.isNull())
1095         return;
1096 
1097     m_categoryImagePopup->populate(m_imageDisplay->currentViewAsThumbnail(), currentFile);
1098 }
1099 
1100 void Viewer::ViewerWidget::show(bool slideShow)
1101 {
1102     QSize size;
1103     bool fullScreen;
1104     if (slideShow) {
1105         fullScreen = Settings::SettingsData::instance()->launchSlideShowFullScreen();
1106         size = Settings::SettingsData::instance()->slideShowSize();
1107     } else {
1108         fullScreen = Settings::SettingsData::instance()->launchViewerFullScreen();
1109         size = Settings::SettingsData::instance()->viewerSize();
1110     }
1111 
1112     if (fullScreen)
1113         setShowFullScreen(true);
1114     else
1115         resize(size);
1116 
1117     QWidget::show();
1118     if (slideShow != m_isRunningSlideShow) {
1119         // The info dialog will show up at the wrong place if we call this function directly
1120         // don't ask me why -  4 Sep. 2004 15:13 -- Jesper K. Pedersen
1121         QTimer::singleShot(0, this, &ViewerWidget::slotStartStopSlideShow);
1122     }
1123 }
1124 
1125 KActionCollection *Viewer::ViewerWidget::actions()
1126 {
1127     return m_actions;
1128 }
1129 
1130 void Viewer::ViewerWidget::keyPressEvent(QKeyEvent *event)
1131 {
1132     const bool readOnly = m_type != UsageType::FullFeaturedViewer;
1133     if (readOnly) {
1134         event->ignore();
1135         return;
1136     }
1137 
1138     bool dirty = false;
1139     // Rating of the image
1140     if (event->modifiers() == 0 && event->key() >= Qt::Key_0 && event->key() <= Qt::Key_5) {
1141         const auto rating = event->key() - Qt::Key_0;
1142         currentInfo()->setRating(rating * 2);
1143         dirty = true;
1144     } else if (m_tagMode == TagMode::Locked) {
1145         return;
1146     } else if (m_tagMode == TagMode::Tokenizing) {
1147         if (event->key() < Qt::Key_A || event->key() > Qt::Key_Z)
1148             return;
1149 
1150         auto category = DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory)->name();
1151         toggleTag(category, event->text());
1152     } else {
1153         TemporarilyDisableCursorHandling dummy(this);
1154         dirty = m_annotationHandler->handle(event);
1155     }
1156 
1157     updateInfoBox();
1158     if (dirty)
1159         MainWindow::DirtyIndicator::markDirty();
1160 }
1161 
1162 void Viewer::ViewerWidget::videoStopped()
1163 {
1164     if (!m_videoPlayerStoppedManually && m_isRunningSlideShow)
1165         slotSlideShowNext();
1166     m_videoPlayerStoppedManually = false;
1167 }
1168 
1169 void Viewer::ViewerWidget::wheelEvent(QWheelEvent *event)
1170 {
1171     const auto angleDelta = event->angleDelta();
1172     const bool isHorizontal = (qAbs(angleDelta.x()) > qAbs(angleDelta.y()));
1173     if ((!isHorizontal && angleDelta.y() < 0) || (isHorizontal && angleDelta.x() < 0)) {
1174         showNext();
1175     } else {
1176         showPrev();
1177     }
1178 }
1179 
1180 void Viewer::ViewerWidget::showExifViewer()
1181 {
1182     const auto currentFile = currentFileName();
1183     if (currentFile.isNull())
1184         return;
1185 
1186     m_exifViewer = new Exif::InfoDialog(currentFile, this);
1187     m_exifViewer->show();
1188 }
1189 
1190 void Viewer::ViewerWidget::zoomIn()
1191 {
1192     m_display->zoomIn();
1193 }
1194 
1195 void Viewer::ViewerWidget::zoomOut()
1196 {
1197     m_display->zoomOut();
1198 }
1199 
1200 void Viewer::ViewerWidget::zoomFull()
1201 {
1202     m_display->zoomFull();
1203 }
1204 
1205 void Viewer::ViewerWidget::zoomPixelForPixel()
1206 {
1207     m_display->zoomPixelForPixel();
1208 }
1209 
1210 void Viewer::ViewerWidget::makeThumbnailImage()
1211 {
1212     VideoShooter::go(currentInfo(), this);
1213 }
1214 
1215 void Viewer::ViewerWidget::addTag()
1216 {
1217     TemporarilyDisableCursorHandling dummy(this);
1218     const bool dirty = m_annotationHandler->askForTagAndInsert();
1219     if (dirty)
1220         MainWindow::DirtyIndicator::markDirty();
1221 }
1222 
1223 void Viewer::ViewerWidget::editDescription()
1224 {
1225     TemporarilyDisableCursorHandling dummy(this);
1226     const auto description = currentInfo()->description();
1227     bool ok;
1228     auto newDescription = QInputDialog::getMultiLineText(this, i18nc("@title", "Edit Image Description"), i18nc("@label:textbox", "Image Description"), description, &ok);
1229     if (ok && description != newDescription) {
1230         currentInfo()->setDescription(newDescription);
1231         MainWindow::DirtyIndicator::markDirty();
1232     }
1233 }
1234 
1235 void Viewer::ViewerWidget::showAnnotationHelp()
1236 {
1237     QDesktopServices::openUrl(QUrl(QLatin1String("help:/kphotoalbum/chp-viewer.html#annotating-from-the-viewer")));
1238 }
1239 
1240 void Viewer::ViewerWidget::createVideoMenu()
1241 {
1242     QMenu *menu = new QMenu(m_popup);
1243     menu->setTitle(i18nc("@title:inmenu", "Seek"));
1244 
1245     m_videoActions.append(m_popup->addMenu(menu));
1246 
1247     int count = 0;
1248     auto add = [&](const QString &title, const char *name, int value, const QKeySequence &key) {
1249         if (count++ == 5) {
1250             QAction *sep = new QAction(menu);
1251             sep->setSeparator(true);
1252             menu->addAction(sep);
1253         }
1254 
1255         QAction *seek = m_actions->addAction(QString::fromLatin1(name));
1256         seek->setText(title);
1257         seek->setShortcut(key);
1258         m_actions->setShortcutsConfigurable(seek, false);
1259         connect(seek, &QAction::triggered, m_videoDisplay, [this, value] {
1260             m_videoDisplay->relativeSeek(value);
1261         });
1262         menu->addAction(seek);
1263     };
1264 
1265     add(i18nc("@action:inmenu", "10 minutes backward"), "seek-10-minute", -600000, QKeySequence(QString::fromLatin1("Ctrl+Left")));
1266     add(i18nc("@action:inmenu", "1 minute backward"), "seek-1-minute", -60000, QKeySequence(QString::fromLatin1("Shift+Left")));
1267     add(i18nc("@action:inmenu", "10 seconds backward"), "seek-10-second", -10000, QKeySequence(QString::fromLatin1("Left")));
1268     add(i18nc("@action:inmenu", "1 seconds backward"), "seek-1-second", -1000, QKeySequence(QString::fromLatin1("Up")));
1269     add(i18nc("@action:inmenu", "100 milliseconds backward"), "seek-100-millisecond", -100, QKeySequence(QString::fromLatin1("Shift+Up")));
1270     add(i18nc("@action:inmenu", "100 milliseconds forward"), "seek+100-millisecond", 100, QKeySequence(QString::fromLatin1("Shift+Down")));
1271     add(i18nc("@action:inmenu", "1 seconds forward"), "seek+1-second", 1000, QKeySequence(QString::fromLatin1("Down")));
1272     add(i18nc("@action:inmenu", "10 seconds forward"), "seek+10-second", 10000, QKeySequence(QString::fromLatin1("Right")));
1273     add(i18nc("@action:inmenu", "1 minute forward"), "seek+1-minute", 60000, QKeySequence(QString::fromLatin1("Shift+Right")));
1274     add(i18nc("@action:inmenu", "10 minutes forward"), "seek+10-minute", 600000, QKeySequence(QString::fromLatin1("Ctrl+Right")));
1275 
1276     QAction *sep = new QAction(m_popup);
1277     sep->setSeparator(true);
1278     m_popup->addAction(sep);
1279     m_videoActions.append(sep);
1280 
1281     m_stop = m_actions->addAction(QString::fromLatin1("viewer-video-stop"), m_videoDisplay, &VideoDisplay::stop);
1282     m_stop->setText(i18nc("@action:inmenu Stop video playback", "Stop"));
1283     m_popup->addAction(m_stop);
1284     m_videoActions.append(m_stop);
1285 
1286     m_playPause = m_actions->addAction(QString::fromLatin1("viewer-video-pause"), m_videoDisplay, &VideoDisplay::playPause);
1287     // text set in contextMenuEvent()
1288     m_playPause->setShortcut(Qt::Key_P);
1289     m_actions->setShortcutsConfigurable(m_playPause, false);
1290     m_popup->addAction(m_playPause);
1291     m_videoActions.append(m_playPause);
1292 
1293     m_makeThumbnailImage = m_actions->addAction(QString::fromLatin1("make-thumbnail-image"), this, &ViewerWidget::makeThumbnailImage);
1294     m_makeThumbnailImage->setText(i18nc("@action:inmenu", "Use current frame in thumbnail view"));
1295     m_makeThumbnailImage->setVisible(m_type == UsageType::FullFeaturedViewer);
1296     m_popup->addAction(m_makeThumbnailImage);
1297     m_videoActions.append(m_makeThumbnailImage);
1298 
1299     QAction *restart = m_actions->addAction(QString::fromLatin1("viewer-video-restart"), m_videoDisplay, &VideoDisplay::restart);
1300     m_actions->setDefaultShortcut(restart, Qt::Key_Home);
1301     restart->setText(i18nc("@action:inmenu Restart video playback.", "Restart"));
1302     m_popup->addAction(restart);
1303     m_videoActions.append(restart);
1304 }
1305 
1306 void Viewer::ViewerWidget::createCategoryImageMenu()
1307 {
1308     m_categoryImagePopup = new MainWindow::CategoryImagePopup(m_popup);
1309     const bool showFullFeatures = m_type == UsageType::FullFeaturedViewer;
1310     m_popup->addMenu(m_categoryImagePopup)->setVisible(showFullFeatures);
1311     connect(m_categoryImagePopup, &MainWindow::CategoryImagePopup::aboutToShow, this, &ViewerWidget::populateCategoryImagePopup);
1312 }
1313 
1314 void Viewer::ViewerWidget::createFilterMenu()
1315 {
1316     m_filterMenu = new QMenu(m_popup);
1317     m_filterMenu->setTitle(i18nc("@title:inmenu", "Filters"));
1318 
1319     m_filterNone = m_actions->addAction(QString::fromLatin1("filter-empty"), this, &ViewerWidget::filterNone);
1320     m_filterNone->setText(i18nc("@action:inmenu", "Remove All Filters"));
1321     m_filterMenu->addAction(m_filterNone);
1322 
1323     m_filterBW = m_actions->addAction(QString::fromLatin1("filter-bw"), this, &ViewerWidget::filterBW);
1324     m_filterBW->setText(i18nc("@action:inmenu", "Apply Grayscale Filter"));
1325     m_filterBW->setCheckable(true);
1326     m_filterMenu->addAction(m_filterBW);
1327 
1328     m_filterContrastStretch = m_actions->addAction(QString::fromLatin1("filter-cs"), this, &ViewerWidget::filterContrastStretch);
1329     m_filterContrastStretch->setText(i18nc("@action:inmenu", "Apply Contrast Stretching Filter"));
1330     m_filterContrastStretch->setCheckable(true);
1331     m_filterMenu->addAction(m_filterContrastStretch);
1332 
1333     m_filterHistogramEqualization = m_actions->addAction(QString::fromLatin1("filter-he"), this, &ViewerWidget::filterHistogramEqualization);
1334     m_filterHistogramEqualization->setText(i18nc("@action:inmenu", "Apply Histogram Equalization Filter"));
1335     m_filterHistogramEqualization->setCheckable(true);
1336     m_filterMenu->addAction(m_filterHistogramEqualization);
1337 
1338     m_filterMono = m_actions->addAction(QString::fromLatin1("filter-mono"), this, &ViewerWidget::filterMono);
1339     m_filterMono->setText(i18nc("@action:inmenu", "Apply Monochrome Filter"));
1340     m_filterMono->setCheckable(true);
1341     m_filterMenu->addAction(m_filterMono);
1342 
1343     const bool showFullFeatures = m_type == UsageType::FullFeaturedViewer;
1344     // hide entries of hidden menus so that they can't be triggered via shortcut:
1345     for (auto &action : m_filterMenu->actions())
1346         action->setVisible(showFullFeatures);
1347     m_popup->addMenu(m_filterMenu)->setVisible(showFullFeatures);
1348 }
1349 
1350 void Viewer::ViewerWidget::test()
1351 {
1352 #ifdef TESTING
1353     QTimeLine *timeline = new QTimeLine;
1354     timeline->setStartFrame(_infoBox->y());
1355     timeline->setEndFrame(height());
1356     connect(timeline, &QTimeLine::frameChanged, this, &ViewerWidget::moveInfoBox);
1357     timeline->start();
1358 #endif // TESTING
1359 }
1360 
1361 void Viewer::ViewerWidget::moveInfoBox(int y)
1362 {
1363     m_infoBox->move(m_infoBox->x(), y);
1364 }
1365 
1366 namespace Viewer
1367 {
1368 static VideoDisplay *instantiateVideoDisplay(QWidget *parent, KPABase::CrashSentinel &sentinel)
1369 {
1370     auto backend = Settings::SettingsData::instance()->videoBackend();
1371     if (backend == Settings::VideoBackend::NotConfigured) {
1372         // just select a backend for the user if they didn't choose one
1373         backend = Settings::preferredVideoBackend(backend);
1374     }
1375     if (sentinel.hasCrashInfo()) {
1376         // KPA crashed during video playback - time to select a different backend based on crash data:
1377         const auto badBackends = sentinel.crashHistory();
1378         const auto backendEnum = QMetaEnum::fromType<Settings::VideoBackend>();
1379         Settings::VideoBackends exclusions;
1380         for (const auto &badBackend : badBackends) {
1381             bool ok = false;
1382             const auto be = static_cast<Settings::VideoBackend>(backendEnum.keyToValue(badBackend.constData(), &ok));
1383             if (ok) {
1384                 exclusions |= be;
1385             } else {
1386                 qCWarning(ViewerLog) << "Could not parse crash data:" << badBackend << "is an unknown video backend value! Ignoring...";
1387             }
1388         }
1389         auto preferredBackend = Settings::preferredVideoBackend(backend, exclusions);
1390         if (preferredBackend != backend) {
1391             qCWarning(ViewerLog) << "A crash was registered during usage of the " << backend << "video backend - preferred new backend:" << preferredBackend;
1392             const bool foundViableBackend = (preferredBackend != Settings::VideoBackend::NotConfigured);
1393             if (foundViableBackend) {
1394                 const auto message = i18n(
1395                     "<p>It seems that KPhotoAlbum previously crashed during video playback. "
1396                     "On some platforms, this is a common problem with some video players.</p>"
1397                     "<p>Press <i>Continue</i> to let KPhotoAlbum try a different backend...</p>");
1398                 const auto messageDetails = i18n(
1399                     "<p>Video backend that was interrupted: <tt>%1</tt></p>"
1400                     "<p>Video backend that will be used instead: <tt>%2</tt></p>",
1401                     Settings::localizedEnumName(backend), Settings::localizedEnumName(preferredBackend));
1402                 const auto choice = KMessageBox::warningContinueCancelDetailed(parent, message, QString(), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QString(), KMessageBox::Notify, messageDetails);
1403                 if (choice == KMessageBox::Continue) {
1404                     Settings::SettingsData::instance()->setVideoBackend(preferredBackend);
1405                     backend = preferredBackend;
1406                 }
1407             } else {
1408                 // if no viable backend was found, that means that all available backends crashed at some point
1409                 // i.e. there's no point in bugging the user again - just disable the crash detection completely
1410                 sentinel.disablePermanently();
1411                 const auto message = i18n(
1412                     "<p>KPhotoAlbum has tried out all available video backends, but every one crashed at some point.</p>"
1413                     "<p>Crash detection is now turned off.</p>");
1414                 KMessageBox::error(parent, message);
1415             }
1416         }
1417     }
1418     bool showSelectorDialog = backend == Settings::VideoBackend::NotConfigured;
1419 
1420     if (showSelectorDialog) {
1421         // no viable backend found yet -> we need the user to choose
1422         Settings::VideoPlayerSelectorDialog dialog;
1423         dialog.exec();
1424         Settings::SettingsData::instance()->setVideoBackend(dialog.backend());
1425         backend = Settings::SettingsData::instance()->videoBackend();
1426         sentinel.clearCrashHistory();
1427     }
1428 
1429     switch (backend) {
1430     case Settings::VideoBackend::VLC:
1431 #if LIBVLC_FOUND
1432         return new VLCDisplay(parent);
1433 #else
1434         qCWarning(ViewerLog) << "Video backend VLC not available. Selecting first available backend...";
1435 #endif
1436         break;
1437     case Settings::VideoBackend::QtAV:
1438 #if QtAV_FOUND
1439         return new QtAVDisplay(parent);
1440 #else
1441         qCWarning(ViewerLog) << "Video backend QtAV not available. Selecting first available backend...";
1442 #endif
1443         break;
1444     case Settings::VideoBackend::Phonon:
1445 #if Phonon4Qt5_FOUND
1446         return new PhononDisplay(parent);
1447 #else
1448         qCWarning(ViewerLog) << "Video backend Phonon not available. Selecting first available backend...";
1449 #endif
1450         break;
1451     case Settings::VideoBackend::NotConfigured:
1452         qCCritical(ViewerLog) << "No viable video backend!";
1453     }
1454 
1455     static_assert(LIBVLC_FOUND || QtAV_FOUND || Phonon4Qt5_FOUND, "A video backend must be provided. The build system should bail out if none is available.");
1456     Q_UNREACHABLE();
1457     return nullptr;
1458 }
1459 }
1460 
1461 void Viewer::ViewerWidget::createVideoViewer()
1462 {
1463 
1464     m_videoDisplay = instantiateVideoDisplay(this, m_crashSentinel);
1465     const auto backendEnum = QMetaEnum::fromType<Settings::VideoBackend>();
1466     const auto backendName = backendEnum.valueToKey(static_cast<int>(Settings::SettingsData::instance()->videoBackend()));
1467     m_crashSentinel.setCrashInfo(backendName);
1468 
1469     addWidget(m_videoDisplay);
1470     connect(m_videoDisplay, &VideoDisplay::stopped, this, &ViewerWidget::videoStopped);
1471     m_cursorHandlerForVideoDisplay = new CursorVisibilityHandler(m_videoDisplay);
1472 }
1473 
1474 void Viewer::ViewerWidget::createAnnotationMenu()
1475 {
1476     auto menu = new QMenu(i18n("Annotate"));
1477 
1478     auto addAction = [&](const char *name, const QString &title, auto slot, auto shortCut) {
1479         QAction *action = m_actions->addAction(QString::fromLatin1(name), this, slot);
1480         action->setText(title);
1481         m_actions->setDefaultShortcut(action, shortCut);
1482         menu->addAction(action);
1483         return action;
1484     };
1485 
1486     auto toggleGroup = new QActionGroup(this);
1487     auto addTagAction = [&](const char *name, const QString &title, TagMode mode, auto shortCut) {
1488         auto action = addAction(
1489             name, title, [this, mode] { setTagMode(mode); }, shortCut);
1490         action->setCheckable(true);
1491         toggleGroup->addAction(action);
1492         return action;
1493     };
1494 
1495     addAction("viewer-show-keybindings", i18nc("@action:inmenu", "Help"), &ViewerWidget::showAnnotationHelp, Qt::CTRL + Qt::Key_Question);
1496 
1497     addAction("viewer-edit-image-properties", i18nc("@action:inmenu", "Annotation Dialog"), &ViewerWidget::editImage, Qt::CTRL + Qt::Key_1);
1498     m_addTagAction = addAction("viewer-add-tag", i18nc("@action:inmenu", "Add tag"), &ViewerWidget::addTag, i18nc("short cut for add tag", "CTRL+a"));
1499     m_addTagAction->setEnabled(false);
1500 
1501     m_copyAction = addAction("viewer-copy-tag-from-previous-image", i18nc("@action:inmenu", "Copy Data from Previous Image"), &ViewerWidget::copyTagsFromPreviousImage,
1502                              i18nc("Shortcut for copy annotations from previous image", "CTRL+c"));
1503     m_copyAction->setEnabled(false);
1504 
1505     m_addDescriptionAction = addAction("viewer-edit-description", i18nc("@action:inmenu", "Edit Description"), &ViewerWidget::editDescription,
1506                                        i18nc("Shortcut for add description to image", "CTRL+d"));
1507     m_addDescriptionAction->setEnabled(false);
1508 
1509     menu->addSection(i18n("Annotation Mode"));
1510     auto action = addTagAction(
1511         "viewer-tagmode-locked", i18nc("@action:inmenu", "Locked"), TagMode::Locked,
1512         i18nc("Shortcut for turning of annotations in the viewer", "CTRL+l"));
1513     action->setChecked(true);
1514 
1515     addTagAction(
1516         "viewer-tagmode-annotating", i18nc("@action:inmenu", "Assign Tags"), TagMode::Annotating,
1517         i18nc("Shortcut for turning annotations mode to annotating", "F2"));
1518 
1519     addTagAction(
1520         "viewer-tagmode-tokenizing", i18nc("@action:inmenu", "Assign Tokens"), TagMode::Tokenizing,
1521         i18nc("Shortcut for turning annotations mode to tokenizing", "CTRL+t"));
1522 
1523     const bool showFullFeatures = m_type == UsageType::FullFeaturedViewer;
1524     // hide entries of hidden menus so that they can't be triggered via shortcut:
1525     for (auto &action : menu->actions())
1526         action->setVisible(showFullFeatures);
1527     m_popup->addMenu(menu)->setVisible(showFullFeatures);
1528 }
1529 
1530 void Viewer::ViewerWidget::stopPlayback()
1531 {
1532     m_videoDisplay->stop();
1533     m_crashSentinel.suspend();
1534 }
1535 
1536 void Viewer::ViewerWidget::invalidateThumbnail() const
1537 {
1538     const auto currentFile = currentFileName();
1539     if (currentFile.isNull())
1540         return;
1541 
1542     MainWindow::Window::theMainWindow()->thumbnailCache()->removeThumbnail(currentFile);
1543 }
1544 
1545 void Viewer::ViewerWidget::setTaggedAreasFromImage()
1546 {
1547     // Clean all areas we probably already have
1548     const auto allAreas = findChildren<TaggedArea *>();
1549     for (TaggedArea *area : allAreas) {
1550         area->deleteLater();
1551     }
1552 
1553     DB::TaggedAreas taggedAreas = currentInfo()->taggedAreas();
1554     addTaggedAreas(taggedAreas, AreaType::Standard);
1555 }
1556 
1557 void Viewer::ViewerWidget::addAdditionalTaggedAreas(DB::TaggedAreas taggedAreas)
1558 {
1559     addTaggedAreas(taggedAreas, AreaType::Highlighted);
1560 }
1561 
1562 void Viewer::ViewerWidget::addTaggedAreas(DB::TaggedAreas taggedAreas, AreaType type)
1563 {
1564     DB::TaggedAreasIterator areasInCategory(taggedAreas);
1565     QString category;
1566     QString tag;
1567 
1568     while (areasInCategory.hasNext()) {
1569         areasInCategory.next();
1570         category = areasInCategory.key();
1571 
1572         DB::PositionTagsIterator areaData(areasInCategory.value());
1573         while (areaData.hasNext()) {
1574             areaData.next();
1575             tag = areaData.key();
1576 
1577             // Add a new frame for the area
1578             TaggedArea *newArea = new TaggedArea(this);
1579             newArea->setTagInfo(category, category, tag);
1580             newArea->setActualGeometry(areaData.value());
1581             newArea->setHighlighted(type == AreaType::Highlighted);
1582             newArea->show();
1583 
1584             connect(m_infoBox, &InfoBox::tagHovered, newArea, &TaggedArea::checkIsSelected);
1585             connect(m_infoBox, &InfoBox::noTagHovered, newArea, &TaggedArea::deselect);
1586         }
1587     }
1588 
1589     // Be sure to display the areas, as viewGeometryChanged is not always emitted on load
1590 
1591     QSize imageSize = currentInfo()->size();
1592     QSize windowSize = this->size();
1593 
1594     // On load, the image is never zoomed, so it's a bit easier ;-)
1595     double scaleWidth = double(imageSize.width()) / windowSize.width();
1596     double scaleHeight = double(imageSize.height()) / windowSize.height();
1597     int offsetTop = 0;
1598     int offsetLeft = 0;
1599     if (scaleWidth > scaleHeight) {
1600         offsetTop = (windowSize.height() - imageSize.height() / scaleWidth);
1601     } else {
1602         offsetLeft = (windowSize.width() - imageSize.width() / scaleHeight);
1603     }
1604 
1605     remapAreas(
1606         QSize(windowSize.width() - offsetLeft, windowSize.height() - offsetTop),
1607         QRect(QPoint(0, 0), QPoint(imageSize.width(), imageSize.height())),
1608         1);
1609 }
1610 
1611 void Viewer::ViewerWidget::remapAreas(QSize viewSize, QRect zoomWindow, double sizeRatio)
1612 {
1613     QSize currentWindowSize = this->size();
1614     int outerOffsetLeft = (currentWindowSize.width() - viewSize.width()) / 2;
1615     int outerOffsetTop = (currentWindowSize.height() - viewSize.height()) / 2;
1616 
1617     if (sizeRatio != 1) {
1618         zoomWindow = QRect(
1619             QPoint(
1620                 double(zoomWindow.left()) * sizeRatio,
1621                 double(zoomWindow.top()) * sizeRatio),
1622             QPoint(
1623                 double(zoomWindow.left() + zoomWindow.width()) * sizeRatio,
1624                 double(zoomWindow.top() + zoomWindow.height()) * sizeRatio));
1625     }
1626 
1627     double scaleHeight = double(viewSize.height()) / zoomWindow.height();
1628     double scaleWidth = double(viewSize.width()) / zoomWindow.width();
1629 
1630     int innerOffsetLeft = -zoomWindow.left() * scaleWidth;
1631     int innerOffsetTop = -zoomWindow.top() * scaleHeight;
1632 
1633     const auto areas = findChildren<TaggedArea *>();
1634     for (TaggedArea *area : areas) {
1635         const QRect actualGeometry = area->actualGeometry();
1636         QRect screenGeometry;
1637 
1638         screenGeometry.setWidth(actualGeometry.width() * scaleWidth);
1639         screenGeometry.setHeight(actualGeometry.height() * scaleHeight);
1640         screenGeometry.moveTo(
1641             actualGeometry.left() * scaleWidth + outerOffsetLeft + innerOffsetLeft,
1642             actualGeometry.top() * scaleHeight + outerOffsetTop + innerOffsetTop);
1643 
1644         area->setGeometry(screenGeometry);
1645     }
1646 }
1647 
1648 void Viewer::ViewerWidget::setCopyLinkEngine(MainWindow::CopyLinkEngine *copyLinkEngine)
1649 {
1650     m_copyLinkEngine = copyLinkEngine;
1651 }
1652 
1653 void Viewer::ViewerWidget::triggerCopyLinkAction(MainWindow::CopyLinkEngine::Action action)
1654 {
1655     const auto currentFile = currentFileName();
1656     if (currentFile.isNull())
1657         return;
1658 
1659     if (!m_copyLinkEngine) {
1660         qCWarning(ViewerLog) << "ViewerWidget::triggerCopyLinkAction called without CopyLinkEngine. This is a bug!";
1661         return;
1662     }
1663     const auto selectedFiles = QList<QUrl> { QUrl::fromLocalFile(currentFile.absolute()) };
1664     m_copyLinkEngine->selectTarget(this, selectedFiles, action);
1665 }
1666 
1667 void Viewer::ViewerWidget::toggleTag(const QString &category, const QString &value)
1668 {
1669     QString tag = value;
1670     if (category == DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory)->name())
1671         tag = value.toUpper();
1672 
1673     const bool tagIsSet = !currentInfo()->hasCategoryInfo(category, tag);
1674     if (tagIsSet)
1675         currentInfo()->addCategoryInfo(category, tag);
1676     else
1677         currentInfo()->removeCategoryInfo(category, tag);
1678 
1679     // Assume we've now annotated this image - this is to avoid removing the untagged item all the time.
1680     currentInfo()->removeCategoryInfo(Settings::SettingsData::instance()->untaggedCategory(), Settings::SettingsData::instance()->untaggedTag());
1681     updateInfoBox();
1682 
1683     if (category == DB::ImageDB::instance()->categoryCollection()->categoryForSpecial(DB::Category::TokensCategory)->name())
1684         tag = i18n("Token %1", tag);
1685     m_transientDisplay->display(tagIsSet ? tag : QLatin1String("<s>%1</s>").arg(tag), 500ms, TransientDisplay::NoFadeOut);
1686 }
1687 
1688 void Viewer::ViewerWidget::copyTagsFromPreviousImage()
1689 {
1690     // Search for the previous image - that is the first one not deleted
1691     int index = m_current - 1;
1692     while (index >= 0) {
1693         const auto fileName = m_list.at(index);
1694         if (!m_removed.contains(fileName))
1695             break;
1696         --index;
1697     }
1698     if (index == -1)
1699         return; // Nothing found
1700 
1701     const auto prevImage = DB::ImageDB::instance()->info(m_list[index]);
1702     currentInfo()->merge(*prevImage);
1703 
1704     updateInfoBox();
1705     MainWindow::DirtyIndicator::markDirty();
1706 }
1707 
1708 #include "moc_ViewerWidget.cpp"
1709 
1710 // vi:expandtab:tabstop=4 shiftwidth=4: