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