File indexing completed on 2024-05-12 08:34:16

0001 /*
0002  *  SPDX-FileCopyrightText: 2019 David Redondo <kde@david-redondo.de>
0003  *  SPDX-FileCopyrightText: 2015 Boudhayan Gupta <bgupta@kde.org>
0004  *
0005  *  SPDX-License-Identifier: LGPL-2.0-or-later
0006  */
0007 
0008 #include "SpectacleCore.h"
0009 #include "CaptureModeModel.h"
0010 #include "CommandLineOptions.h"
0011 #include "ExportManager.h"
0012 #include "Geometry.h"
0013 #include "Gui/Annotations/AnnotationViewport.h"
0014 #include "Gui/CaptureWindow.h"
0015 #include "Gui/Selection.h"
0016 #include "Gui/SelectionEditor.h"
0017 #include "Gui/SpectacleWindow.h"
0018 #include "Gui/ExportMenu.h"
0019 #include "Gui/HelpMenu.h"
0020 #include "Gui/OptionsMenu.h"
0021 #include "Platforms/VideoPlatform.h"
0022 #include "ShortcutActions.h"
0023 #include "PlasmaVersion.h"
0024 // generated
0025 #include "Config.h"
0026 #include "settings.h"
0027 #include "spectacle_core_debug.h"
0028 
0029 #include <KFormat>
0030 #include <KGlobalAccel>
0031 #include <KIO/OpenUrlJob>
0032 #include <KLocalizedString>
0033 #include <KMessageBox>
0034 #include <KNotification>
0035 #include <KWindowSystem>
0036 #include <KX11Extras>
0037 #include <LayerShellQt/Shell>
0038 #include <LayerShellQt/Window>
0039 
0040 #include <QApplication>
0041 #include <QClipboard>
0042 #include <QCommandLineParser>
0043 #include <QDBusConnection>
0044 #include <QDBusMessage>
0045 #include <QDir>
0046 #include <QDrag>
0047 #include <QKeySequence>
0048 #include <QMimeData>
0049 #include <QProcess>
0050 #include <QQmlComponent>
0051 #include <QQmlContext>
0052 #include <QQmlEngine>
0053 #include <QScopedPointer>
0054 #include <QScreen>
0055 #include <QSystemTrayIcon>
0056 #include <QTimer>
0057 #include <QtMath>
0058 #include <qobjectdefs.h>
0059 #include <utility>
0060 
0061 using namespace Qt::StringLiterals;
0062 
0063 SpectacleCore *SpectacleCore::s_self = nullptr;
0064 static std::unique_ptr<QSystemTrayIcon> s_systemTrayIcon;
0065 
0066 SpectacleCore::SpectacleCore(QObject *parent)
0067     : QObject(parent)
0068 {
0069     s_self = this;
0070     // Timer to prevent lots of extra rendering to images
0071     m_annotationSyncTimer = std::make_unique<QTimer>(new QTimer(this));
0072     m_annotationSyncTimer->setInterval(400);
0073     m_annotationSyncTimer->setSingleShot(true);
0074 
0075     m_delayAnimation = std::make_unique<QVariantAnimation>(this);
0076     m_delayAnimation->setStartValue(0.0);
0077     m_delayAnimation->setEndValue(1.0);
0078     m_delayAnimation->setDuration(1);
0079     m_delayAnimation->setCurrentTime(0);
0080     auto delayAnimation = m_delayAnimation.get();
0081     // We need to reset this on start in case a previous instance
0082     // didn't reset these before it closed or crashed.
0083     unityLauncherUpdate({
0084         {u"progress-visible"_s, false},
0085         {u"progress"_s, 0}
0086     });
0087     using State = QVariantAnimation::State;
0088     auto onStateChanged = [this](State newState, State oldState) {
0089         Q_UNUSED(oldState)
0090         if (newState == State::Running) {
0091             unityLauncherUpdate({{u"progress-visible"_s, true}});
0092         } else if (newState == State::Stopped) {
0093             unityLauncherUpdate({{u"progress-visible"_s, false}});
0094             m_delayAnimation->setCurrentTime(0);
0095         }
0096     };
0097     auto onValueChanged = [this](const QVariant &value) {
0098         Q_EMIT captureTimeRemainingChanged();
0099         Q_EMIT captureProgressChanged();
0100         unityLauncherUpdate({{u"progress"_s, value.toReal()}});
0101         const auto windows = SpectacleWindow::instances();
0102         if (m_delayAnimation->state() != State::Stopped && !windows.isEmpty()) {
0103             if (captureTimeRemaining() <= 500 && windows.constFirst()->isVisible()) {
0104                 SpectacleWindow::setVisibilityForAll(QWindow::Hidden);
0105             }
0106             SpectacleWindow::setTitleForAll(SpectacleWindow::Timer);
0107         }
0108     };
0109     auto onFinished = [this]() {
0110         m_imagePlatform->doGrab(ImagePlatform::ShutterMode::Immediate, m_lastGrabMode, m_lastIncludePointer, m_lastIncludeDecorations, m_lastIncludeShadow);
0111     };
0112     QObject::connect(delayAnimation, &QVariantAnimation::stateChanged,
0113                      this, onStateChanged, Qt::QueuedConnection);
0114     QObject::connect(delayAnimation, &QVariantAnimation::valueChanged,
0115                      this, onValueChanged, Qt::QueuedConnection);
0116     QObject::connect(delayAnimation, &QVariantAnimation::finished,
0117                      this, onFinished, Qt::QueuedConnection);
0118 
0119     m_imagePlatform = loadImagePlatform();
0120     m_videoPlatform = loadVideoPlatform();
0121     auto imagePlatform = m_imagePlatform.get();
0122     m_annotationDocument = std::make_unique<AnnotationDocument>(new AnnotationDocument(this));
0123 
0124     // essential connections
0125     connect(SelectionEditor::instance(), &SelectionEditor::accepted,
0126             this, [this](const QRectF &rect, const ExportManager::Actions &actions){
0127         ExportManager::instance()->updateTimestamp();
0128         if (m_videoMode) {
0129             const auto captureWindows = CaptureWindow::instances();
0130             SpectacleWindow::setVisibilityForAll(QWindow::Hidden);
0131             for (auto captureWindow : captureWindows) {
0132                 // Destroy the QPlatformWindow so we can change the window behavior.
0133                 // The QPlatformWindow will be recreated when the window is shown again.
0134                 captureWindow->destroy();
0135                 captureWindow->setFlag(Qt::WindowTransparentForInput, true);
0136                 captureWindow->setFlag(Qt::WindowStaysOnTopHint, true);
0137                 if (auto window = LayerShellQt::Window::get(captureWindow)) {
0138                     using namespace LayerShellQt;
0139                     window->setCloseOnDismissed(true);
0140                     window->setLayer(Window::LayerOverlay);
0141                     auto anchors = Window::Anchors::fromInt(Window::AnchorTop | Window::AnchorBottom | Window::AnchorLeft | Window::AnchorRight);
0142                     window->setAnchors(anchors);
0143                     window->setKeyboardInteractivity(Window::KeyboardInteractivityNone);
0144                 }
0145             }
0146             SpectacleWindow::setVisibilityForAll(QWindow::FullScreen);
0147             // deleteWindows();
0148             // showViewerIfGuiMode(true);
0149             bool includePointer = m_cliOptions[CommandLineOptions::Pointer];
0150             includePointer |= m_startMode != StartMode::Background && Settings::videoIncludePointer();
0151             const auto &output = m_outputUrl.isLocalFile() ? videoOutputUrl() : QUrl();
0152             m_videoPlatform->startRecording(output, VideoPlatform::Region, rect.toRect(), includePointer);
0153         } else {
0154             deleteWindows();
0155             m_annotationDocument->cropCanvas(rect);
0156             syncExportImage();
0157             showViewerIfGuiMode();
0158             SpectacleWindow::setTitleForAll(SpectacleWindow::Unsaved);
0159             const auto &exportActions = actions & ExportManager::AnyAction ? actions : autoExportActions();
0160             ExportManager::instance()->exportImage(exportActions, outputUrl());
0161         }
0162     });
0163 
0164     connect(imagePlatform, &ImagePlatform::newScreenshotTaken, this, [this](const QImage &image){
0165         m_annotationDocument->clearAnnotations();
0166         m_annotationDocument->setImage(image);
0167         setExportImage(image);
0168         ExportManager::instance()->updateTimestamp();
0169         showViewerIfGuiMode();
0170         SpectacleWindow::setTitleForAll(SpectacleWindow::Unsaved);
0171         ExportManager::instance()->exportImage(autoExportActions(), outputUrl());
0172         setVideoMode(false);
0173     });
0174     connect(imagePlatform, &ImagePlatform::newCroppableScreenshotTaken, this, [this](const QImage &image) {
0175         m_annotationDocument->clearAnnotations();
0176         m_annotationDocument->setImage(image);
0177         SelectionEditor::instance()->reset();
0178 
0179         initCaptureWindows(CaptureWindow::Image);
0180         SpectacleWindow::setTitleForAll(SpectacleWindow::Unsaved);
0181         SpectacleWindow::setVisibilityForAll(QWindow::FullScreen);
0182     });
0183     connect(imagePlatform, &ImagePlatform::newScreenshotFailed, this, &SpectacleCore::onScreenshotFailed);
0184 
0185     // set up the export manager
0186     auto exportManager = ExportManager::instance();
0187     auto onImageExported = [this](const ExportManager::Actions &actions, const QUrl &url) {
0188         if (actions & ExportManager::UserAction && Settings::quitAfterSaveCopyExport()) {
0189             deleteWindows();
0190         }
0191 
0192         if (isGuiNull()) {
0193             if (m_cliOptions[CommandLineOptions::NoNotify]) {
0194                 // if we notify, we Q_EMIT allDone only if the user either dismissed the notification or pressed
0195                 // the "Open" button, otherwise the app closes before it can react to it.
0196                 if (actions & ExportManager::CopyImage) {
0197                     // Allow some time for clipboard content to transfer if '--nonotify' is used, see Bug #411263
0198                     // TODO: Find better solution
0199                     QTimer::singleShot(250, this, &SpectacleCore::allDone);
0200                 } else {
0201                     Q_EMIT allDone();
0202                 }
0203             } else {
0204                 doNotify(ScreenCapture::Screenshot, actions, url);
0205             }
0206             return;
0207         }
0208 
0209         auto viewerWindow = ViewerWindow::instance();
0210         if (!viewerWindow) {
0211             return;
0212         }
0213 
0214         if (actions & ExportManager::AnySave) {
0215             SpectacleWindow::setTitleForAll(SpectacleWindow::Saved, url.fileName());
0216             if (actions & ExportManager::CopyImage) {
0217                 viewerWindow->showSavedAndCopiedMessage(url);
0218             } else if (actions & ExportManager::CopyPath) {
0219                 viewerWindow->showSavedAndLocationCopiedMessage(url);
0220             } else {
0221                 viewerWindow->showSavedMessage(url);
0222             }
0223         } else if (actions & ExportManager::CopyImage) {
0224             viewerWindow->showCopiedMessage();
0225         }
0226     };
0227     connect(exportManager, &ExportManager::imageExported, this, onImageExported);
0228     auto onVideoExported = [this](const ExportManager::Actions &actions, const QUrl &url) {
0229         setCurrentVideo(url);
0230 
0231         if (actions & ExportManager::UserAction && Settings::quitAfterSaveCopyExport()) {
0232             deleteWindows();
0233         } else if (!ViewerWindow::instance()) {
0234             showViewerIfGuiMode();
0235         }
0236 
0237         if (isGuiNull()) {
0238             if (m_cliOptions[CommandLineOptions::NoNotify]) {
0239                 Q_EMIT allDone();
0240             } else {
0241                 doNotify(ScreenCapture::Recording, actions, url);
0242             }
0243             return;
0244         }
0245 
0246         auto viewerWindow = ViewerWindow::instance();
0247         if (!viewerWindow) {
0248             return;
0249         }
0250 
0251         if (actions & ExportManager::AnySave) {
0252             SpectacleWindow::setTitleForAll(SpectacleWindow::Saved, url.fileName());
0253             if (actions & ExportManager::CopyPath) {
0254                 viewerWindow->showSavedAndLocationCopiedMessage(url, true);
0255             } else {
0256                 viewerWindow->showSavedMessage(url, true);
0257             }
0258         } else if (actions & ExportManager::CopyPath) {
0259             viewerWindow->showLocationCopiedMessage();
0260         }
0261     };
0262     connect(exportManager, &ExportManager::videoExported, this, onVideoExported);
0263     connect(exportManager, &ExportManager::errorMessage, this, &SpectacleCore::showErrorMessage);
0264 
0265     connect(imagePlatform, &ImagePlatform::windowTitleChanged, exportManager, &ExportManager::setWindowTitle);
0266     connect(m_annotationDocument.get(), &AnnotationDocument::repaintNeeded, m_annotationSyncTimer.get(), qOverload<>(&QTimer::start));
0267     connect(m_annotationSyncTimer.get(), &QTimer::timeout, this, [this] {
0268         ExportManager::instance()->setImage(m_annotationDocument->renderToImage());
0269     }, Qt::QueuedConnection); // QueuedConnection to help prevent making the visible render lag.
0270 
0271     // set up shortcuts
0272     KGlobalAccel::self()->setGlobalShortcut(ShortcutActions::self()->openAction(),
0273                                             QList<QKeySequence>{
0274                                                 Qt::Key_Print,
0275                                                 // Default screenshot shortcut on Windows.
0276                                                 // Also for keyboards without a print screen key.
0277                                                 Qt::META | Qt::SHIFT | Qt::Key_S,
0278                                             });
0279     KGlobalAccel::self()->setGlobalShortcut(ShortcutActions::self()->fullScreenAction(), Qt::SHIFT | Qt::Key_Print);
0280     KGlobalAccel::self()->setGlobalShortcut(ShortcutActions::self()->activeWindowAction(), Qt::META | Qt::Key_Print);
0281     KGlobalAccel::self()->setGlobalShortcut(ShortcutActions::self()->windowUnderCursorAction(), Qt::META | Qt::CTRL | Qt::Key_Print);
0282     KGlobalAccel::self()->setGlobalShortcut(ShortcutActions::self()->regionAction(), Qt::META | Qt::SHIFT | Qt::Key_Print);
0283     KGlobalAccel::self()->setGlobalShortcut(ShortcutActions::self()->currentScreenAction(), QList<QKeySequence>());
0284     KGlobalAccel::self()->setGlobalShortcut(ShortcutActions::self()->openWithoutScreenshotAction(), QList<QKeySequence>());
0285     KGlobalAccel::self()->setGlobalShortcut(ShortcutActions::self()->recordScreenAction(), Qt::META | Qt::ALT | Qt::Key_R);
0286     KGlobalAccel::self()->setGlobalShortcut(ShortcutActions::self()->recordWindowAction(), Qt::META | Qt::CTRL | Qt::Key_R);
0287     KGlobalAccel::self()->setGlobalShortcut(ShortcutActions::self()->recordRegionAction(),
0288                                             QList<QKeySequence>{
0289                                                 // Similar to region screenshot
0290                                                 Qt::META | Qt::SHIFT | Qt::Key_R,
0291                                                 // Also use Meta+R for now
0292                                                 Qt::META | Qt::Key_R,
0293                                             });
0294 
0295     // set up CaptureMode model
0296     m_captureModeModel = std::make_unique<CaptureModeModel>(imagePlatform->supportedGrabModes(), this);
0297     m_recordingModeModel = std::make_unique<RecordingModeModel>(m_videoPlatform->supportedRecordingModes(), this);
0298     m_videoFormatModel = std::make_unique<VideoFormatModel>(m_videoPlatform->supportedFormats(), this);
0299     auto captureModeModel = m_captureModeModel.get();
0300     connect(imagePlatform, &ImagePlatform::supportedGrabModesChanged, captureModeModel, [this](){
0301         m_captureModeModel->setGrabModes(m_imagePlatform->supportedGrabModes());
0302     });
0303 
0304     connect(qApp, &QApplication::screenRemoved, this, [this](QScreen *screen) {
0305         // It's dangerous to erase from within a for loop, so we use std::find_if
0306         auto hasScreen = [screen](const CaptureWindow::UniquePointer &window) {
0307             return window->screen() == screen;
0308         };
0309         auto it = std::find_if(m_captureWindows.begin(), m_captureWindows.end(), hasScreen);
0310         if (it != m_captureWindows.end()) {
0311             m_captureWindows.erase(it);
0312         }
0313     });
0314 
0315     s_systemTrayIcon = std::make_unique<QSystemTrayIcon>(QIcon::fromTheme(u"media-record-symbolic"_s));
0316     auto systemTrayIcon = s_systemTrayIcon.get();
0317     connect(systemTrayIcon, &QSystemTrayIcon::activated, systemTrayIcon, [](auto reason) {
0318         if (reason == QSystemTrayIcon::Trigger) {
0319             SpectacleCore::instance()->finishRecording();
0320         }
0321     });
0322 
0323     auto videoPlatform = m_videoPlatform.get();
0324     connect(videoPlatform, &VideoPlatform::recordingChanged,
0325             systemTrayIcon, [this](bool isRecording){
0326         s_systemTrayIcon->setVisible(isRecording);
0327         if (!isRecording) {
0328             m_captureWindows.clear();
0329         }
0330     });
0331     connect(videoPlatform, &VideoPlatform::recordedTimeChanged, this, [this] {
0332         Q_EMIT recordedTimeChanged();
0333         s_systemTrayIcon->setToolTip(i18nc("@info:tooltip", "Spectacle is recording: %1\nClick to finish recording", recordedTime()));
0334     });
0335     connect(videoPlatform, &VideoPlatform::recordingSaved, this, [this](const QUrl &fileUrl) {
0336         // Always try to save. Needed to move recordings out of temp dir.
0337         ExportManager::instance()->exportVideo(autoExportActions() | ExportManager::Save, fileUrl, videoOutputUrl());
0338     });
0339     connect(videoPlatform, &VideoPlatform::recordingCanceled, this, [this] {
0340         if (m_startMode != StartMode::Gui || isGuiNull()) {
0341             Q_EMIT allDone();
0342             return;
0343         }
0344         SpectacleWindow::setTitleForAll(SpectacleWindow::Previous);
0345     });
0346     connect(videoPlatform, &VideoPlatform::recordingFailed, this, [this](const QString &message){
0347         switch (m_startMode) {
0348         case StartMode::Background:
0349             if (!message.isEmpty()) {
0350                 showErrorMessage(message);
0351             }
0352             Q_EMIT allDone();
0353             return;
0354         case StartMode::DBus:
0355             Q_EMIT dbusRecordingFailed();
0356             Q_EMIT allDone();
0357             return;
0358         case StartMode::Gui:
0359             if (!ViewerWindow::instance()) {
0360                 initViewerWindow(ViewerWindow::Dialog);
0361             }
0362             ViewerWindow::instance()->showRecordingFailedMessage(message);
0363             return;
0364         }
0365     });
0366 }
0367 
0368 SpectacleCore::~SpectacleCore() noexcept
0369 {
0370     s_self = nullptr;
0371 }
0372 
0373 SpectacleCore *SpectacleCore::instance()
0374 {
0375     return s_self;
0376 }
0377 
0378 ImagePlatform *SpectacleCore::imagePlatform() const
0379 {
0380     return m_imagePlatform.get();
0381 }
0382 
0383 CaptureModeModel *SpectacleCore::captureModeModel() const
0384 {
0385     return m_captureModeModel.get();
0386 }
0387 
0388 RecordingModeModel *SpectacleCore::recordingModeModel() const
0389 {
0390     return m_recordingModeModel.get();
0391 }
0392 
0393 VideoFormatModel *SpectacleCore::videoFormatModel() const
0394 {
0395     return m_videoFormatModel.get();
0396 }
0397 
0398 AnnotationDocument *SpectacleCore::annotationDocument() const
0399 {
0400     return m_annotationDocument.get();
0401 }
0402 
0403 QUrl SpectacleCore::screenCaptureUrl() const
0404 {
0405     return m_screenCaptureUrl;
0406 }
0407 
0408 void SpectacleCore::setScreenCaptureUrl(const QUrl &url)
0409 {
0410     if(m_screenCaptureUrl == url) {
0411         return;
0412     }
0413     m_screenCaptureUrl = url;
0414     Q_EMIT screenCaptureUrlChanged();
0415 }
0416 
0417 void SpectacleCore::setScreenCaptureUrl(const QString &filePath)
0418 {
0419     if (QDir::isRelativePath(filePath)) {
0420         setScreenCaptureUrl(QUrl::fromUserInput(QDir::current().absoluteFilePath(filePath)));
0421     } else {
0422         setScreenCaptureUrl(QUrl::fromUserInput(filePath));
0423     }
0424 }
0425 
0426 QUrl SpectacleCore::outputUrl() const
0427 {
0428     return m_outputUrl.isEmpty() ? m_editExistingUrl : m_outputUrl;
0429 }
0430 
0431 int SpectacleCore::captureTimeRemaining() const
0432 {
0433     int totalDuration = m_delayAnimation->totalDuration();
0434     int currentTime = m_delayAnimation->currentTime();
0435     return currentTime > totalDuration || m_delayAnimation->state() == QVariantAnimation::Stopped ?
0436         0 : totalDuration - currentTime;
0437 }
0438 
0439 qreal SpectacleCore::captureProgress() const
0440 {
0441     // using currentValue() sometimes gives 1.0 when we don't want it.
0442     return m_delayAnimation->state() == QVariantAnimation::Stopped ?
0443         0 : m_delayAnimation->currentValue().toReal();
0444 }
0445 
0446 void SpectacleCore::activate(const QStringList &arguments, const QString &workingDirectory)
0447 {
0448     if (!workingDirectory.isEmpty()) {
0449         QDir::setCurrent(workingDirectory);
0450     }
0451 
0452     // We can't re-use QCommandLineParser instances, it preserves earlier parsed values
0453     QCommandLineParser parser;
0454     parser.addOptions(CommandLineOptions::self()->allOptions);
0455     parser.parse(arguments);
0456 
0457     // Collect parsed command line options
0458     using Option = CommandLineOptions::Option;
0459     m_cliOptions.fill(false); // reset all values to false
0460     int optionsToCheck = parser.optionNames().size();
0461     for (int i = 0; optionsToCheck > 0 && i < CommandLineOptions::self()->allOptions.size(); ++i) {
0462         m_cliOptions[i] = parser.isSet(CommandLineOptions::self()->allOptions[i]);
0463         if (m_cliOptions[i]) {
0464             --optionsToCheck;
0465         }
0466     }
0467 
0468     // Determine start mode
0469     m_startMode = StartMode::Gui; // Default to Gui
0470     // Gui is an option that's normally useless since it's the default mode.
0471     // Make it override the other modes if explicitly set, using the launchonly option,
0472     // or editing an existing image. Editing an existing image requires a viewer window.
0473     if (!m_cliOptions[Option::Gui]
0474         && !m_cliOptions[Option::LaunchOnly]
0475         && !m_cliOptions[Option::EditExisting]) {
0476         // Background gets precidence over DBus
0477         if (m_cliOptions[Option::Background]) {
0478             m_startMode = StartMode::Background;
0479         } else if (m_cliOptions[Option::DBus]) {
0480             m_startMode = StartMode::DBus;
0481         }
0482     }
0483 
0484     if (parser.optionNames().size() > 0 || m_startMode != StartMode::Gui) {
0485         // Delete windows if we have CLI options or not in GUI mode.
0486         // We don't want to delete them otherwise because that will mess with the
0487         // settings for PrintScreen key behavior.
0488         deleteWindows();
0489     }
0490 
0491     // reset last region if it should not be remembered across restarts
0492     if (!(Settings::rememberSelectionRect() == Settings::EnumRememberSelectionRect::Always)) {
0493         Settings::setSelectionRect({0, 0, 0, 0});
0494     }
0495 
0496     /* The logic for setting options for each start mode:
0497      *
0498      * - Gui/DBus: Prioritise command line options and default to saved settings.
0499      * - Background: Prioritise command line options and use defaults based on
0500      *   how command line options are meant to be used.
0501      *
0502      * Never start with a delay by default. It is annoying and confuses users
0503      * when nothing happens immediately after starting spectacle.
0504      */
0505 
0506     // In the GUI/CLI, the TransientWithParent mode is represented by the
0507     // "Window Under Cursor" option and the real WindowUnderCursor mode is
0508     // represented by the popup-only/transientOnly setting, which is meant to
0509     // override TransientWithParent. Needless to say, This is rather convoluted.
0510     // TODO: Improve the API for transientOnly or make it obsolete.
0511     bool transientOnly;
0512     bool onClick;
0513     bool includeDecorations;
0514     bool includePointer;
0515     bool includeShadow;
0516     if (m_startMode == StartMode::Background) {
0517         transientOnly = m_cliOptions[Option::TransientOnly];
0518         onClick = m_cliOptions[Option::OnClick];
0519         includeDecorations = !m_cliOptions[Option::NoDecoration];
0520         includePointer = m_cliOptions[Option::Pointer];
0521         includeShadow = !m_cliOptions[Option::NoShadow];
0522     } else {
0523         transientOnly = Settings::transientOnly() || m_cliOptions[Option::TransientOnly];
0524         onClick = Settings::captureOnClick() || m_cliOptions[Option::OnClick];
0525         includeDecorations = Settings::includeDecorations()
0526                             && !m_cliOptions[Option::NoDecoration];
0527         includeShadow = Settings::includeShadow() && !m_cliOptions[Option::NoShadow];
0528         includePointer = Settings::includePointer() || m_cliOptions[Option::Pointer];
0529     }
0530 
0531     int delayMsec = 0; // default to 0 if cli value parse fails
0532     if (onClick) {
0533         delayMsec = -1;
0534     } else if (m_cliOptions[Option::Delay]) {
0535         bool parseOk = false;
0536         int value = parser.value(CommandLineOptions::self()->delay).toInt(&parseOk);
0537         if (parseOk) {
0538             delayMsec = value;
0539         }
0540     }
0541 
0542     if (m_cliOptions[Option::EditExisting]) {
0543         auto input = parser.value(CommandLineOptions::self()->editExisting);
0544         m_editExistingUrl = QUrl::fromUserInput(input, QDir::currentPath(), QUrl::AssumeLocalFile);
0545         // QFileInfo::exists() only works with local files.
0546         auto existingLocalFile = m_editExistingUrl.toLocalFile();
0547         if (QFileInfo::exists(existingLocalFile)) {
0548             // If editing an existing image, open the annotation editor.
0549             // This QImage constructor only works with local files or Qt resource file names.
0550             QImage existingImage(existingLocalFile);
0551             m_annotationDocument->clearAnnotations();
0552             m_annotationDocument->setImage(existingImage);
0553             showViewerIfGuiMode();
0554             SpectacleWindow::setTitleForAll(SpectacleWindow::Saved, m_editExistingUrl.fileName());
0555             return;
0556         } else {
0557             m_cliOptions[Option::EditExisting] = false;
0558             m_editExistingUrl.clear();
0559         }
0560     } else {
0561         m_editExistingUrl.clear();
0562     }
0563 
0564     if (m_cliOptions[Option::Output]) {
0565         m_outputUrl = QUrl::fromUserInput(parser.value(CommandLineOptions::self()->output),
0566                                           QDir::currentPath(), QUrl::AssumeLocalFile);
0567         if (!m_outputUrl.isValid()) {
0568             m_cliOptions[Option::Output] = false;
0569             m_outputUrl.clear();
0570         }
0571     } else {
0572         m_outputUrl.clear();
0573     }
0574 
0575     // Determine grab mode
0576     using CaptureMode = CaptureModeModel::CaptureMode;
0577     using GrabMode = ImagePlatform::GrabMode;
0578     GrabMode grabMode = GrabMode::AllScreens; // Default to all screens
0579     if (m_cliOptions[Option::Fullscreen]) {
0580         grabMode = GrabMode::AllScreens;
0581     } else if (m_cliOptions[Option::Current]) {
0582         grabMode = GrabMode::CurrentScreen;
0583     } else if (m_cliOptions[Option::ActiveWindow]) {
0584         grabMode = GrabMode::ActiveWindow;
0585     } else if (m_cliOptions[Option::Region]) {
0586         grabMode = GrabMode::PerScreenImageNative;
0587     } else if (m_cliOptions[Option::WindowUnderCursor]) {
0588         grabMode = GrabMode::WindowUnderCursor;
0589     } else if (Settings::launchAction() == Settings::UseLastUsedCapturemode) {
0590         grabMode = toGrabMode(CaptureMode(Settings::captureMode()), transientOnly);
0591     }
0592 
0593     using RecordingMode = VideoPlatform::RecordingMode;
0594     RecordingMode recordingMode = RecordingMode::NoRecordingModes;
0595     if (m_cliOptions[Option::Record]) {
0596         auto input = parser.value(CommandLineOptions::self()->record);
0597         if (input.startsWith(u"s"_s, Qt::CaseInsensitive)) {
0598             recordingMode = RecordingMode::Screen;
0599         } else if (input.startsWith(u"w"_s, Qt::CaseInsensitive)) {
0600             recordingMode = RecordingMode::Window;
0601         } else if (input.startsWith(u"r"_s, Qt::CaseInsensitive)) {
0602             recordingMode = RecordingMode::Region;
0603         } else {
0604             // QCommandLineParser handles the case where input is empty
0605             qWarning().noquote() << i18nc("@info:shell", "%1 is not a valid mode for --record", input);
0606             Q_EMIT allDone();
0607             return;
0608         }
0609         setVideoMode(true);
0610 
0611         if (m_startMode != StartMode::Background) {
0612             includePointer = Settings::videoIncludePointer() || m_cliOptions[Option::Pointer];
0613         }
0614     } else {
0615         setVideoMode(false);
0616     }
0617 
0618     // If any capture mode is given in the cli options, let it override
0619     // the setting to not take a screenshot on launch
0620     // clang-format off
0621     bool captureModeFromCli =
0622         m_cliOptions[Option::Fullscreen] ||
0623         m_cliOptions[Option::Current] ||
0624         m_cliOptions[Option::ActiveWindow] ||
0625         m_cliOptions[Option::WindowUnderCursor] ||
0626         m_cliOptions[Option::TransientOnly] ||
0627         m_cliOptions[Option::Region] ||
0628         m_cliOptions[Option::Record];
0629     // clang-format on
0630 
0631     switch (m_startMode) {
0632     case StartMode::DBus:
0633         break;
0634     case StartMode::Background:
0635         if (m_videoMode) {
0636             startRecording(recordingMode, includePointer);
0637         } else {
0638             takeNewScreenshot(grabMode, delayMsec, includePointer, includeDecorations, includeShadow);
0639         }
0640         break;
0641     case StartMode::Gui:
0642         if (isGuiNull()) {
0643             if (m_cliOptions[Option::LaunchOnly] || //
0644                 (Settings::launchAction() == Settings::DoNotTakeScreenshot && !captureModeFromCli)) {
0645                 initViewerWindow(ViewerWindow::Dialog);
0646                 ViewerWindow::instance()->setVisible(true);
0647             } else {
0648                 if (m_videoMode) {
0649                     startRecording(recordingMode, includePointer);
0650                 } else {
0651                     takeNewScreenshot(grabMode, delayMsec, includePointer, includeDecorations, includeShadow);
0652                 }
0653             }
0654         } else {
0655             using Actions = Settings::EnumPrintKeyRunningAction;
0656             switch (Settings::printKeyRunningAction()) {
0657             case Actions::TakeNewScreenshot: {
0658                 // takeNewScreenshot switches to on click if immediate is not supported.
0659                 takeNewScreenshot(grabMode, 0, includePointer, includeDecorations, includeShadow);
0660                 break;
0661             }
0662             case Actions::FocusWindow: {
0663                 bool isCaptureWindow = !CaptureWindow::instances().isEmpty();
0664                 SpectacleWindow *window = nullptr;
0665                 if (isCaptureWindow) {
0666                     window = CaptureWindow::instances().front();
0667                 } else {
0668                     window = ViewerWindow::instance();
0669                 }
0670                 if (isCaptureWindow) {
0671                     SpectacleWindow::setVisibilityForAll(QWindow::FullScreen);
0672                 } else {
0673                     // Unminimize the window.
0674                     window->unminimize();
0675                 }
0676                 window->requestActivate();
0677                 break;
0678             }
0679             case Actions::StartNewInstance: {
0680                 QProcess newInstance;
0681                 newInstance.setProgram(QCoreApplication::applicationFilePath());
0682                 newInstance.setArguments({
0683                     CommandLineOptions::toArgument(CommandLineOptions::self()->newInstance)
0684                 });
0685                 newInstance.startDetached();
0686                 break;
0687             }
0688             }
0689         }
0690 
0691         break;
0692     }
0693 }
0694 
0695 void SpectacleCore::takeNewScreenshot(ImagePlatform::GrabMode grabMode, int timeout, bool includePointer, bool includeDecorations, bool includeWindowShadow)
0696 {
0697     if (m_cliOptions[CommandLineOptions::EditExisting]) {
0698         // Clear when a new screenshot is taken to avoid overwriting
0699         // the existing file with a completely unrelated image.
0700         m_editExistingUrl.clear();
0701         m_cliOptions[CommandLineOptions::EditExisting] = false;
0702     }
0703 
0704     // Clear the window title that can be used in file names.
0705     ExportManager::instance()->setWindowTitle({});
0706 
0707     m_delayAnimation->stop();
0708 
0709     m_lastGrabMode = grabMode;
0710     m_lastIncludePointer = includePointer;
0711     m_lastIncludeDecorations = includeDecorations;
0712     m_lastIncludeShadow = includeWindowShadow;
0713 
0714     if ((timeout < 0 || !m_imagePlatform->supportedShutterModes().testFlag(ImagePlatform::Immediate))
0715         && m_imagePlatform->supportedShutterModes().testFlag(ImagePlatform::OnClick)
0716     ) {
0717         SpectacleWindow::setVisibilityForAll(QWindow::Hidden);
0718         m_imagePlatform->doGrab(ImagePlatform::ShutterMode::OnClick, m_lastGrabMode, m_lastIncludePointer, m_lastIncludeDecorations, m_lastIncludeShadow);
0719         return;
0720     }
0721 
0722     const bool noDelay = timeout == 0;
0723 
0724     if (PlasmaVersion::get() < PlasmaVersion::check(5, 27, 4) && KX11Extras::compositingActive()) {
0725         // when compositing is enabled, we need to give it enough time for the window
0726         // to disappear and all the effects are complete before we take the shot. there's
0727         // no way of knowing how long the disappearing effects take, but as per default
0728         // settings (and unless the user has set an extremely slow effect), 200
0729         // milliseconds is a good amount of wait time.
0730         timeout = qMax(timeout, 200);
0731     } else if (m_imagePlatform->inherits("PlatformXcb")) {
0732         // Minimum 50ms delay to prevent segfaults from xcb function calls
0733         // that don't get replies fast enough.
0734         timeout = qMax(timeout, 50);
0735     }
0736 
0737     if (noDelay) {
0738         SpectacleWindow::setVisibilityForAll(QWindow::Hidden);
0739         QTimer::singleShot(timeout, this, [this]() {
0740             m_imagePlatform->doGrab(ImagePlatform::ShutterMode::Immediate, m_lastGrabMode, m_lastIncludePointer, m_lastIncludeDecorations, m_lastIncludeShadow);
0741         });
0742         return;
0743     }
0744 
0745     m_delayAnimation->setDuration(timeout);
0746     m_delayAnimation->start();
0747 
0748     // skip minimize animation.
0749     SpectacleWindow::setVisibilityForAll(QWindow::Hidden);
0750     SpectacleWindow::setVisibilityForAll(QWindow::Minimized);
0751 }
0752 
0753 void SpectacleCore::takeNewScreenshot(int captureMode, int timeout, bool includePointer, bool includeDecorations, bool includeShadow)
0754 {
0755     using CaptureMode = CaptureModeModel::CaptureMode;
0756     takeNewScreenshot(toGrabMode(CaptureMode(captureMode), Settings::transientOnly()), timeout, includePointer, includeDecorations, includeShadow);
0757 }
0758 
0759 void SpectacleCore::cancelScreenshot()
0760 {
0761     if (m_startMode != StartMode::Gui) {
0762         Q_EMIT allDone();
0763         return;
0764     }
0765 
0766     int currentTime = m_delayAnimation->currentTime();
0767     m_delayAnimation->stop();
0768     if (currentTime > 0) {
0769         SpectacleWindow::setTitleForAll(SpectacleWindow::Previous);
0770     } else if (!ViewerWindow::instance()) {
0771         initViewerWindow(ViewerWindow::Image);
0772         ViewerWindow::instance()->setVisible(true);
0773     } else if (ViewerWindow::instance()) {
0774         Q_EMIT allDone();
0775     }
0776 }
0777 
0778 void SpectacleCore::showErrorMessage(const QString &message)
0779 {
0780     qCDebug(SPECTACLE_CORE_LOG) << "ERROR: " << message;
0781 
0782     if (m_startMode == StartMode::Gui) {
0783         KMessageBox::error(nullptr, message);
0784     }
0785 }
0786 
0787 void SpectacleCore::showViewerIfGuiMode(bool minimized)
0788 {
0789     if (m_startMode != StartMode::Gui) {
0790         return;
0791     }
0792     initViewerWindow(ViewerWindow::Image);
0793     if (!m_videoMode && m_cliOptions[CommandLineOptions::EditExisting]) {
0794         ViewerWindow::instance()->setAnnotating(true);
0795     }
0796     if (minimized) {
0797         ViewerWindow::instance()->showMinimized();
0798     } else {
0799         ViewerWindow::instance()->setVisible(true);
0800     }
0801 }
0802 
0803 void SpectacleCore::onScreenshotFailed()
0804 {
0805     switch (m_startMode) {
0806     case StartMode::Background:
0807         showErrorMessage(i18n("Screenshot capture canceled or failed"));
0808         Q_EMIT allDone();
0809         return;
0810     case StartMode::DBus:
0811         Q_EMIT dbusScreenshotFailed();
0812         Q_EMIT allDone();
0813         return;
0814     case StartMode::Gui:
0815         if (!ViewerWindow::instance()) {
0816             initViewerWindow(ViewerWindow::Dialog);
0817         }
0818         ViewerWindow::instance()->showScreenshotFailedMessage();
0819         return;
0820     }
0821 }
0822 
0823 static QList<KNotification *> notifications;
0824 
0825 void SpectacleCore::doNotify(ScreenCapture type, const ExportManager::Actions &actions, const QUrl &saveUrl)
0826 {
0827     if (m_cliOptions[CommandLineOptions::NoNotify]) {
0828         return;
0829     }
0830 
0831     // ensure program stays alive until the notification finishes.
0832     if (!m_eventLoopLocker) {
0833         m_eventLoopLocker = std::make_unique<QEventLoopLocker>();
0834     }
0835 
0836     KNotification *notification = nullptr;
0837     QString title;
0838     if (type == ScreenCapture::Screenshot) {
0839         notification = new KNotification(u"newScreenshotSaved"_s, KNotification::CloseOnTimeout, this);
0840         int index = captureModeModel()->indexOfCaptureMode(toCaptureMode(m_lastGrabMode));
0841         title = captureModeModel()->data(captureModeModel()->index(index), Qt::DisplayRole).toString();
0842     } else {
0843         notification = new KNotification(u"recordingSaved"_s, KNotification::CloseOnTimeout, this);
0844         int index = m_recordingModeModel->indexOfRecordingMode(m_lastRecordingMode);
0845         title = m_recordingModeModel->data(m_recordingModeModel->index(index), Qt::DisplayRole).toString();
0846     }
0847     notification->setTitle(title);
0848 
0849     notifications.append(notification);
0850 
0851     // a speaking message is prettier than a URL, special case for copy image/location to clipboard and the default pictures location
0852     const QString &saveDirPath = saveUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path();
0853     const QString &saveFileName = saveUrl.fileName();
0854 
0855     using Action = ExportManager::Action;
0856     if (type == ScreenCapture::Screenshot) {
0857         if (actions & Action::AnySave && !saveFileName.isEmpty()) {
0858             if (actions & Action::CopyPath) {
0859                 notification->setText(i18n("A screenshot was saved as '%1' to '%2' and the file path of the screenshot has been saved to your clipboard.",
0860                                         saveFileName, saveDirPath));
0861             } else if (saveDirPath == QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)) {
0862                 notification->setText(i18nc("Placeholder is filename",
0863                                             "A screenshot was saved as '%1' to your Pictures folder.",
0864                                             saveFileName));
0865             } else {
0866                 notification->setText(i18n("A screenshot was saved as '%1' to '%2'.",
0867                                         saveFileName, saveDirPath));
0868             }
0869         } else if (actions & Action::CopyImage) {
0870             notification->setText(i18n("A screenshot was saved to your clipboard."));
0871         }
0872     } else if (type == ScreenCapture::Recording && actions & Action::AnySave && !saveFileName.isEmpty()) {
0873         if (actions & Action::CopyPath) {
0874             notification->setText(
0875                 i18n("A recording was saved as '%1' to '%2' and the file path of the recording has been saved to your clipboard.", saveFileName, saveDirPath));
0876         } else if (saveDirPath == QStandardPaths::writableLocation(QStandardPaths::MoviesLocation)) {
0877             notification->setText(i18nc("Placeholder is filename", "A recording was saved as '%1' to your Videos folder.", saveFileName));
0878         } else {
0879             notification->setText(i18n("A recording was saved as '%1' to '%2'.", saveFileName, saveDirPath));
0880         }
0881     }
0882 
0883     if (!saveUrl.isEmpty()) {
0884         notification->setUrls({saveUrl});
0885 
0886         auto open = [saveUrl]() {
0887             auto job = new KIO::OpenUrlJob(saveUrl);
0888             job->start();
0889         };
0890         auto defaultAction = notification->addDefaultAction(i18nc("Open the screenshot we just saved", "Open"));
0891         connect(defaultAction, &KNotificationAction::activated, this, open);
0892 
0893         if (type == ScreenCapture::Screenshot) {
0894             auto annotate = [saveUrl]() {
0895                 QProcess newInstance;
0896                 newInstance.setProgram(QCoreApplication::applicationFilePath());
0897                 newInstance.setArguments({
0898                     CommandLineOptions::toArgument(CommandLineOptions::self()->newInstance),
0899                     CommandLineOptions::toArgument(CommandLineOptions::self()->editExisting),
0900                     saveUrl.toLocalFile()
0901                 });
0902                 newInstance.startDetached();
0903             };
0904             auto annotateAction = notification->addAction(i18n("Annotate"));
0905             connect(annotateAction, &KNotificationAction::activated, this, annotate);
0906         }
0907     }
0908 
0909     connect(notification, &QObject::destroyed, this, [this](QObject *notification) {
0910         notifications.removeOne(static_cast<KNotification *>(notification));
0911         // When there are no more notifications running, we can remove the loop locker.
0912         if (notifications.empty()) {
0913             QTimer::singleShot(250, this, [this] {
0914                 m_eventLoopLocker.reset();
0915             });
0916         }
0917     });
0918 
0919     notification->sendEvent();
0920 }
0921 
0922 ExportManager::Actions SpectacleCore::autoExportActions() const
0923 {
0924     using Action = ExportManager::Action;
0925     using Option = CommandLineOptions::Option;
0926     bool save = (m_startMode != StartMode::Gui && m_cliOptions[Option::Output]) || m_videoMode;
0927     bool copyImage = m_cliOptions[Option::CopyImage] && !m_videoMode;
0928     bool copyPath = m_cliOptions[Option::CopyPath];
0929     ExportManager::Actions actions;
0930     if (m_startMode != StartMode::Background) {
0931         save |= Settings::autoSaveImage();
0932         copyImage |= Settings::clipboardGroup() == Settings::PostScreenshotCopyImage && !m_videoMode;
0933         copyPath |= Settings::clipboardGroup() == Settings::PostScreenshotCopyLocation;
0934     }
0935     if (m_startMode == StartMode::Gui) {
0936         actions.setFlag(Action::Save, save);
0937         actions.setFlag(Action::CopyImage, copyImage);
0938     } else {
0939         // In background and dbus mode, ensure that either save or copy image is enabled.
0940         actions.setFlag(Action::Save, save || !copyImage);
0941         actions.setFlag(Action::CopyImage, !actions.testFlag(Action::Save) || copyImage);
0942     }
0943     actions.setFlag(Action::CopyPath, actions.testFlag(Action::Save) && copyPath);
0944     return actions;
0945 }
0946 
0947 ImagePlatform::GrabMode SpectacleCore::toGrabMode(CaptureModeModel::CaptureMode captureMode, bool transientOnly) const
0948 {
0949     using GrabMode = ImagePlatform::GrabMode;
0950     using CaptureMode = CaptureModeModel::CaptureMode;
0951     const auto &supportedGrabModes = m_imagePlatform->supportedGrabModes();
0952     if (captureMode == CaptureMode::CurrentScreen
0953         && supportedGrabModes.testFlag(ImagePlatform::CurrentScreen)) {
0954         return GrabMode::CurrentScreen;
0955     } else if (captureMode == CaptureMode::ActiveWindow
0956         && supportedGrabModes.testFlag(ImagePlatform::ActiveWindow)) {
0957         return GrabMode::ActiveWindow;
0958     } else if (captureMode == CaptureMode::WindowUnderCursor
0959         && supportedGrabModes.testFlag(ImagePlatform::WindowUnderCursor)) {
0960         // TODO: Improve API for transientOnly or make it obsolete.
0961         if (transientOnly || !supportedGrabModes.testFlag(ImagePlatform::TransientWithParent)) {
0962             return GrabMode::WindowUnderCursor;
0963         } else {
0964             return GrabMode::TransientWithParent;
0965         }
0966     } else if (captureMode == CaptureMode::RectangularRegion
0967         && supportedGrabModes.testFlag(ImagePlatform::PerScreenImageNative)) {
0968         return GrabMode::PerScreenImageNative;
0969     } else if (captureMode == CaptureMode::AllScreensScaled
0970         && supportedGrabModes.testFlag(ImagePlatform::AllScreensScaled)) {
0971         return GrabMode::AllScreensScaled;
0972     } else if (supportedGrabModes.testFlag(ImagePlatform::AllScreens)) { // default if supported
0973         return GrabMode::AllScreens;
0974     } else {
0975         return GrabMode::NoGrabModes;
0976     }
0977 }
0978 
0979 CaptureModeModel::CaptureMode SpectacleCore::toCaptureMode(ImagePlatform::GrabMode grabMode) const
0980 {
0981     using GrabMode = ImagePlatform::GrabMode;
0982     using CaptureMode = CaptureModeModel::CaptureMode;
0983     if (grabMode == GrabMode::CurrentScreen) {
0984         return CaptureMode::CurrentScreen;
0985     } else if (grabMode == GrabMode::ActiveWindow) {
0986         return CaptureMode::ActiveWindow;
0987     } else if (grabMode == GrabMode::WindowUnderCursor) {
0988         return CaptureMode::WindowUnderCursor;
0989     } else if (grabMode == GrabMode::PerScreenImageNative) {
0990         return CaptureMode::RectangularRegion;
0991     } else if (grabMode == GrabMode::AllScreensScaled) {
0992         return CaptureMode::AllScreensScaled;
0993     } else {
0994         return CaptureMode::AllScreens;
0995     }
0996 }
0997 
0998 bool SpectacleCore::isGuiNull() const
0999 {
1000     return SpectacleWindow::instances().isEmpty();
1001 }
1002 
1003 void SpectacleCore::initGuiNoScreenshot()
1004 {
1005     initViewerWindow(ViewerWindow::Dialog);
1006     ViewerWindow::instance()->setVisible(true);
1007 }
1008 
1009 // Hurry up the sync if the sync timer is active.
1010 void SpectacleCore::syncExportImage()
1011 {
1012     if (!m_annotationSyncTimer->isActive()) {
1013         return;
1014     }
1015     setExportImage(m_annotationDocument->renderToImage());
1016 }
1017 
1018 // A convenient way to stop the sync timer and set the export image.
1019 void SpectacleCore::setExportImage(const QImage &image)
1020 {
1021     m_annotationSyncTimer->stop();
1022     ExportManager::instance()->setImage(image);
1023 }
1024 
1025 QQmlEngine *SpectacleCore::getQmlEngine()
1026 {
1027     if (m_engine == nullptr) {
1028         m_engine = std::make_unique<QQmlEngine>(this);
1029         m_engine->rootContext()->setContextObject(new KLocalizedContext(m_engine.get()));
1030 
1031         qmlRegisterSingletonInstance(SPECTACLE_QML_URI, 1, 0, "SpectacleCore", this);
1032         qmlRegisterSingletonInstance(SPECTACLE_QML_URI, 1, 0, "ImagePlatform", m_imagePlatform.get());
1033         qmlRegisterSingletonInstance(SPECTACLE_QML_URI, 1, 0, "VideoPlatform", m_videoPlatform.get());
1034         qmlRegisterSingletonInstance(SPECTACLE_QML_URI, 1, 0, "Settings", Settings::self());
1035         qmlRegisterSingletonInstance(SPECTACLE_QML_URI, 1, 0, "CaptureModeModel", m_captureModeModel.get());
1036         qmlRegisterSingletonInstance(SPECTACLE_QML_URI, 1, 0, "SelectionEditor", SelectionEditor::instance());
1037         qmlRegisterSingletonInstance(SPECTACLE_QML_URI, 1, 0, "Selection", SelectionEditor::instance()->selection());
1038         qmlRegisterSingletonInstance(SPECTACLE_QML_URI, 1, 0, "Geometry", Geometry::instance());
1039         qmlRegisterSingletonInstance(SPECTACLE_QML_URI, 1, 0, "G", Geometry::instance());
1040         qmlRegisterSingletonInstance(SPECTACLE_QML_URI, 1, 0, "ExportMenu", ExportMenu::instance());
1041         qmlRegisterSingletonInstance(SPECTACLE_QML_URI, 1, 0, "HelpMenu", HelpMenu::instance());
1042         qmlRegisterSingletonInstance(SPECTACLE_QML_URI, 1, 0, "OptionsMenu", OptionsMenu::instance());
1043 
1044         qmlRegisterSingletonInstance(SPECTACLE_QML_URI, 1, 0, "AnnotationDocument", m_annotationDocument.get());
1045         qmlRegisterUncreatableType<AnnotationTool>(SPECTACLE_QML_URI, 1, 0, "AnnotationTool",
1046                                                    u"Use AnnotationDocument.tool"_s);
1047         qmlRegisterUncreatableType<SelectedActionWrapper>(SPECTACLE_QML_URI, 1, 0, "SelectedAction",
1048                                                           u"Use AnnotationDocument.selectedAction"_s);
1049         qmlRegisterType<AnnotationViewport>(SPECTACLE_QML_URI, 1, 0, "AnnotationViewport");
1050         qmlRegisterUncreatableType<QScreen>(SPECTACLE_QML_URI, 1, 0, "QScreen",
1051                                             u"Only created by Qt"_s);
1052     }
1053     return m_engine.get();
1054 }
1055 
1056 void SpectacleCore::initCaptureWindows(CaptureWindow::Mode mode)
1057 {
1058     deleteWindows();
1059 
1060     if (mode == CaptureWindow::Video) {
1061         LayerShellQt::Shell::useLayerShell();
1062     }
1063 
1064     // Allow the window to be transparent. Used for video recording UI.
1065     // It has to be set before creating the window.
1066     QQuickWindow::setDefaultAlphaBuffer(true);
1067 
1068     auto engine = getQmlEngine();
1069     const auto screens = qApp->screens();
1070     for (auto *screen : screens) {
1071         m_captureWindows.emplace_back(CaptureWindow::makeUnique(mode, screen, engine));
1072     }
1073 }
1074 
1075 void SpectacleCore::initViewerWindow(ViewerWindow::Mode mode)
1076 {
1077     // always switch to gui mode when a viewer window is used.
1078     m_startMode = SpectacleCore::StartMode::Gui;
1079     deleteWindows();
1080 
1081     // Transparency isn't needed for this window.
1082     QQuickWindow::setDefaultAlphaBuffer(false);
1083 
1084     m_viewerWindow = ViewerWindow::makeUnique(mode, getQmlEngine());
1085 }
1086 
1087 void SpectacleCore::deleteWindows()
1088 {
1089     m_viewerWindow.reset();
1090     m_captureWindows.clear();
1091 }
1092 
1093 void SpectacleCore::unityLauncherUpdate(const QVariantMap &properties) const
1094 {
1095     QDBusMessage message = QDBusMessage::createSignal(u"/org/kde/Spectacle"_s,
1096                                                       u"com.canonical.Unity.LauncherEntry"_s,
1097                                                       u"Update"_s);
1098     message.setArguments({QApplication::desktopFileName(), properties});
1099     QDBusConnection::sessionBus().send(message);
1100 }
1101 
1102 void SpectacleCore::startRecording(VideoPlatform::RecordingMode mode, bool withPointer)
1103 {
1104     if (m_videoPlatform->isRecording() || mode == VideoPlatform::NoRecordingModes) {
1105         return;
1106     }
1107     m_lastRecordingMode = mode;
1108     setVideoMode(true);
1109     if (mode == VideoPlatform::Region) {
1110         SelectionEditor::instance()->reset();
1111         initCaptureWindows(CaptureWindow::Video);
1112         SpectacleWindow::setTitleForAll(SpectacleWindow::Unsaved);
1113         SpectacleWindow::setVisibilityForAll(QWindow::FullScreen);
1114     } else {
1115         const auto &output = m_outputUrl.isLocalFile() ? videoOutputUrl() : QUrl();
1116         m_videoPlatform->startRecording(output, mode, {}, withPointer);
1117     }
1118 }
1119 
1120 void SpectacleCore::finishRecording()
1121 {
1122     Q_ASSERT(m_videoPlatform->isRecording());
1123     m_videoPlatform->finishRecording();
1124 }
1125 
1126 bool SpectacleCore::videoMode() const
1127 {
1128     return m_videoMode;
1129 }
1130 
1131 void SpectacleCore::setVideoMode(bool videoMode)
1132 {
1133     if (videoMode == m_videoMode) {
1134         return;
1135     }
1136     m_videoMode = videoMode;
1137     Q_EMIT videoModeChanged(videoMode);
1138 }
1139 
1140 QUrl SpectacleCore::currentVideo() const
1141 {
1142     return m_currentVideo;
1143 }
1144 
1145 void SpectacleCore::setCurrentVideo(const QUrl &currentVideo)
1146 {
1147     if (currentVideo == m_currentVideo) {
1148         return;
1149     }
1150     m_currentVideo = currentVideo;
1151     Q_EMIT currentVideoChanged(currentVideo);
1152 }
1153 
1154 QUrl SpectacleCore::videoOutputUrl() const
1155 {
1156     return VideoPlatform::formatForPath(m_outputUrl.path()) != VideoPlatform::NoFormat ? m_outputUrl : QUrl();
1157 }
1158 
1159 QString SpectacleCore::recordedTime() const
1160 {
1161     return timeFromMilliseconds(m_videoPlatform->recordedTime());
1162 }
1163 
1164 QString SpectacleCore::timeFromMilliseconds(qint64 milliseconds) const
1165 {
1166     KFormat::DurationFormatOptions options = KFormat::DefaultDuration;
1167     if (milliseconds < 1000.0 * 60.0 * 60.0) {
1168         options |= KFormat::FoldHours;
1169     }
1170     return KFormat().formatDuration(milliseconds, options);
1171 }
1172 
1173 void SpectacleCore::activateAction(const QString &actionName, const QVariant &parameter)
1174 {
1175     Q_UNUSED(parameter)
1176     m_startMode = StartMode::DBus;
1177     if (actionName == ShortcutActions::self()->fullScreenAction()->objectName()) {
1178         takeNewScreenshot(CaptureModeModel::AllScreens, 0);
1179     } else if (actionName == ShortcutActions::self()->currentScreenAction()->objectName()) {
1180         takeNewScreenshot(CaptureModeModel::CurrentScreen, 0);
1181     } else if (actionName == ShortcutActions::self()->activeWindowAction()->objectName()) {
1182         takeNewScreenshot(CaptureModeModel::ActiveWindow, 0);
1183     } else if (actionName == ShortcutActions::self()->windowUnderCursorAction()->objectName()) {
1184         takeNewScreenshot(CaptureModeModel::WindowUnderCursor, 0);
1185     } else if (actionName == ShortcutActions::self()->regionAction()->objectName()) {
1186         takeNewScreenshot(CaptureModeModel::RectangularRegion, 0);
1187     } else if (actionName == ShortcutActions::self()->recordRegionAction()->objectName()) {
1188         if (!m_videoPlatform->isRecording()) {
1189             startRecording(VideoPlatform::Region);
1190         } else {
1191             finishRecording();
1192         }
1193     } else if (actionName == ShortcutActions::self()->recordScreenAction()->objectName()) {
1194         if (!m_videoPlatform->isRecording()) {
1195             startRecording(VideoPlatform::Screen);
1196         } else {
1197             finishRecording();
1198         }
1199     } else if (actionName == ShortcutActions::self()->recordWindowAction()->objectName()) {
1200         if (!m_videoPlatform->isRecording()) {
1201             startRecording(VideoPlatform::Window);
1202         } else {
1203             finishRecording();
1204         }
1205     } else if (actionName == ShortcutActions::self()->openWithoutScreenshotAction()->objectName()) {
1206         initGuiNoScreenshot();
1207     }
1208 }
1209 
1210 #include "moc_SpectacleCore.cpp"