File indexing completed on 2024-04-28 15:40:29
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 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: