1 /* This file is part of Spectacle, the KDE screenshot utility
2  * SPDX-FileCopyrightText: 2019 David Redondo <kde@david-redondo.de>
3  * SPDX-FileCopyrightText: 2015 Boudhayan Gupta <bgupta@kde.org>
4  * SPDX-License-Identifier: LGPL-2.0-or-later
5  */
6 
7 #include "KSMainWindow.h"
8 
9 #include "SettingsDialog/GeneralOptionsPage.h"
10 #include "SettingsDialog/SaveOptionsPage.h"
11 #include "SettingsDialog/SettingsDialog.h"
12 #include "SettingsDialog/ShortcutsOptionsPage.h"
13 #include "settings.h"
14 
15 #include <QApplication>
16 #include <QClipboard>
17 #include <QDBusConnection>
18 #include <QDBusMessage>
19 #include <QDesktopServices>
20 #include <QKeyEvent>
21 #include <QPainter>
22 #include <QPrintDialog>
23 #include <QPushButton>
24 #include <QTimer>
25 #include <QVBoxLayout>
26 #include <QVariantAnimation>
27 #include <QtMath>
28 
29 #ifdef XCB_FOUND
30 #include <QX11Info>
31 #include <xcb/xcb.h>
32 #endif
33 
34 #include <KAboutData>
35 #include <KConfigGroup>
36 #include <KGuiItem>
37 #include <KHelpMenu>
38 #include <KIO/JobUiDelegate>
39 #include <KIO/OpenFileManagerWindowJob>
40 #include <KIO/OpenUrlJob>
41 #include <KLocalizedString>
42 #include <KSharedConfig>
43 #include <KStandardAction>
44 #include <KWindowSystem>
45 
46 static const int DEFAULT_WINDOW_HEIGHT = 420;
47 static const int DEFAULT_WINDOW_WIDTH = 840;
48 static const int MAXIMUM_WINDOW_WIDTH = 1000;
49 
KSMainWindow(Platform::GrabModes theGrabModes,Platform::ShutterModes theShutterModes,QWidget * parent)50 KSMainWindow::KSMainWindow(Platform::GrabModes theGrabModes, Platform::ShutterModes theShutterModes, QWidget *parent)
51     : QDialog(parent)
52     , mKSWidget(new KSWidget(theGrabModes, this))
53     , mDivider(new QFrame(this))
54     , mDialogButtonBox(new QDialogButtonBox(this))
55     , mConfigureButton(new QToolButton(this))
56     , mToolsButton(new QPushButton(this))
57     , mSendToButton(new QPushButton(this))
58     , mClipboardButton(new QToolButton(this))
59     , mClipboardMenu(new QMenu(this))
60     , mClipboardLocationAction(new QAction(this))
61     , mClipboardImageAction(new QAction(this))
62     , mSaveButton(new QToolButton(this))
63     , mSaveMenu(new QMenu(this))
64     , mSaveAsAction(new QAction(this))
65     , mSaveAction(new QAction(this))
66     , mMessageWidget(new KMessageWidget(this))
67     , mToolsMenu(new QMenu(this))
68     , mScreenRecorderToolsMenu(new QMenu(this))
69     , mExportMenu(new ExportMenu(this))
70     , mShutterModes(theShutterModes)
71 #ifdef KIMAGEANNOTATOR_FOUND
72     , mAnnotateButton(new QToolButton(this))
73     , mAnnotatorActive(false)
74 #endif
75 {
76     // before we do anything, we need to set a window property
77     // that skips the close/hide window animation on kwin. this
78     // fixes a ghost image of the spectacle window that appears
79     // on subsequent screenshots taken with the take new screenshot
80     // button
81     //
82     // credits for this goes to Thomas Lübking <thomas.luebking@gmail.com>
83 
84 #ifdef XCB_FOUND
85     if (KWindowSystem::isPlatformX11()) {
86         // create a window if we haven't already. note that the QWidget constructor
87         // should already have done this
88 
89         if (winId() == 0) {
90             create(0, true, true);
91         }
92 
93         // do the xcb shenanigans
94         xcb_connection_t *xcbConn = QX11Info::connection();
95         const QByteArray effectName = QByteArrayLiteral("_KDE_NET_WM_SKIP_CLOSE_ANIMATION");
96 
97         xcb_intern_atom_cookie_t atomCookie = xcb_intern_atom_unchecked(xcbConn, false, effectName.length(), effectName.constData());
98         QScopedPointer<xcb_intern_atom_reply_t, QScopedPointerPodDeleter> atom(xcb_intern_atom_reply(xcbConn, atomCookie, nullptr));
99         if (atom.isNull()) {
100             goto done;
101         }
102 
103         uint32_t value = 1;
104         xcb_change_property(xcbConn, XCB_PROP_MODE_REPLACE, winId(), atom->atom, XCB_ATOM_CARDINAL, 32, 1, &value);
105     }
106 done:
107 #endif
108 
109     QMetaObject::invokeMethod(this, "init", Qt::QueuedConnection);
110 }
111 
112 // GUI init
113 
init()114 void KSMainWindow::init()
115 {
116     KSharedConfigPtr config = KSharedConfig::openConfig(QStringLiteral("spectaclerc"));
117     KConfigGroup guiConfig(config, "GuiConfig");
118 
119     QPoint location = guiConfig.readEntry("window-position", QPoint(50, 50));
120     move(location);
121 
122     // change window title on save and on autosave
123 
124     connect(ExportManager::instance(), &ExportManager::imageSaved, this, &KSMainWindow::imageSaved);
125     connect(ExportManager::instance(), &ExportManager::imageCopied, this, &KSMainWindow::imageCopied);
126     connect(ExportManager::instance(), &ExportManager::imageLocationCopied, this, &KSMainWindow::imageSavedAndLocationCopied);
127     connect(ExportManager::instance(), &ExportManager::imageSavedAndCopied, this, &KSMainWindow::imageSavedAndCopied);
128 
129     // the KSGWidget
130 
131     connect(mKSWidget, &KSWidget::newScreenshotRequest, this, &KSMainWindow::captureScreenshot);
132     connect(mKSWidget, &KSWidget::dragInitiated, this, &KSMainWindow::dragAndDropRequest);
133 
134     // the Button Bar
135 
136     mDialogButtonBox->setStandardButtons(QDialogButtonBox::Help);
137     mDialogButtonBox->button(QDialogButtonBox::Help)->setAutoDefault(false);
138 
139     mConfigureButton->setDefaultAction(KStandardAction::preferences(this, SLOT(showPreferencesDialog()), this));
140     mConfigureButton->setText(i18n("Configure..."));
141     mConfigureButton->setToolTip(i18n("Change Spectacle's settings."));
142     mConfigureButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
143     mDialogButtonBox->addButton(mConfigureButton, QDialogButtonBox::ResetRole);
144 
145 #ifdef KIMAGEANNOTATOR_FOUND
146     mAnnotateButton->setText(i18n("Annotate"));
147     mAnnotateButton->setToolTip(i18n("Add annotation to the screenshot"));
148     mAnnotateButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
149     mAnnotateButton->setIcon(QIcon::fromTheme(QStringLiteral("document-edit")));
150     connect(mAnnotateButton, &QToolButton::clicked, this, [this] {
151         if (mAnnotatorActive) {
152             mKSWidget->hideAnnotator();
153             mAnnotateButton->setText(i18n("Annotate"));
154         } else {
155             mKSWidget->showAnnotator();
156             mAnnotateButton->setText(i18n("Annotation done"));
157         }
158 
159         mToolsButton->setEnabled(mAnnotatorActive);
160         mSendToButton->setEnabled(mAnnotatorActive);
161         mClipboardButton->setEnabled(mAnnotatorActive);
162         mSaveButton->setEnabled(mAnnotatorActive);
163 
164         mAnnotatorActive = !mAnnotatorActive;
165     });
166 
167     mDialogButtonBox->addButton(mAnnotateButton, QDialogButtonBox::ActionRole);
168 #endif
169 
170     KGuiItem::assign(mToolsButton, KGuiItem(i18n("Tools")));
171     mToolsButton->setIcon(QIcon::fromTheme(QStringLiteral("tools"), QIcon::fromTheme(QStringLiteral("application-menu"))));
172     mToolsButton->setAutoDefault(false);
173     mDialogButtonBox->addButton(mToolsButton, QDialogButtonBox::ActionRole);
174     mToolsButton->setMenu(mToolsMenu);
175 
176     KGuiItem::assign(mSendToButton, KGuiItem(i18n("Export")));
177     mSendToButton->setIcon(QIcon::fromTheme(QStringLiteral("document-share")));
178     mSendToButton->setAutoDefault(false);
179     mDialogButtonBox->addButton(mSendToButton, QDialogButtonBox::ActionRole);
180 
181     mClipboardButton->setMenu(mClipboardMenu);
182     mClipboardButton->setPopupMode(QToolButton::MenuButtonPopup);
183     mClipboardButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
184     mDialogButtonBox->addButton(mClipboardButton, QDialogButtonBox::ActionRole);
185 
186     mSaveButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
187     mSaveButton->setMenu(mSaveMenu);
188     mSaveButton->setPopupMode(QToolButton::MenuButtonPopup);
189     mDialogButtonBox->addButton(mSaveButton, QDialogButtonBox::ActionRole);
190 
191     // the help menu
192     KHelpMenu *helpMenu = new KHelpMenu(this, KAboutData::applicationData(), true);
193     mDialogButtonBox->button(QDialogButtonBox::Help)->setMenu(helpMenu->menu());
194 
195     // the tools menu
196     mToolsMenu->addAction(QIcon::fromTheme(QStringLiteral("document-open-folder")),
197                           i18n("Open Default Screenshots Folder"),
198                           this,
199                           &KSMainWindow::openScreenshotsFolder);
200     mToolsMenu->addAction(KStandardAction::print(this, &KSMainWindow::showPrintDialog, this));
201     mScreenRecorderToolsMenu = mToolsMenu->addMenu(i18n("Record Screen"));
202     mScreenRecorderToolsMenu->setIcon(QIcon::fromTheme(QStringLiteral("media-record")));
203     connect(mScreenRecorderToolsMenu, &QMenu::aboutToShow, this, [this]() {
204         KMoreToolsMenuFactory *moreToolsMenuFactory = new KMoreToolsMenuFactory(QStringLiteral("spectacle/screenrecorder-tools"));
205         moreToolsMenuFactory->setParentWidget(this);
206         mScreenrecorderToolsMenuFactory.reset(moreToolsMenuFactory);
207         mScreenRecorderToolsMenu->clear();
208         mScreenrecorderToolsMenuFactory->fillMenuFromGroupingNames(mScreenRecorderToolsMenu, {QStringLiteral("screenrecorder")});
209     });
210 
211     // the save menu
212     mSaveAsAction = KStandardAction::saveAs(this, &KSMainWindow::saveAs, this);
213     mSaveAction = KStandardAction::save(this, &KSMainWindow::save, this);
214     mSaveMenu->addAction(mSaveAsAction);
215     mSaveMenu->addAction(mSaveAction);
216     setDefaultSaveAction();
217 
218     // the clipboard menu
219     mClipboardImageAction = KStandardAction::copy(this, &KSMainWindow::copyImage, this);
220     mClipboardImageAction->setText(i18n("Copy Image to Clipboard"));
221     mClipboardLocationAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy Location to Clipboard"), this);
222     connect(mClipboardLocationAction, &QAction::triggered, this, &KSMainWindow::copyLocation);
223     mClipboardMenu->addAction(mClipboardImageAction);
224     mClipboardMenu->addAction(mClipboardLocationAction);
225     setDefaultCopyAction();
226 
227     // message widget
228     connect(mMessageWidget, &KMessageWidget::linkActivated, this, [](const QString &str) {
229         QDesktopServices::openUrl(QUrl(str));
230     });
231 
232     // layouts
233     mDivider->setFrameShape(QFrame::HLine);
234     mDivider->setLineWidth(2);
235 
236     QVBoxLayout *layout = new QVBoxLayout(this);
237     layout->addWidget(mKSWidget);
238     layout->addWidget(mMessageWidget);
239     layout->addWidget(mDivider);
240     layout->addWidget(mDialogButtonBox);
241     mMessageWidget->hide();
242 
243     // populate our send-to actions
244     mSendToButton->setMenu(mExportMenu);
245     connect(mExportMenu, &ExportMenu::imageShared, this, &KSMainWindow::showImageSharedFeedback);
246 
247     // lock down the onClick mode depending on available shutter modes
248     if (!mShutterModes.testFlag(Platform::ShutterMode::OnClick)) {
249         mKSWidget->lockOnClickDisabled();
250     } else if (!mShutterModes.testFlag(Platform::ShutterMode::Immediate)) {
251         mKSWidget->lockOnClickEnabled();
252     }
253 
254     // Allow Ctrl+Q to quit the app
255     QAction *actionQuit = KStandardAction::quit(qApp, &QApplication::quit, this);
256     actionQuit->setShortcut(QKeySequence::Quit);
257     addAction(actionQuit);
258 
259     // message: open containing folder
260     mOpenContaining = new QAction(QIcon::fromTheme(QStringLiteral("document-open-folder")), i18n("Open Containing Folder"), mMessageWidget);
261     connect(mOpenContaining, &QAction::triggered, [=] {
262         QUrl imageUrl;
263         if (ExportManager::instance()->isImageSavedNotInTemp()) {
264             imageUrl = Settings::lastSaveLocation();
265         } else {
266             imageUrl = ExportManager::instance()->tempSave();
267         }
268 
269         KIO::highlightInFileManager({imageUrl});
270     });
271 
272     mHideMessageWidgetTimer = new QTimer(this);
273     //     connect(mHideMessageWidgetTimer, &QTimer::timeout,
274     //             mMessageWidget, &KMessageWidget::animatedHide);
275     mHideMessageWidgetTimer->setInterval(10000);
276     // done with the init
277 }
278 
sizeHint() const279 QSize KSMainWindow::sizeHint() const
280 {
281     return QSize(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT).expandedTo(minimumSize());
282 }
283 
windowWidth(const QPixmap & pixmap) const284 int KSMainWindow::windowWidth(const QPixmap &pixmap) const
285 {
286     // Calculates what the width of the window should be for the captured image to perfectly fit
287     // the area reserved for the image, with the height already set.
288 
289     const float pixmapAspectRatio = (float)pixmap.width() / pixmap.height();
290     const int imageHeight = mKSWidget->height() - 2 * layout()->spacing();
291     const int imageWidth = pixmapAspectRatio * imageHeight;
292 
293     int alignedWindowWidth = qMin(mKSWidget->imagePaddingWidth() + imageWidth, MAXIMUM_WINDOW_WIDTH);
294     alignedWindowWidth += layout()->contentsMargins().left() + layout()->contentsMargins().right();
295     alignedWindowWidth += 2; // margins is removing 1 - 1 pixel for some reason
296     return alignedWindowWidth;
297 }
298 
setDefaultSaveAction()299 void KSMainWindow::setDefaultSaveAction()
300 {
301     switch (Settings::lastUsedSaveMode()) {
302     case Settings::SaveAs:
303         mSaveButton->setDefaultAction(mSaveAsAction);
304         mSaveButton->setText(i18n("Save As..."));
305         break;
306     case Settings::Save:
307         mSaveButton->setDefaultAction(mSaveAction);
308         break;
309     }
310     mSaveButton->setEnabled(mPixmapExists);
311 }
312 
setDefaultCopyAction()313 void KSMainWindow::setDefaultCopyAction()
314 {
315     switch (Settings::lastUsedCopyMode()) {
316     case Settings::CopyImage:
317         mClipboardButton->setText(i18n("Copy Image to Clipboard"));
318         mClipboardButton->setDefaultAction(mClipboardImageAction);
319         break;
320     case Settings::CopyLocation:
321         mClipboardButton->setText(i18n("Copy Location to Clipboard"));
322         mClipboardButton->setDefaultAction(mClipboardLocationAction);
323         break;
324     }
325     mClipboardButton->setEnabled(mPixmapExists);
326 }
327 
328 // overrides
329 
moveEvent(QMoveEvent * event)330 void KSMainWindow::moveEvent(QMoveEvent *event)
331 {
332     Q_UNUSED(event)
333 
334     KSharedConfigPtr config = KSharedConfig::openConfig(QStringLiteral("spectaclerc"));
335     KConfigGroup guiConfig(config, "GuiConfig");
336 
337     guiConfig.writeEntry("window-position", pos());
338     guiConfig.sync();
339 }
340 
341 // slots
captureScreenshot(Spectacle::CaptureMode theCaptureMode,int theTimeout,bool theIncludePointer,bool theIncludeDecorations)342 void KSMainWindow::captureScreenshot(Spectacle::CaptureMode theCaptureMode, int theTimeout, bool theIncludePointer, bool theIncludeDecorations)
343 {
344     if (theTimeout < 0) { // OnClick is checked (always the case on Wayland)
345         hide();
346         Q_EMIT newScreenshotRequest(theCaptureMode, theTimeout, theIncludePointer, theIncludeDecorations);
347         return;
348     }
349 
350     showMinimized();
351     mMessageWidget->hide();
352     QTimer *timer = new QTimer;
353     timer->setSingleShot(true);
354     timer->setInterval(theTimeout);
355     auto unityUpdate = [](const QVariantMap &properties) {
356         QDBusMessage message =
357             QDBusMessage::createSignal(QStringLiteral("/org/kde/Spectacle"), QStringLiteral("com.canonical.Unity.LauncherEntry"), QStringLiteral("Update"));
358         message.setArguments({QGuiApplication::desktopFileName(), properties});
359         QDBusConnection::sessionBus().send(message);
360     };
361     auto delayAnimation = new QVariantAnimation(timer);
362     delayAnimation->setStartValue(0.0);
363     delayAnimation->setEndValue(1.0);
364     delayAnimation->setDuration(timer->interval());
365     connect(delayAnimation, &QVariantAnimation::valueChanged, this, [=] {
366         const double progress = delayAnimation->currentValue().toDouble();
367         const double timeoutInSeconds = theTimeout / 1000.0;
368         mKSWidget->setProgress(progress);
369         unityUpdate({{QStringLiteral("progress"), progress}});
370         setWindowTitle(i18ncp("@title:window", "%1 second", "%1 seconds", qMin(int(timeoutInSeconds), qCeil((1 - progress) * timeoutInSeconds))));
371     });
372     connect(timer, &QTimer::timeout, this, [=] {
373         this->hide();
374         timer->deleteLater();
375         mKSWidget->setProgress(0);
376         unityUpdate({{QStringLiteral("progress-visible"), false}});
377         Q_EMIT newScreenshotRequest(theCaptureMode, 0, theIncludePointer, theIncludeDecorations);
378     });
379 
380     connect(mKSWidget, &KSWidget::screenshotCanceled, timer, [=] {
381         timer->stop();
382         timer->deleteLater();
383         restoreWindowTitle();
384         unityUpdate({{QStringLiteral("progress-visible"), false}});
385     });
386 
387     unityUpdate({{QStringLiteral("progress-visible"), true}, {QStringLiteral("progress"), 0}});
388     timer->start();
389     delayAnimation->start();
390 }
391 
setScreenshotAndShow(const QPixmap & pixmap)392 void KSMainWindow::setScreenshotAndShow(const QPixmap &pixmap)
393 {
394     mPixmapExists = !pixmap.isNull();
395     if (mPixmapExists) {
396         mKSWidget->setScreenshotPixmap(pixmap);
397         mExportMenu->imageUpdated();
398         setWindowTitle(i18nc("@title:window Unsaved Screenshot", "Unsaved[*]"));
399         setWindowModified(true);
400     } else {
401         restoreWindowTitle();
402     }
403 
404 #ifdef KIMAGEANNOTATOR_FOUND
405     mAnnotateButton->setEnabled(mPixmapExists);
406 #endif
407     mSendToButton->setEnabled(mPixmapExists);
408     mClipboardButton->setEnabled(mPixmapExists);
409     mSaveButton->setEnabled(mPixmapExists);
410 
411     mKSWidget->setButtonState(KSWidget::State::TakeNewScreenshot);
412     show();
413     activateWindow();
414     /* NOTE windowWidth only produces the right result if it is called after the window is visible.
415      * Because of this the call is not moved into the if above */
416     if (mPixmapExists) {
417         resize(QSize(windowWidth(pixmap), DEFAULT_WINDOW_HEIGHT));
418     }
419 }
420 
showPrintDialog()421 void KSMainWindow::showPrintDialog()
422 {
423     QPrinter *printer = new QPrinter(QPrinter::HighResolution);
424     QPrintDialog printDialog(printer, this);
425     if (printDialog.exec() == QDialog::Accepted) {
426         ExportManager::instance()->doPrint(printer);
427         return;
428     }
429     delete printer;
430 }
431 
openScreenshotsFolder()432 void KSMainWindow::openScreenshotsFolder()
433 {
434     auto job = new KIO::OpenUrlJob(Settings::defaultSaveLocation());
435     job->setUiDelegate(new KIO::JobUiDelegate(KIO::JobUiDelegate::AutoHandlingEnabled, this));
436     job->start();
437 }
438 
quit(const QuitBehavior quitBehavior)439 void KSMainWindow::quit(const QuitBehavior quitBehavior)
440 {
441     qApp->setQuitOnLastWindowClosed(false);
442     hide();
443 
444     if (quitBehavior == QuitBehavior::QuitImmediately) {
445         // Allow some time for clipboard content to transfer
446         // TODO: Find better solution
447         QTimer::singleShot(250, qApp, &QApplication::quit);
448     }
449 }
450 
showInlineMessage(const QString & message,const KMessageWidget::MessageType messageType,const MessageDuration messageDuration,const QList<QAction * > & actions)451 void KSMainWindow::showInlineMessage(const QString &message,
452                                      const KMessageWidget::MessageType messageType,
453                                      const MessageDuration messageDuration,
454                                      const QList<QAction *> &actions)
455 {
456     const auto messageWidgetActions = mMessageWidget->actions();
457     for (QAction *action : messageWidgetActions) {
458         mMessageWidget->removeAction(action);
459     }
460     for (QAction *action : actions) {
461         mMessageWidget->addAction(action);
462     }
463     mMessageWidget->setText(message);
464     mMessageWidget->setMessageType(messageType);
465 
466     switch (messageType) {
467     case KMessageWidget::Error:
468         mMessageWidget->setIcon(QIcon::fromTheme(QStringLiteral("dialog-error")));
469         break;
470     case KMessageWidget::Warning:
471         mMessageWidget->setIcon(QIcon::fromTheme(QStringLiteral("dialog-warning")));
472         break;
473     case KMessageWidget::Positive:
474         mMessageWidget->setIcon(QIcon::fromTheme(QStringLiteral("dialog-ok-apply")));
475         break;
476     case KMessageWidget::Information:
477         mMessageWidget->setIcon(QIcon::fromTheme(QStringLiteral("dialog-information")));
478         break;
479     }
480 
481     mHideMessageWidgetTimer->stop();
482     mMessageWidget->animatedShow();
483     if (messageDuration == MessageDuration::AutoHide) {
484         mHideMessageWidgetTimer->start();
485     }
486 }
487 
showImageSharedFeedback(bool error,const QString & message)488 void KSMainWindow::showImageSharedFeedback(bool error, const QString &message)
489 {
490     if (error == 1) {
491         // error == 1 means the user cancelled the sharing
492         return;
493     }
494 
495     if (error) {
496         showInlineMessage(i18n("There was a problem sharing the image: %1", message), KMessageWidget::Error);
497     } else {
498         if (message.isEmpty()) {
499             showInlineMessage(i18n("Image shared"), KMessageWidget::Positive);
500         } else {
501             showInlineMessage(i18n("The shared image link (<a href=\"%1\">%1</a>) has been copied to the clipboard.", message),
502                               KMessageWidget::Positive,
503                               MessageDuration::Persistent);
504             QApplication::clipboard()->setText(message);
505         }
506     }
507 }
508 
copyLocation()509 void KSMainWindow::copyLocation()
510 {
511     Settings::setLastUsedCopyMode(Settings::CopyLocation);
512     setDefaultCopyAction();
513 
514     const bool quitChecked = Settings::quitAfterSaveCopyExport();
515     ExportManager::instance()->doCopyLocationToClipboard();
516     if (quitChecked) {
517         quit(QuitBehavior::QuitExternally);
518     }
519 }
520 
copyImage()521 void KSMainWindow::copyImage()
522 {
523     Settings::setLastUsedCopyMode(Settings::CopyImage);
524     setDefaultCopyAction();
525 
526     const bool quitChecked = Settings::quitAfterSaveCopyExport();
527     ExportManager::instance()->doCopyToClipboard();
528     if (quitChecked) {
529         quit(QuitBehavior::QuitExternally);
530     }
531 }
532 
imageCopied()533 void KSMainWindow::imageCopied()
534 {
535     showInlineMessage(i18n("The screenshot has been copied to the clipboard."), KMessageWidget::Information);
536 }
537 
imageSavedAndLocationCopied(const QUrl & location)538 void KSMainWindow::imageSavedAndLocationCopied(const QUrl &location)
539 {
540     showInlineMessage(
541         i18n("The screenshot has been saved as <a href=\"%1\">%2</a> and its location has been copied to clipboard", location.toString(), location.fileName()),
542         KMessageWidget::Positive,
543         MessageDuration::AutoHide,
544         {mOpenContaining});
545 }
546 
screenshotFailed()547 void KSMainWindow::screenshotFailed()
548 {
549     showInlineMessage(i18n("Could not take a screenshot. Please report this bug here: <a href=\"https://bugs.kde.org/enter_bug.cgi?product=Spectacle\">create "
550                            "a spectacle bug</a>"),
551                       KMessageWidget::Warning);
552 #ifdef KIMAGEANNOTATOR_FOUND
553     mAnnotateButton->setEnabled(false);
554 #endif
555 }
556 
setPlaceholderTextOnLaunch()557 void KSMainWindow::setPlaceholderTextOnLaunch()
558 {
559     QString placeholderText(i18n("Ready to take a screenshot"));
560     mKSWidget->showPlaceholderText(placeholderText);
561     setWindowTitle(placeholderText);
562 }
563 
showPreferencesDialog()564 void KSMainWindow::showPreferencesDialog()
565 {
566     if (KConfigDialog::showDialog(QStringLiteral("settings"))) {
567         return;
568     }
569     (new SettingsDialog(this))->show();
570 }
571 
imageSaved(const QUrl & location)572 void KSMainWindow::imageSaved(const QUrl &location)
573 {
574     setWindowTitle(location.fileName());
575     setWindowModified(false);
576     showInlineMessage(i18n("The screenshot was saved as <a href=\"%1\">%2</a>", location.toString(), location.fileName()),
577                       KMessageWidget::Positive,
578                       MessageDuration::AutoHide,
579                       {mOpenContaining});
580 }
581 
imageSavedAndCopied(const QUrl & location)582 void KSMainWindow::imageSavedAndCopied(const QUrl &location)
583 {
584     setWindowTitle(location.fileName());
585     setWindowModified(false);
586     showInlineMessage(i18n("The screenshot was copied to the clipboard and saved as <a href=\"%1\">%2</a>", location.toString(), location.fileName()),
587                       KMessageWidget::Positive,
588                       MessageDuration::AutoHide,
589                       {mOpenContaining});
590 }
591 
save()592 void KSMainWindow::save()
593 {
594     Settings::setLastUsedSaveMode(Settings::Save);
595     setDefaultSaveAction();
596 
597     const bool quitChecked = Settings::quitAfterSaveCopyExport();
598     ExportManager::instance()->doSave(QUrl(), /* notify */ quitChecked);
599     if (quitChecked) {
600         quit(QuitBehavior::QuitExternally);
601     }
602 }
603 
saveAs()604 void KSMainWindow::saveAs()
605 {
606     Settings::setLastUsedSaveMode(Settings::SaveAs);
607     setDefaultSaveAction();
608 
609     const bool quitChecked = Settings::quitAfterSaveCopyExport();
610     if (ExportManager::instance()->doSaveAs(this, /* notify */ quitChecked) && quitChecked) {
611         quit(QuitBehavior::QuitExternally);
612     }
613 }
614 
restoreWindowTitle()615 void KSMainWindow::restoreWindowTitle()
616 {
617     if (isWindowModified()) {
618         setWindowTitle(i18nc("@title:window Unsaved Screenshot", "Unsaved[*]"));
619     } else {
620         // if a screenshot is not visible inside of mKSWidget, it means we have launched spectacle
621         // with the last mode set to 'rectangular region' and canceled the screenshot
622         if (mKSWidget->isScreenshotSet()) {
623             setWindowTitle(Settings::lastSaveLocation().fileName());
624         } else {
625             setPlaceholderTextOnLaunch();
626         }
627 #ifdef KIMAGEANNOTATOR_FOUND
628         mAnnotateButton->setEnabled(mKSWidget->isScreenshotSet());
629 #endif
630     }
631 }
632 
633 /* This event handler enables all Buttons to be activated with Enter. Normally only QPushButton can
634  * be activated with Enter but we also use QToolButtons so we handle the event ourselves */
keyPressEvent(QKeyEvent * event)635 void KSMainWindow::keyPressEvent(QKeyEvent *event)
636 {
637     if (event->key() == Qt::Key_Return) {
638         QWidget *fw = focusWidget();
639         auto pb = qobject_cast<QPushButton *>(fw);
640         if (pb) {
641             pb->animateClick();
642             return;
643         }
644         auto tb = qobject_cast<QToolButton *>(fw);
645         if (tb) {
646             tb->animateClick();
647             return;
648         }
649     }
650     QDialog::keyPressEvent(event);
651 }
652