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 ¤tVideo) 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 ¶meter) 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"