1 /*
2  *  SPDX-FileCopyrightText: 2019 David Redondo <kde@david-redondo.de>
3  *  SPDX-FileCopyrightText: 2015 Boudhayan Gupta <bgupta@kde.org>
4  *
5  *  SPDX-License-Identifier: LGPL-2.0-or-later
6  */
7 
8 #include "KSWidget.h"
9 #include "spectacle_gui_debug.h"
10 
11 #include "ExportManager.h"
12 #include "KSImageWidget.h"
13 #include "ProgressButton.h"
14 #include "SmartSpinBox.h"
15 #include "settings.h"
16 
17 #include <QAction>
18 #include <QApplication>
19 #include <QCheckBox>
20 #include <QComboBox>
21 #include <QFormLayout>
22 #include <QGridLayout>
23 #include <QLabel>
24 #include <QShortcut>
25 #include <QStackedLayout>
26 
27 #ifdef KIMAGEANNOTATOR_FOUND
28 #include <kImageAnnotator/KImageAnnotator.h>
29 #endif
30 
31 #include <KConfigDialogManager>
32 #include <KLocalizedString>
33 
KSWidget(Platform::GrabModes theGrabModes,QWidget * parent)34 KSWidget::KSWidget(Platform::GrabModes theGrabModes, QWidget *parent)
35     : QWidget(parent)
36 {
37     mStack = new QStackedLayout(this);
38 
39     // we'll init the widget that holds the image first
40     mImageWidget = new KSImageWidget(this);
41     mImageWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
42     connect(mImageWidget, &KSImageWidget::dragInitiated, this, &KSWidget::dragInitiated);
43 
44 #ifdef KIMAGEANNOTATOR_FOUND
45     mAnnotator = new kImageAnnotator::KImageAnnotator();
46     mAnnotator->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
47     mAnnotator->setCanvasColor(QColor(255, 255, 255, 0));
48     mStack->addWidget(mAnnotator);
49 #endif
50 
51     // the capture mode options first
52     mCaptureModeLabel = new QLabel(i18n("<b>Capture Mode</b>"), this);
53     mCaptureArea = new QComboBox(this);
54 
55     if (theGrabModes.testFlag(Platform::GrabMode::AllScreens)) {
56         QString lFullScreenLabel = QApplication::screens().count() == 1 ? i18n("Full Screen") : i18n("Full Screen (All Monitors)");
57 
58         mCaptureArea->insertItem(0, lFullScreenLabel, Spectacle::CaptureMode::AllScreens);
59     }
60     if (theGrabModes.testFlag(Platform::GrabMode::AllScreensScaled) && QApplication::screens().count() > 1) {
61         QString lFullScreenLabel = i18n("Full Screen (All Monitors, scaled)");
62         mCaptureArea->insertItem(1, lFullScreenLabel, Spectacle::CaptureMode::AllScreensScaled);
63     }
64     if (theGrabModes.testFlag(Platform::GrabMode::PerScreenImageNative)) {
65         mCaptureArea->insertItem(2, i18n("Rectangular Region"), Spectacle::CaptureMode::RectangularRegion);
66     }
67     if (theGrabModes.testFlag(Platform::GrabMode::CurrentScreen)) {
68         mCaptureArea->insertItem(3, i18n("Current Screen"), Spectacle::CaptureMode::CurrentScreen);
69     }
70     if (theGrabModes.testFlag(Platform::GrabMode::ActiveWindow)) {
71         mCaptureArea->insertItem(4, i18n("Active Window"), Spectacle::CaptureMode::ActiveWindow);
72     }
73     if (theGrabModes.testFlag(Platform::GrabMode::WindowUnderCursor)) {
74         mCaptureArea->insertItem(5, i18n("Window Under Cursor"), Spectacle::CaptureMode::WindowUnderCursor);
75     }
76     if (theGrabModes.testFlag(Platform::GrabMode::TransientWithParent)) {
77         mTransientWithParentAvailable = true;
78     }
79     mCaptureArea->setMinimumWidth(240);
80     mCaptureArea->setObjectName(QStringLiteral("kcfg_captureMode"));
81     mCaptureArea->setProperty("kcfg_property", QByteArray("currentData"));
82     connect(mCaptureArea, qOverload<int>(&QComboBox::currentIndexChanged), this, &KSWidget::captureModeChanged);
83 
84     mDelayMsec = new SmartSpinBox(this);
85     mDelayMsec->setDecimals(1);
86     mDelayMsec->setSingleStep(1.0);
87     mDelayMsec->setMinimum(0.0);
88     mDelayMsec->setMaximum(999.9);
89     mDelayMsec->setSpecialValueText(i18n("No Delay"));
90     mDelayMsec->setMinimumWidth(160);
91     mDelayMsec->setObjectName(QStringLiteral("kcfg_captureDelay"));
92 
93     mCaptureOnClick = new QCheckBox(i18n("On Click"), this);
94     mCaptureOnClick->setToolTip(i18n("Wait for a mouse click before capturing the screenshot image"));
95     connect(mCaptureOnClick, &QCheckBox::stateChanged, this, &KSWidget::onClickStateChanged);
96     mCaptureOnClick->setObjectName(QStringLiteral("kcfg_onClickChecked"));
97 
98     mDelayLayout = new QHBoxLayout;
99     mDelayLayout->addWidget(mDelayMsec);
100     mDelayLayout->addWidget(mCaptureOnClick);
101 
102     mCaptureModeForm = new QFormLayout;
103     mCaptureModeForm->addRow(i18n("Area:"), mCaptureArea);
104     mCaptureModeForm->addRow(i18n("Delay:"), mDelayLayout);
105     mCaptureModeForm->setContentsMargins(24, 0, 0, 0);
106 
107     // options (mouse pointer, window decorations, quit after saving or copying)
108     mContentOptionsLabel = new QLabel(this);
109     mContentOptionsLabel->setText(i18n("<b>Options</b>"));
110 
111     mMousePointer = new QCheckBox(i18n("Include mouse pointer"), this);
112     mMousePointer->setToolTip(i18n("Show the mouse cursor in the screenshot image"));
113     mMousePointer->setObjectName(QStringLiteral("kcfg_includePointer"));
114 
115     mWindowDecorations = new QCheckBox(i18n("Include window titlebar and borders"), this);
116     mWindowDecorations->setToolTip(i18n("Show the window title bar, the minimize/maximize/close buttons, and the window border"));
117     mWindowDecorations->setEnabled(false);
118     mWindowDecorations->setObjectName(QStringLiteral("kcfg_includeDecorations"));
119 
120     mCaptureTransientOnly = new QCheckBox(i18n("Capture the current pop-up only"), this);
121     mCaptureTransientOnly->setToolTip(
122         i18n("Capture only the current pop-up window (like a menu, tooltip etc).\n"
123              "If disabled, the pop-up is captured along with the parent window"));
124     mCaptureTransientOnly->setEnabled(false);
125     mCaptureTransientOnly->setObjectName(QStringLiteral("kcfg_transientOnly"));
126 
127     mQuitAfterSaveOrCopy = new QCheckBox(i18n("Quit after manual Save or Copy"), this);
128     mQuitAfterSaveOrCopy->setToolTip(i18n("Quit Spectacle after manually saving or copying the image"));
129     mQuitAfterSaveOrCopy->setObjectName(QStringLiteral("kcfg_quitAfterSaveCopyExport"));
130 
131     mContentOptionsForm = new QVBoxLayout;
132     mContentOptionsForm->addWidget(mMousePointer);
133     mContentOptionsForm->addWidget(mWindowDecorations);
134     mContentOptionsForm->addWidget(mCaptureTransientOnly);
135     mContentOptionsForm->addWidget(mQuitAfterSaveOrCopy);
136     mContentOptionsForm->setContentsMargins(24, 0, 0, 0);
137 
138     mTakeNewScreenshotAction = new QAction(QIcon::fromTheme(QStringLiteral("spectacle")), i18n("Take a New Screenshot"), this);
139     mTakeNewScreenshotAction->setShortcut(QKeySequence::New);
140     connect(mTakeNewScreenshotAction, &QAction::triggered, this, &KSWidget::newScreenshotClicked);
141 
142     mCancelAction = new QAction(QIcon::fromTheme(QStringLiteral("dialog-cancel")), i18n("Cancel"), this);
143     mCancelAction->setShortcut(QKeySequence::Cancel);
144     connect(mCancelAction, &QAction::triggered, this, [this] {
145         Q_EMIT screenshotCanceled();
146         setButtonState(State::TakeNewScreenshot);
147     });
148 
149     // the take a new screenshot button
150     mTakeScreenshotButton = new ProgressButton(this);
151     mTakeScreenshotButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
152     mTakeScreenshotButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
153     setButtonState(State::TakeNewScreenshot);
154     mTakeScreenshotButton->setFocus();
155 
156     // finally, finish up the layouts
157     mRightLayout = new QVBoxLayout;
158     mRightLayout->addStretch(1);
159     mRightLayout->addWidget(mCaptureModeLabel);
160     mRightLayout->addLayout(mCaptureModeForm);
161     mRightLayout->addStretch(1);
162     mRightLayout->addWidget(mContentOptionsLabel);
163     mRightLayout->addLayout(mContentOptionsForm);
164     mRightLayout->addStretch(10);
165     mRightLayout->addWidget(mTakeScreenshotButton, 1, Qt::AlignHCenter);
166     mRightLayout->setContentsMargins(10, 0, 0, 10);
167 
168     mPlaceholderLabel = new QLabel;
169 
170     QFont placeholderLabelFont;
171     // To match the size of a level 2 Heading/KTitleWidget
172     placeholderLabelFont.setPointSize(qRound(placeholderLabelFont.pointSize() * 1.3));
173     mPlaceholderLabel->setFont(placeholderLabelFont);
174     mPlaceholderLabel->setTextInteractionFlags(Qt::NoTextInteraction);
175     mPlaceholderLabel->setWordWrap(true);
176     mPlaceholderLabel->setAlignment(Qt::AlignCenter);
177     mPlaceholderLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
178     // Match opacity of QML placeholder label component
179     auto *effect = new QGraphicsOpacityEffect(mPlaceholderLabel);
180     effect->setOpacity(0.5);
181     mPlaceholderLabel->setGraphicsEffect(effect);
182 
183     mMainLayout = new QGridLayout();
184 
185     mMainLayout->addWidget(mPlaceholderLabel, 0, 0, 1, 1);
186     mMainLayout->addWidget(mImageWidget, 0, 0, 1, 1);
187     mMainLayout->addLayout(mRightLayout, 0, 1, 1, 1);
188     mMainLayout->setColumnMinimumWidth(0, 320);
189     mMainLayout->setColumnMinimumWidth(1, 320);
190 
191     int index = mCaptureArea->findData(Settings::captureMode());
192     mCaptureArea->setCurrentIndex(index >= 0 ? index : 0);
193     auto mConfigManager = new KConfigDialogManager(this, Settings::self());
194     connect(mConfigManager, &KConfigDialogManager::widgetModified, mConfigManager, &KConfigDialogManager::updateSettings);
195 
196     placeHolder = new QWidget();
197     placeHolder->setLayout(mMainLayout);
198 
199     mStack->addWidget(placeHolder);
200 
201 #ifdef KIMAGEANNOTATOR_FOUND
202     mStack->addWidget(mAnnotator);
203 #endif
204 }
205 
imagePaddingWidth() const206 int KSWidget::imagePaddingWidth() const
207 {
208     int lRightLayoutLeft = 0;
209     int lRightLayoutRight = 0;
210     int lMainLayoutRight = 0;
211 
212     mRightLayout->getContentsMargins(&lRightLayoutLeft, nullptr, &lRightLayoutRight, nullptr);
213     mMainLayout->getContentsMargins(nullptr, nullptr, &lMainLayoutRight, nullptr);
214 
215     int lPaddingWidth = (lRightLayoutLeft + lRightLayoutRight + lMainLayoutRight);
216     lPaddingWidth += mRightLayout->contentsRect().width();
217     lPaddingWidth += 2 * SpectacleImage::SHADOW_RADIUS; // image drop shadow
218 
219     return lPaddingWidth;
220 }
221 
isScreenshotSet()222 bool KSWidget::isScreenshotSet()
223 {
224     return mImageWidget->isPixmapSet();
225 }
226 
227 // public slots
228 
showPlaceholderText(const QString & label)229 void KSWidget::showPlaceholderText(const QString &label)
230 {
231     mImageWidget->hide();
232     mPlaceholderLabel->setText(label);
233     mPlaceholderLabel->show();
234 }
235 
setScreenshotPixmap(const QPixmap & thePixmap)236 void KSWidget::setScreenshotPixmap(const QPixmap &thePixmap)
237 {
238     if (mPlaceholderLabel->isVisible()) {
239         mPlaceholderLabel->hide();
240     }
241     mImageWidget->show();
242     mImageWidget->setScreenshot(thePixmap);
243 }
244 
lockOnClickEnabled()245 void KSWidget::lockOnClickEnabled()
246 {
247     mCaptureOnClick->setCheckState(Qt::Checked);
248     mCaptureOnClick->setEnabled(false);
249     mDelayMsec->setEnabled(false);
250 }
251 
lockOnClickDisabled()252 void KSWidget::lockOnClickDisabled()
253 {
254     mCaptureOnClick->setCheckState(Qt::Unchecked);
255     mCaptureOnClick->hide();
256     mDelayMsec->setEnabled(true);
257 }
258 
259 // private slots
260 
newScreenshotClicked()261 void KSWidget::newScreenshotClicked()
262 {
263     int lDelay = mCaptureOnClick->isChecked() ? -1 : (mDelayMsec->value() * 1000);
264     auto lMode = static_cast<Spectacle::CaptureMode>(mCaptureArea->currentData().toInt());
265     if (mTransientWithParentAvailable && lMode == Spectacle::CaptureMode::WindowUnderCursor && !(mCaptureTransientOnly->isChecked())) {
266         lMode = Spectacle::CaptureMode::TransientWithParent;
267     }
268     setButtonState(State::Cancel);
269     Q_EMIT newScreenshotRequest(lMode, lDelay, mMousePointer->isChecked(), mWindowDecorations->isChecked());
270 }
271 
onClickStateChanged(int theState)272 void KSWidget::onClickStateChanged(int theState)
273 {
274     if (theState == Qt::Checked) {
275         mDelayMsec->setEnabled(false);
276     } else if (theState == Qt::Unchecked) {
277         mDelayMsec->setEnabled(true);
278     }
279 }
280 
captureModeChanged(int theIndex)281 void KSWidget::captureModeChanged(int theIndex)
282 {
283     Spectacle::CaptureMode captureMode = static_cast<Spectacle::CaptureMode>(mCaptureArea->itemData(theIndex).toInt());
284     switch (captureMode) {
285     case Spectacle::CaptureMode::WindowUnderCursor:
286         mWindowDecorations->setEnabled(true);
287         if (mTransientWithParentAvailable) {
288             mCaptureTransientOnly->setEnabled(true);
289         } else {
290             mCaptureTransientOnly->setEnabled(false);
291         }
292         break;
293     case Spectacle::CaptureMode::ActiveWindow:
294         mWindowDecorations->setEnabled(true);
295         mCaptureTransientOnly->setEnabled(false);
296         break;
297     case Spectacle::CaptureMode::AllScreens:
298     case Spectacle::CaptureMode::AllScreensScaled:
299     case Spectacle::CaptureMode::CurrentScreen:
300     case Spectacle::CaptureMode::RectangularRegion:
301         mWindowDecorations->setEnabled(false);
302         mCaptureTransientOnly->setEnabled(false);
303         break;
304     case Spectacle::CaptureMode::TransientWithParent:
305     case Spectacle::CaptureMode::InvalidChoice:
306     default:
307         qCWarning(SPECTACLE_GUI_LOG) << "Skipping invalid or unreachable enum value";
308         break;
309     }
310 }
311 
setButtonState(State state)312 void KSWidget::setButtonState(State state)
313 {
314     switch (state) {
315     case State::TakeNewScreenshot:
316         mTakeScreenshotButton->removeAction(mCancelAction);
317         mTakeScreenshotButton->setDefaultAction(mTakeNewScreenshotAction);
318         mTakeScreenshotButton->setProgress(0);
319         break;
320     case State::Cancel:
321         mTakeScreenshotButton->removeAction(mTakeNewScreenshotAction);
322         mTakeScreenshotButton->setDefaultAction(mCancelAction);
323         break;
324     }
325 }
326 
setProgress(double progress)327 void KSWidget::setProgress(double progress)
328 {
329     mTakeScreenshotButton->setProgress(progress);
330 }
331 
332 #ifdef KIMAGEANNOTATOR_FOUND
showAnnotator()333 void KSWidget::showAnnotator()
334 {
335     mStack->setCurrentIndex(1);
336     QPixmap px = ExportManager::instance()->pixmap();
337     px.setDevicePixelRatio(qApp->devicePixelRatio());
338     mAnnotator->loadImage(px);
339 }
340 
hideAnnotator()341 void KSWidget::hideAnnotator()
342 {
343     mStack->setCurrentIndex(0);
344     QImage image = mAnnotator->image();
345     QPixmap px = QPixmap::fromImage(image);
346     setScreenshotPixmap(px);
347     ExportManager::instance()->setPixmap(px);
348 }
349 #endif
350