File indexing completed on 2024-05-05 04:34:54

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