1 /*
2  *  SPDX-FileCopyrightText: 2018 Ambareesh "Amby" Balaji <ambareeshbalaji@gmail.com>
3  *
4  *  SPDX-License-Identifier: LGPL-2.0-or-later
5  */
6 
7 #include "QuickEditor.h"
8 
9 #include "ComparableQPoint.h"
10 #include "settings.h"
11 
12 #include <KLocalizedString>
13 #include <KWayland/Client/plasmashell.h>
14 #include <KWayland/Client/surface.h>
15 #include <KWindowSystem>
16 
17 #include <QGuiApplication>
18 #include <QMouseEvent>
19 #include <QPainter>
20 #include <QPainterPath>
21 #include <QScreen>
22 #include <QX11Info>
23 #include <QtMath>
24 
25 const int QuickEditor::handleRadiusMouse = 9;
26 const int QuickEditor::handleRadiusTouch = 12;
27 const qreal QuickEditor::increaseDragAreaFactor = 2.0;
28 const int QuickEditor::minSpacingBetweenHandles = 20;
29 const int QuickEditor::borderDragAreaSize = 10;
30 
31 const int QuickEditor::selectionSizeThreshold = 100;
32 
33 const int QuickEditor::selectionBoxPaddingX = 5;
34 const int QuickEditor::selectionBoxPaddingY = 4;
35 const int QuickEditor::selectionBoxMarginY = 5;
36 
37 bool QuickEditor::bottomHelpTextPrepared = false;
38 const int QuickEditor::bottomHelpBoxPaddingX = 12;
39 const int QuickEditor::bottomHelpBoxPaddingY = 8;
40 const int QuickEditor::bottomHelpBoxPairSpacing = 6;
41 const int QuickEditor::bottomHelpBoxMarginBottom = 5;
42 const int QuickEditor::midHelpTextFontSize = 12;
43 
44 const int QuickEditor::magnifierLargeStep = 15;
45 
46 const int QuickEditor::magZoom = 5;
47 const int QuickEditor::magPixels = 16;
48 const int QuickEditor::magOffset = 32;
49 
QuickEditor(const QMap<const QScreen *,QImage> & images,KWayland::Client::PlasmaShell * plasmashell,QWidget * parent)50 QuickEditor::QuickEditor(const QMap<const QScreen *, QImage> &images, KWayland::Client::PlasmaShell *plasmashell, QWidget *parent)
51     : QWidget(parent, Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint | Qt::Popup | Qt::WindowStaysOnTopHint)
52     , mMaskColor(QColor::fromRgbF(0, 0, 0, 0.15))
53     , mStrokeColor(palette().highlight().color())
54     , mCrossColor(QColor::fromRgbF(mStrokeColor.redF(), mStrokeColor.greenF(), mStrokeColor.blueF(), 0.7))
55     , mLabelBackgroundColor(QColor::fromRgbF(palette().light().color().redF(), palette().light().color().greenF(), palette().light().color().blueF(), 0.85))
56     , mLabelForegroundColor(palette().windowText().color())
57     , mMidHelpText(i18n("Click and drag to draw a selection rectangle,\nor press Esc to quit"))
58     , mMidHelpTextFont(font())
59     , mBottomHelpTextFont(font())
60     , mBottomHelpGridLeftWidth(0)
61     , mMouseDragState(MouseState::None)
62     , mImages(images)
63     , mMagnifierAllowed(false)
64     , mShowMagnifier(Settings::showMagnifier())
65     , mToggleMagnifier(false)
66     , mReleaseToCapture(Settings::useReleaseToCapture())
67     , mDisableArrowKeys(false)
68     , mbottomHelpLength(bottomHelpMaxLength)
69     , mHandleRadius(handleRadiusMouse)
70 {
71     if (Settings::useLightMaskColour()) {
72         mMaskColor = QColor(255, 255, 255, 100);
73     }
74 
75     setMouseTracking(true);
76     setAttribute(Qt::WA_StaticContents);
77 
78     devicePixelRatio = plasmashell ? 1.0 : devicePixelRatioF();
79     devicePixelRatioI = 1.0 / devicePixelRatio;
80 
81     preparePaint();
82     createPixmapFromScreens();
83     setGeometryToScreenPixmap(plasmashell);
84 
85     if (!(Settings::rememberLastRectangularRegion() == Settings::EnumRememberLastRectangularRegion::Never)) {
86         auto savedRect = Settings::cropRegion();
87         QRect cropRegion = QRect(savedRect[0], savedRect[1], savedRect[2], savedRect[3]);
88         if (!cropRegion.isEmpty()) {
89             mSelection = QRect(cropRegion.x() * devicePixelRatioI,
90                                cropRegion.y() * devicePixelRatioI,
91                                cropRegion.width() * devicePixelRatioI,
92                                cropRegion.height() * devicePixelRatioI)
93                              .intersected(rect());
94         }
95         setMouseCursor(QCursor::pos());
96     } else {
97         setCursor(Qt::CrossCursor);
98     }
99 
100     setBottomHelpText();
101     mMidHelpTextFont.setPointSize(midHelpTextFontSize);
102     if (!bottomHelpTextPrepared) {
103         bottomHelpTextPrepared = true;
104         const auto prepare = [this](QStaticText &item) {
105             item.prepare(QTransform(), mBottomHelpTextFont);
106             item.setPerformanceHint(QStaticText::AggressiveCaching);
107         };
108         for (auto &pair : mBottomHelpText) {
109             prepare(pair.first);
110             for (auto &item : pair.second) {
111                 prepare(item);
112             }
113         }
114     }
115     layoutBottomHelpText();
116 
117     update();
118 }
119 
acceptSelection()120 void QuickEditor::acceptSelection()
121 {
122     if (!mSelection.isEmpty()) {
123         QRect scaledCropRegion = QRect(qRound(mSelection.x() * devicePixelRatio),
124                                        qRound(mSelection.y() * devicePixelRatio),
125                                        qRound(mSelection.width() * devicePixelRatio),
126                                        qRound(mSelection.height() * devicePixelRatio));
127         Settings::setCropRegion({scaledCropRegion.x(), scaledCropRegion.y(), scaledCropRegion.width(), scaledCropRegion.height()});
128 
129         if (KWindowSystem::isPlatformX11()) {
130             Q_EMIT grabDone(mPixmap.copy(scaledCropRegion));
131 
132         } else {
133             // Wayland case
134             qreal maxDpr = 1.0;
135             for (const QScreen *screen : QGuiApplication::screens()) {
136                 if (screen->devicePixelRatio() > maxDpr) {
137                     maxDpr = screen->devicePixelRatio();
138                 }
139             }
140 
141             QPixmap output(mSelection.size() * maxDpr);
142             QPainter painter(&output);
143 
144             for (auto it = mScreenToDpr.constBegin(); it != mScreenToDpr.constEnd(); ++it) {
145                 const QScreen *screen = it.key();
146                 const QRect &screenRect = screen->geometry();
147 
148                 if (mSelection.intersects(screenRect)) {
149                     const ComparableQPoint pos = screenRect.topLeft();
150                     const qreal dpr = it.value();
151 
152                     QRect intersected = screenRect.intersected(mSelection);
153 
154                     // converts to screen size & position
155                     QRect pixelOnScreenIntersected;
156                     pixelOnScreenIntersected.moveTopLeft((intersected.topLeft() - pos) * dpr);
157                     pixelOnScreenIntersected.setWidth(intersected.width() * dpr);
158                     pixelOnScreenIntersected.setHeight(intersected.height() * dpr);
159 
160                     QPixmap screenOutput = QPixmap::fromImage(mImages.value(screen).copy(pixelOnScreenIntersected));
161 
162                     if (intersected.size() == mSelection.size()) {
163                         // short path when single screen
164                         // keep native screen resolution
165                         Q_EMIT grabDone(screenOutput);
166                         return;
167                     }
168 
169                     // upscale the image according to max screen dpr, to keep the image not distorted
170                     const auto dprI = maxDpr / dpr;
171                     QBrush brush(screenOutput);
172                     brush.setTransform(QTransform::fromScale(dprI, dprI));
173                     intersected.moveTopLeft((intersected.topLeft() - mSelection.topLeft()) * maxDpr);
174                     intersected.setSize(intersected.size() * maxDpr);
175                     painter.setBrushOrigin(intersected.topLeft());
176                     painter.fillRect(intersected, brush);
177                 }
178             }
179 
180             Q_EMIT grabDone(output);
181         }
182     }
183 }
184 
keyPressEvent(QKeyEvent * event)185 void QuickEditor::keyPressEvent(QKeyEvent *event)
186 {
187     const auto modifiers = event->modifiers();
188     const bool shiftPressed = modifiers & Qt::ShiftModifier;
189     if (shiftPressed) {
190         mToggleMagnifier = true;
191         update();
192     }
193     switch (event->key()) {
194     case Qt::Key_Escape:
195         Q_EMIT grabCancelled();
196         break;
197     case Qt::Key_Return:
198     case Qt::Key_Enter:
199         acceptSelection();
200         break;
201     case Qt::Key_Up: {
202         if (mDisableArrowKeys) {
203             update();
204             break;
205         }
206         const qreal step = (shiftPressed ? 1 : magnifierLargeStep);
207         const int newPos = boundsUp(qRound(mSelection.top() * devicePixelRatio - step), false);
208         if (modifiers & Qt::AltModifier) {
209             mSelection.setBottom(devicePixelRatioI * newPos + mSelection.height());
210             mSelection = mSelection.normalized();
211         } else {
212             mSelection.moveTop(devicePixelRatioI * newPos);
213         }
214         update();
215         break;
216     }
217     case Qt::Key_Right: {
218         if (mDisableArrowKeys) {
219             update();
220             break;
221         }
222         const qreal step = (shiftPressed ? 1 : magnifierLargeStep);
223         const int newPos = boundsRight(qRound(mSelection.left() * devicePixelRatio + step), false);
224         if (modifiers & Qt::AltModifier) {
225             mSelection.setRight(devicePixelRatioI * newPos + mSelection.width());
226         } else {
227             mSelection.moveLeft(devicePixelRatioI * newPos);
228         }
229         update();
230         break;
231     }
232     case Qt::Key_Down: {
233         if (mDisableArrowKeys) {
234             update();
235             break;
236         }
237         const qreal step = (shiftPressed ? 1 : magnifierLargeStep);
238         const int newPos = boundsDown(qRound(mSelection.top() * devicePixelRatio + step), false);
239         if (modifiers & Qt::AltModifier) {
240             mSelection.setBottom(devicePixelRatioI * newPos + mSelection.height());
241         } else {
242             mSelection.moveTop(devicePixelRatioI * newPos);
243         }
244         update();
245         break;
246     }
247     case Qt::Key_Left: {
248         if (mDisableArrowKeys) {
249             update();
250             break;
251         }
252         const qreal step = (shiftPressed ? 1 : magnifierLargeStep);
253         const int newPos = boundsLeft(qRound(mSelection.left() * devicePixelRatio - step), false);
254         if (modifiers & Qt::AltModifier) {
255             mSelection.setRight(devicePixelRatioI * newPos + mSelection.width());
256             mSelection = mSelection.normalized();
257         } else {
258             mSelection.moveLeft(devicePixelRatioI * newPos);
259         }
260         update();
261         break;
262     }
263     default:
264         break;
265     }
266     event->accept();
267 }
268 
keyReleaseEvent(QKeyEvent * event)269 void QuickEditor::keyReleaseEvent(QKeyEvent *event)
270 {
271     if (mToggleMagnifier && !(event->modifiers() & Qt::ShiftModifier)) {
272         mToggleMagnifier = false;
273         update();
274     }
275     event->accept();
276 }
277 
boundsLeft(int newTopLeftX,const bool mouse)278 int QuickEditor::boundsLeft(int newTopLeftX, const bool mouse)
279 {
280     if (newTopLeftX < 0) {
281         if (mouse) {
282             // tweak startPos to prevent rectangle from getting stuck
283             mStartPos.setX(mStartPos.x() + newTopLeftX * devicePixelRatioI);
284         }
285         newTopLeftX = 0;
286     }
287 
288     return newTopLeftX;
289 }
290 
boundsRight(int newTopLeftX,const bool mouse)291 int QuickEditor::boundsRight(int newTopLeftX, const bool mouse)
292 {
293     // the max X coordinate of the top left point
294     const int realMaxX = qRound((width() - mSelection.width()) * devicePixelRatioF());
295     const int xOffset = newTopLeftX - realMaxX;
296     if (xOffset > 0) {
297         if (mouse) {
298             mStartPos.setX(mStartPos.x() + xOffset * devicePixelRatioI);
299         }
300         newTopLeftX = realMaxX;
301     }
302 
303     return newTopLeftX;
304 }
305 
boundsUp(int newTopLeftY,const bool mouse)306 int QuickEditor::boundsUp(int newTopLeftY, const bool mouse)
307 {
308     if (newTopLeftY < 0) {
309         if (mouse) {
310             mStartPos.setY(mStartPos.y() + newTopLeftY * devicePixelRatioI);
311         }
312         newTopLeftY = 0;
313     }
314 
315     return newTopLeftY;
316 }
317 
boundsDown(int newTopLeftY,const bool mouse)318 int QuickEditor::boundsDown(int newTopLeftY, const bool mouse)
319 {
320     // the max Y coordinate of the top left point
321     const int realMaxY = qRound((height() - mSelection.height()) * devicePixelRatio);
322     const int yOffset = newTopLeftY - realMaxY;
323     if (yOffset > 0) {
324         if (mouse) {
325             mStartPos.setY(mStartPos.y() + yOffset * devicePixelRatioI);
326         }
327         newTopLeftY = realMaxY;
328     }
329 
330     return newTopLeftY;
331 }
332 
mousePressEvent(QMouseEvent * event)333 void QuickEditor::mousePressEvent(QMouseEvent *event)
334 {
335     if (event->source() == Qt::MouseEventNotSynthesized) {
336         mHandleRadius = handleRadiusMouse;
337     } else {
338         mHandleRadius = handleRadiusTouch;
339     }
340 
341     if (event->button() & Qt::LeftButton) {
342         /* NOTE  Workaround for Bug 407843
343          * If we show the selection Widget when a right click menu is open we lose focus on X.
344          * When the user clicks we get the mouse back. We can only grab the keyboard if we already
345          * have mouse focus. So just grab it undconditionally here.
346          */
347         grabKeyboard();
348         mMousePos = event->pos();
349         mMagnifierAllowed = true;
350         mMouseDragState = mouseLocation(mMousePos);
351         mDisableArrowKeys = true;
352         switch (mMouseDragState) {
353         case MouseState::Outside:
354             mStartPos = mMousePos;
355             break;
356         case MouseState::Inside:
357             mStartPos = mMousePos;
358             mMagnifierAllowed = false;
359             mInitialTopLeft = mSelection.topLeft();
360             setCursor(Qt::ClosedHandCursor);
361             break;
362         case MouseState::Top:
363         case MouseState::Left:
364         case MouseState::TopLeft:
365             mStartPos = mSelection.bottomRight();
366             break;
367         case MouseState::Bottom:
368         case MouseState::Right:
369         case MouseState::BottomRight:
370             mStartPos = mSelection.topLeft();
371             break;
372         case MouseState::TopRight:
373             mStartPos = mSelection.bottomLeft();
374             break;
375         case MouseState::BottomLeft:
376             mStartPos = mSelection.topRight();
377             break;
378         default:
379             break;
380         }
381     }
382     if (mMagnifierAllowed) {
383         update();
384     }
385     event->accept();
386 }
387 
mouseMoveEvent(QMouseEvent * event)388 void QuickEditor::mouseMoveEvent(QMouseEvent *event)
389 {
390     mMousePos = event->pos();
391     mMagnifierAllowed = true;
392     switch (mMouseDragState) {
393     case MouseState::None: {
394         setMouseCursor(mMousePos);
395         mMagnifierAllowed = false;
396         break;
397     }
398     case MouseState::TopLeft:
399     case MouseState::TopRight:
400     case MouseState::BottomRight:
401     case MouseState::BottomLeft: {
402         const bool afterX = mMousePos.x() >= mStartPos.x();
403         const bool afterY = mMousePos.y() >= mStartPos.y();
404         mSelection.setRect(afterX ? mStartPos.x() : mMousePos.x(),
405                            afterY ? mStartPos.y() : mMousePos.y(),
406                            qAbs(mMousePos.x() - mStartPos.x()) + (afterX ? devicePixelRatioI : 0),
407                            qAbs(mMousePos.y() - mStartPos.y()) + (afterY ? devicePixelRatioI : 0));
408         update();
409         break;
410     }
411     case MouseState::Outside: {
412         mSelection.setRect(qMin(mMousePos.x(), mStartPos.x()),
413                            qMin(mMousePos.y(), mStartPos.y()),
414                            qAbs(mMousePos.x() - mStartPos.x()) + devicePixelRatioI,
415                            qAbs(mMousePos.y() - mStartPos.y()) + devicePixelRatioI);
416         update();
417         break;
418     }
419     case MouseState::Top:
420     case MouseState::Bottom: {
421         const bool afterY = mMousePos.y() >= mStartPos.y();
422         mSelection.setRect(mSelection.x(),
423                            afterY ? mStartPos.y() : mMousePos.y(),
424                            mSelection.width(),
425                            qAbs(mMousePos.y() - mStartPos.y()) + (afterY ? devicePixelRatioI : 0));
426         update();
427         break;
428     }
429     case MouseState::Right:
430     case MouseState::Left: {
431         const bool afterX = mMousePos.x() >= mStartPos.x();
432         mSelection.setRect(afterX ? mStartPos.x() : mMousePos.x(),
433                            mSelection.y(),
434                            qAbs(mMousePos.x() - mStartPos.x()) + (afterX ? devicePixelRatioI : 0),
435                            mSelection.height());
436         update();
437         break;
438     }
439     case MouseState::Inside: {
440         mMagnifierAllowed = false;
441         // We use some math here to figure out if the diff with which we
442         // move the rectangle with moves it out of bounds,
443         // in which case we adjust the diff to not let that happen
444 
445         // new top left point of the rectangle
446         QPoint newTopLeft = ((mMousePos - mStartPos + mInitialTopLeft) * devicePixelRatio).toPoint();
447 
448         const QRect newRect(newTopLeft, mSelection.size() * devicePixelRatio);
449 
450         const QRect translatedScreensRect = mScreensRect.translated(-mScreensRect.topLeft());
451         if (!translatedScreensRect.contains(newRect)) {
452             // Keep the item inside the scene screen region bounding rect.
453             newTopLeft.setX(qMin(translatedScreensRect.right() - newRect.width(), qMax(newTopLeft.x(), translatedScreensRect.left())));
454             newTopLeft.setY(qMin(translatedScreensRect.bottom() - newRect.height(), qMax(newTopLeft.y(), translatedScreensRect.top())));
455         }
456 
457         mSelection.moveTo(newTopLeft * devicePixelRatioI);
458         update();
459         break;
460     }
461     default:
462         break;
463     }
464 
465     event->accept();
466 }
467 
mouseReleaseEvent(QMouseEvent * event)468 void QuickEditor::mouseReleaseEvent(QMouseEvent *event)
469 {
470     switch (event->button()) {
471     case Qt::LeftButton:
472         if (mMouseDragState == MouseState::Outside && mReleaseToCapture) {
473             acceptSelection();
474             return;
475         }
476         mDisableArrowKeys = false;
477         if (mMouseDragState == MouseState::Inside) {
478             setCursor(Qt::OpenHandCursor);
479         }
480         break;
481     case Qt::RightButton:
482         mSelection.setWidth(0);
483         mSelection.setHeight(0);
484         break;
485     default:
486         break;
487     }
488     event->accept();
489     mMouseDragState = MouseState::None;
490     update();
491 }
492 
mouseDoubleClickEvent(QMouseEvent * event)493 void QuickEditor::mouseDoubleClickEvent(QMouseEvent *event)
494 {
495     event->accept();
496     if (event->button() == Qt::LeftButton && mSelection.contains(event->pos())) {
497         acceptSelection();
498     }
499 }
500 
computeCoordinatesAfterScaling(const QMap<ComparableQPoint,QPair<qreal,QSize>> & outputsRect)501 QMap<ComparableQPoint, ComparableQPoint> QuickEditor::computeCoordinatesAfterScaling(const QMap<ComparableQPoint, QPair<qreal, QSize>> &outputsRect)
502 {
503     QMap<ComparableQPoint, ComparableQPoint> translationMap;
504 
505     for (auto i = outputsRect.keyBegin(); i != outputsRect.keyEnd(); ++i) {
506         translationMap.insert(*i, *i);
507     }
508 
509     for (auto i = outputsRect.constBegin(); i != outputsRect.constEnd(); ++i) {
510         const ComparableQPoint p = i.key();
511         const QSize &size = i.value().second;
512         const double dpr = i.value().first;
513         if (!qFuzzyCompare(dpr, 1.0)) {
514             // must update all coordinates of next rects
515             int newWidth = size.width();
516             int newHeight = size.height();
517 
518             int deltaX = newWidth - (size.width());
519             int deltaY = newHeight - (size.height());
520 
521             // for the next size
522             for (auto i2 = outputsRect.constFind(p); i2 != outputsRect.constEnd(); ++i2) {
523                 auto point = i2.key();
524                 auto finalPoint = translationMap.value(point);
525 
526                 if (point.x() >= newWidth + p.x() - deltaX) {
527                     finalPoint.setX(finalPoint.x() + deltaX);
528                 }
529                 if (point.y() >= newHeight + p.y() - deltaY) {
530                     finalPoint.setY(finalPoint.y() + deltaY);
531                 }
532                 // update final position point with the necessary deltas
533                 translationMap.insert(point, finalPoint);
534             }
535         }
536     }
537 
538     return translationMap;
539 }
540 
preparePaint()541 void QuickEditor::preparePaint()
542 {
543     for (auto i = mImages.constBegin(); i != mImages.constEnd(); ++i) {
544         const QScreen *screen = i.key();
545         const QImage &screenImage = i.value();
546 
547         const qreal dpr = screenImage.width() / static_cast<qreal>(screen->geometry().width());
548         mScreenToDpr.insert(screen, dpr);
549 
550         QRect virtualScreenRect;
551         if (KWindowSystem::isPlatformX11()) {
552             virtualScreenRect = QRect(screen->geometry().topLeft(), screenImage.size());
553         } else {
554             virtualScreenRect = QRect(screen->geometry().topLeft(), screenImage.size() / dpr);
555         }
556         mScreensRect = mScreensRect.united(virtualScreenRect);
557     }
558 }
559 
createPixmapFromScreens()560 void QuickEditor::createPixmapFromScreens()
561 {
562     const QList<QScreen *> screens = QGuiApplication::screens();
563     QMap<ComparableQPoint, QPair<qreal, QSize>> input;
564     for (auto it = mImages.constBegin(); it != mImages.constEnd(); ++it) {
565         const QScreen *screen = it.key();
566         const QImage &screenImage = it.value();
567         input.insert(screen->geometry().topLeft(), QPair<qreal, QSize>(screenImage.width() / static_cast<qreal>(screen->size().width()), screenImage.size()));
568     }
569     const auto pointsTranslationMap = computeCoordinatesAfterScaling(input);
570 
571     // Geometry can have negative coordinates, so it is necessary to subtract the upper left point, because coordinates on the widget are counted from 0
572     mPixmap = QPixmap(mScreensRect.width(), mScreensRect.height());
573     QPainter painter(&mPixmap);
574     for (auto it = mImages.constBegin(); it != mImages.constEnd(); ++it) {
575         painter.drawImage(pointsTranslationMap.value(it.key()->geometry().topLeft()) - mScreensRect.topLeft(), it.value());
576     }
577 }
578 
setGeometryToScreenPixmap(KWayland::Client::PlasmaShell * plasmashell)579 void QuickEditor::setGeometryToScreenPixmap(KWayland::Client::PlasmaShell *plasmashell)
580 {
581     if (!KWindowSystem::isPlatformX11()) {
582         setGeometry(mScreensRect);
583     } else {
584         // Even though we want the quick editor window to be placed at (0, 0) in the native
585         // pixels, we cannot really specify a window position of (0, 0) if HiDPI support is on.
586         //
587         // The main reason for that is that Qt will scale the window position relative to the
588         // upper left corner of the screen where the quick editor is on in order to perform
589         // a conversion from the device-independent coordinates to the native pixels.
590         //
591         // Since (0, 0) in the device-independent pixels may not correspond to (0, 0) in the
592         // native pixels, we use XCB API to place the quick editor window at (0, 0).
593 
594         uint16_t mask = 0;
595 
596         mask |= XCB_CONFIG_WINDOW_X;
597         mask |= XCB_CONFIG_WINDOW_Y;
598 
599         const uint32_t values[] = {
600             /* x */ 0,
601             /* y */ 0,
602         };
603 
604         xcb_configure_window(QX11Info::connection(), winId(), mask, values);
605         resize(qRound(mScreensRect.width() / devicePixelRatio), qRound(mScreensRect.height() / devicePixelRatio));
606     }
607 
608     // TODO This is a hack until a better interface is available
609     if (plasmashell) {
610         using namespace KWayland::Client;
611         winId();
612         auto surface = Surface::fromWindow(windowHandle());
613         if (surface) {
614             PlasmaShellSurface *plasmashellSurface = plasmashell->createSurface(surface, this);
615             plasmashellSurface->setRole(PlasmaShellSurface::Role::Panel);
616             plasmashellSurface->setPanelTakesFocus(true);
617             plasmashellSurface->setPosition(geometry().topLeft());
618         }
619     }
620 }
621 
paintEvent(QPaintEvent * event)622 void QuickEditor::paintEvent(QPaintEvent *event)
623 {
624     Q_UNUSED(event)
625 
626     QPainter painter(this);
627     painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
628     painter.eraseRect(rect());
629 
630     for (auto i = mImages.constBegin(); i != mImages.constEnd(); ++i) {
631         const QImage &screenImage = i.value();
632         const QScreen *screen = i.key();
633 
634         QRect rectToDraw = screen->geometry().translated(-mScreensRect.topLeft());
635         const qreal dpr = screenImage.width() / static_cast<qreal>(rectToDraw.width());
636         const qreal dprI = 1.0 / dpr;
637 
638         QBrush brush(screenImage);
639         brush.setTransform(QTransform::fromScale(dprI, dprI));
640 
641         rectToDraw.moveTopLeft(rectToDraw.topLeft() / devicePixelRatio);
642         if (KWindowSystem::isPlatformWayland()) {
643             rectToDraw.setSize(rectToDraw.size() * devicePixelRatio);
644         }
645 
646         painter.setBrushOrigin(rectToDraw.topLeft());
647         painter.fillRect(rectToDraw, brush);
648     }
649 
650     if (!mSelection.size().isEmpty() || mMouseDragState != MouseState::None) {
651         const QRectF innerRect = mSelection.adjusted(1, 1, -1, -1);
652         if (innerRect.width() > 0 && innerRect.height() > 0) {
653             painter.setPen(mStrokeColor);
654             painter.drawLine(mSelection.topLeft(), mSelection.topRight());
655             painter.drawLine(mSelection.bottomRight(), mSelection.topRight());
656             painter.drawLine(mSelection.bottomRight(), mSelection.bottomLeft());
657             painter.drawLine(mSelection.bottomLeft(), mSelection.topLeft());
658         }
659 
660         QRectF top(0, 0, width(), mSelection.top());
661         QRectF right(mSelection.right(), mSelection.top(), width() - mSelection.right(), mSelection.height());
662         QRectF bottom(0, mSelection.bottom() + 1, width(), height() - mSelection.bottom());
663         QRectF left(0, mSelection.top(), mSelection.left(), mSelection.height());
664         for (const auto &rect : {top, right, bottom, left}) {
665             painter.fillRect(rect, mMaskColor);
666         }
667 
668         bool dragHandlesVisible = false;
669         if (mMouseDragState == MouseState::None) {
670             dragHandlesVisible = true;
671             drawDragHandles(painter);
672         } else if (mMagnifierAllowed && (mShowMagnifier ^ mToggleMagnifier)) {
673             drawMagnifier(painter);
674         }
675         drawSelectionSizeTooltip(painter, dragHandlesVisible);
676         drawBottomHelpText(painter);
677     } else {
678         drawMidHelpText(painter);
679     }
680 }
681 
layoutBottomHelpText()682 void QuickEditor::layoutBottomHelpText()
683 {
684     int maxRightWidth = 0;
685     int contentWidth = 0;
686     int contentHeight = 0;
687     mBottomHelpGridLeftWidth = 0;
688     for (int i = 0; i < mbottomHelpLength; i++) {
689         const auto &item = mBottomHelpText[i];
690         const auto &left = item.first;
691         const auto &right = item.second;
692         const auto leftSize = left.size().toSize();
693         mBottomHelpGridLeftWidth = qMax(mBottomHelpGridLeftWidth, leftSize.width());
694         for (const auto &item : right) {
695             const auto rightItemSize = item.size().toSize();
696             maxRightWidth = qMax(maxRightWidth, rightItemSize.width());
697             contentHeight += rightItemSize.height();
698         }
699         contentWidth = qMax(contentWidth, mBottomHelpGridLeftWidth + maxRightWidth + bottomHelpBoxPairSpacing);
700         contentHeight += (i != bottomHelpMaxLength ? bottomHelpBoxMarginBottom : 0);
701     }
702     const QRect primaryGeometry = QGuiApplication::primaryScreen()->geometry().translated(-mScreensRect.topLeft());
703     mBottomHelpContentPos.setX((primaryGeometry.width() - contentWidth) / 2 + primaryGeometry.x() / devicePixelRatio);
704     mBottomHelpContentPos.setY((primaryGeometry.height() + primaryGeometry.y() / devicePixelRatio) - contentHeight - 8);
705     mBottomHelpGridLeftWidth += mBottomHelpContentPos.x();
706     mBottomHelpBorderBox.setRect(mBottomHelpContentPos.x() - bottomHelpBoxPaddingX,
707                                  mBottomHelpContentPos.y() - bottomHelpBoxPaddingY,
708                                  contentWidth + bottomHelpBoxPaddingX * 2,
709                                  contentHeight + bottomHelpBoxPaddingY * 2 - 1);
710 }
711 
setBottomHelpText()712 void QuickEditor::setBottomHelpText()
713 {
714     if (mReleaseToCapture && mSelection.size().isEmpty()) {
715         // Release to capture enabled and NO saved region available
716         mbottomHelpLength = 3;
717         mBottomHelpText[0] = {QStaticText(i18n("Take Screenshot:")),
718                               {QStaticText(i18nc("Mouse action", "Release left-click")), QStaticText(i18nc("Keyboard action", "Enter"))}};
719         mBottomHelpText[1] = {
720             QStaticText(i18n("Create new selection rectangle:")),
721             {QStaticText(i18nc("Mouse action", "Drag outside selection rectangle")), QStaticText(i18nc("Keyboard action", "+ Shift: Magnifier"))}};
722         mBottomHelpText[2] = {QStaticText(i18n("Cancel:")), {QStaticText(i18nc("Keyboard action", "Escape"))}};
723     } else {
724         // Default text, Release to capture option disabled
725         mBottomHelpText[0] = {QStaticText(i18n("Take Screenshot:")),
726                               {QStaticText(i18nc("Mouse action", "Double-click")), QStaticText(i18nc("Keyboard action", "Enter"))}};
727         mBottomHelpText[1] = {
728             QStaticText(i18n("Create new selection rectangle:")),
729             {QStaticText(i18nc("Mouse action", "Drag outside selection rectangle")), QStaticText(i18nc("Keyboard action", "+ Shift: Magnifier"))}};
730         mBottomHelpText[2] = {QStaticText(i18n("Move selection rectangle:")),
731                               {QStaticText(i18nc("Mouse action", "Drag inside selection rectangle")),
732                                QStaticText(i18nc("Keyboard action", "Arrow keys")),
733                                QStaticText(i18nc("Keyboard action", "+ Shift: Move in 1 pixel steps"))}};
734         mBottomHelpText[3] = {QStaticText(i18n("Resize selection rectangle:")),
735                               {QStaticText(i18nc("Mouse action", "Drag handles")),
736                                QStaticText(i18nc("Keyboard action", "Arrow keys + Alt")),
737                                QStaticText(i18nc("Keyboard action", "+ Shift: Resize in 1 pixel steps"))}};
738         mBottomHelpText[4] = {QStaticText(i18n("Reset selection:")), {QStaticText(i18nc("Mouse action", "Right-click"))}};
739         mBottomHelpText[5] = {QStaticText(i18n("Cancel:")), {QStaticText(i18nc("Keyboard action", "Escape"))}};
740     }
741 }
742 
drawBottomHelpText(QPainter & painter)743 void QuickEditor::drawBottomHelpText(QPainter &painter)
744 {
745     if (mSelection.intersects(mBottomHelpBorderBox)) {
746         return;
747     }
748 
749     painter.setBrush(mLabelBackgroundColor);
750     painter.setPen(mLabelForegroundColor);
751     painter.setFont(mBottomHelpTextFont);
752     painter.setRenderHint(QPainter::Antialiasing, false);
753     painter.drawRect(mBottomHelpBorderBox);
754     painter.setRenderHint(QPainter::Antialiasing, true);
755 
756     int topOffset = mBottomHelpContentPos.y();
757     for (int i = 0; i < mbottomHelpLength; i++) {
758         const auto &item = mBottomHelpText[i];
759         const auto &left = item.first;
760         const auto &right = item.second;
761         const auto leftSize = left.size().toSize();
762         painter.drawStaticText(mBottomHelpGridLeftWidth - leftSize.width(), topOffset, left);
763         for (const auto &item : right) {
764             const auto rightItemSize = item.size().toSize();
765             painter.drawStaticText(mBottomHelpGridLeftWidth + bottomHelpBoxPairSpacing, topOffset, item);
766             topOffset += rightItemSize.height();
767         }
768         if (i != bottomHelpMaxLength) {
769             topOffset += bottomHelpBoxMarginBottom;
770         }
771     }
772 }
773 
drawDragHandles(QPainter & painter)774 void QuickEditor::drawDragHandles(QPainter &painter)
775 {
776     // Rectangular region
777     const qreal left = mSelection.x();
778     const qreal centerX = left + mSelection.width() / 2.0;
779     const qreal right = left + mSelection.width();
780     const qreal top = mSelection.y();
781     const qreal centerY = top + mSelection.height() / 2.0;
782     const qreal bottom = top + mSelection.height();
783 
784     // rectangle too small: make handles free-floating
785     qreal offset = 0;
786     // rectangle too close to screen edges: move handles on that edge inside the rectangle, so they're still visible
787     qreal offsetTop = 0;
788     qreal offsetRight = 0;
789     qreal offsetBottom = 0;
790     qreal offsetLeft = 0;
791 
792     const qreal minDragHandleSpace = 4 * mHandleRadius + 2 * minSpacingBetweenHandles;
793     const qreal minEdgeLength = qMin(mSelection.width(), mSelection.height());
794     if (minEdgeLength < minDragHandleSpace) {
795         offset = (minDragHandleSpace - minEdgeLength) / 2.0;
796     } else {
797         const QRect translatedScreensRect = mScreensRect.translated(-mScreensRect.topLeft());
798         const int penWidth = painter.pen().width();
799 
800         offsetTop = top - translatedScreensRect.top() - mHandleRadius;
801         offsetTop = (offsetTop >= 0) ? 0 : offsetTop;
802 
803         offsetRight = translatedScreensRect.right() - right - mHandleRadius + penWidth;
804         offsetRight = (offsetRight >= 0) ? 0 : offsetRight;
805 
806         offsetBottom = translatedScreensRect.bottom() - bottom - mHandleRadius + penWidth;
807         offsetBottom = (offsetBottom >= 0) ? 0 : offsetBottom;
808 
809         offsetLeft = left - translatedScreensRect.left() - mHandleRadius;
810         offsetLeft = (offsetLeft >= 0) ? 0 : offsetLeft;
811     }
812 
813     // top-left handle
814     this->mHandlePositions[0] = QPointF{left - offset - offsetLeft, top - offset - offsetTop};
815     // top-right handle
816     this->mHandlePositions[1] = QPointF{right + offset + offsetRight, top - offset - offsetTop};
817     // bottom-right handle
818     this->mHandlePositions[2] = QPointF{right + offset + offsetRight, bottom + offset + offsetBottom};
819     // bottom-left
820     this->mHandlePositions[3] = QPointF{left - offset - offsetLeft, bottom + offset + offsetBottom};
821     // top-center handle
822     this->mHandlePositions[4] = QPointF{centerX, top - offset - offsetTop};
823     // right-center handle
824     this->mHandlePositions[5] = QPointF{right + offset + offsetRight, centerY};
825     // bottom-center handle
826     this->mHandlePositions[6] = QPointF{centerX, bottom + offset + offsetBottom};
827     // left-center handle
828     this->mHandlePositions[7] = QPointF{left - offset - offsetLeft, centerY};
829 
830     // start path
831     QPainterPath path;
832 
833     // add handles to the path
834     for (QPointF handlePosition : std::as_const(mHandlePositions)) {
835         path.addEllipse(handlePosition, mHandleRadius, mHandleRadius);
836     }
837 
838     // draw the path
839     painter.fillPath(path, mStrokeColor);
840 }
841 
drawMagnifier(QPainter & painter)842 void QuickEditor::drawMagnifier(QPainter &painter)
843 {
844     const int pixels = 2 * magPixels + 1;
845     int magX = static_cast<int>(mMousePos.x() * devicePixelRatio - magPixels);
846     int offsetX = 0;
847     if (magX < 0) {
848         offsetX = magX;
849         magX = 0;
850     } else {
851         const int maxX = mPixmap.width() - pixels;
852         if (magX > maxX) {
853             offsetX = magX - maxX;
854             magX = maxX;
855         }
856     }
857     int magY = static_cast<int>(mMousePos.y() * devicePixelRatio - magPixels);
858     int offsetY = 0;
859     if (magY < 0) {
860         offsetY = magY;
861         magY = 0;
862     } else {
863         const int maxY = mPixmap.height() - pixels;
864         if (magY > maxY) {
865             offsetY = magY - maxY;
866             magY = maxY;
867         }
868     }
869     QRectF magniRect(magX, magY, pixels, pixels);
870 
871     qreal drawPosX = mMousePos.x() + magOffset + pixels * magZoom / 2;
872     if (drawPosX > width() - pixels * magZoom / 2) {
873         drawPosX = mMousePos.x() - magOffset - pixels * magZoom / 2;
874     }
875     qreal drawPosY = mMousePos.y() + magOffset + pixels * magZoom / 2;
876     if (drawPosY > height() - pixels * magZoom / 2) {
877         drawPosY = mMousePos.y() - magOffset - pixels * magZoom / 2;
878     }
879     QPointF drawPos(drawPosX, drawPosY);
880     QRectF crossHairTop(drawPos.x() + magZoom * (offsetX - 0.5), drawPos.y() - magZoom * (magPixels + 0.5), magZoom, magZoom * (magPixels + offsetY));
881     QRectF crossHairRight(drawPos.x() + magZoom * (0.5 + offsetX), drawPos.y() + magZoom * (offsetY - 0.5), magZoom * (magPixels - offsetX), magZoom);
882     QRectF crossHairBottom(drawPos.x() + magZoom * (offsetX - 0.5), drawPos.y() + magZoom * (0.5 + offsetY), magZoom, magZoom * (magPixels - offsetY));
883     QRectF crossHairLeft(drawPos.x() - magZoom * (magPixels + 0.5), drawPos.y() + magZoom * (offsetY - 0.5), magZoom * (magPixels + offsetX), magZoom);
884     QRectF crossHairBorder(drawPos.x() - magZoom * (magPixels + 0.5) - 1,
885                            drawPos.y() - magZoom * (magPixels + 0.5) - 1,
886                            pixels * magZoom + 2,
887                            pixels * magZoom + 2);
888     const auto frag = QPainter::PixmapFragment::create(drawPos, magniRect, magZoom, magZoom);
889 
890     painter.fillRect(crossHairBorder, mLabelForegroundColor);
891     painter.drawPixmapFragments(&frag, 1, mPixmap, QPainter::OpaqueHint);
892     painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
893     for (auto &rect : {crossHairTop, crossHairRight, crossHairBottom, crossHairLeft}) {
894         painter.fillRect(rect, mCrossColor);
895     }
896 }
897 
drawMidHelpText(QPainter & painter)898 void QuickEditor::drawMidHelpText(QPainter &painter)
899 {
900     painter.fillRect(rect(), mMaskColor);
901     painter.setFont(mMidHelpTextFont);
902     QRect textSize = painter.boundingRect(QRect(), Qt::AlignCenter, mMidHelpText);
903     const QRect primaryGeometry = QGuiApplication::primaryScreen()->geometry().translated(-mScreensRect.topLeft());
904     QPoint pos((primaryGeometry.width() - textSize.width()) / 2 + primaryGeometry.x() / devicePixelRatio,
905                (primaryGeometry.height() - textSize.height()) / 2 + primaryGeometry.y() / devicePixelRatio);
906 
907     painter.setBrush(mLabelBackgroundColor);
908     QPen pen(mLabelForegroundColor);
909     pen.setWidth(2);
910     painter.setPen(pen);
911     painter.drawRoundedRect(QRect(pos.x() - 20, pos.y() - 20, textSize.width() + 40, textSize.height() + 40), 4, 4);
912 
913     painter.setCompositionMode(QPainter::CompositionMode_Source);
914     painter.drawText(QRect(pos, textSize.size()), Qt::AlignCenter, mMidHelpText);
915 }
916 
drawSelectionSizeTooltip(QPainter & painter,bool dragHandlesVisible)917 void QuickEditor::drawSelectionSizeTooltip(QPainter &painter, bool dragHandlesVisible)
918 {
919     // Set the selection size and finds the most appropriate position:
920     // - vertically centered inside the selection if the box is not covering the a large part of selection
921     // - on top of the selection if the selection x position fits the box height plus some margin
922     // - at the bottom otherwise
923     QString selectionSizeText =
924         ki18n("%1×%2").subs(qRound(mSelection.width() * devicePixelRatio)).subs(qRound(mSelection.height() * devicePixelRatio)).toString();
925     const QRect selectionSizeTextRect = painter.boundingRect(QRect(), 0, selectionSizeText);
926 
927     const int selectionBoxWidth = selectionSizeTextRect.width() + selectionBoxPaddingX * 2;
928     const int selectionBoxHeight = selectionSizeTextRect.height() + selectionBoxPaddingY * 2;
929     const int selectionBoxX =
930         qBound(0,
931                static_cast<int>(mSelection.x()) + (static_cast<int>(mSelection.width()) - selectionSizeTextRect.width()) / 2 - selectionBoxPaddingX,
932                width() - selectionBoxWidth);
933     int selectionBoxY;
934     if ((mSelection.width() >= selectionSizeThreshold) && (mSelection.height() >= selectionSizeThreshold)) {
935         // show inside the box
936         selectionBoxY = static_cast<int>(mSelection.y() + (mSelection.height() - selectionSizeTextRect.height()) / 2);
937     } else {
938         // show on top by default, above the drag Handles if they're visible
939         if (dragHandlesVisible) {
940             selectionBoxY = static_cast<int>(mHandlePositions[4].y() - mHandleRadius - selectionBoxHeight - selectionBoxMarginY);
941             if (selectionBoxY < 0) {
942                 selectionBoxY = static_cast<int>(mHandlePositions[6].y() + mHandleRadius + selectionBoxMarginY);
943             }
944         } else {
945             selectionBoxY = static_cast<int>(mSelection.y() - selectionBoxHeight - selectionBoxMarginY);
946             if (selectionBoxY < 0) {
947                 selectionBoxY = static_cast<int>(mSelection.y() + mSelection.height() + selectionBoxMarginY);
948             }
949         }
950     }
951 
952     // Now do the actual box, border, and text drawing
953     painter.setBrush(mLabelBackgroundColor);
954     painter.setPen(mLabelForegroundColor);
955     const QRect selectionBoxRect(selectionBoxX, selectionBoxY, selectionBoxWidth, selectionBoxHeight);
956 
957     painter.setRenderHint(QPainter::Antialiasing, false);
958     painter.drawRect(selectionBoxRect);
959     painter.setRenderHint(QPainter::Antialiasing, true);
960     painter.drawText(selectionBoxRect, Qt::AlignCenter, selectionSizeText);
961 }
962 
setMouseCursor(const QPointF & pos)963 void QuickEditor::setMouseCursor(const QPointF &pos)
964 {
965     MouseState mouseState = mouseLocation(pos);
966     if (mouseState == MouseState::Outside) {
967         setCursor(Qt::CrossCursor);
968     } else if (MouseState::TopLeftOrBottomRight & mouseState) {
969         setCursor(Qt::SizeFDiagCursor);
970     } else if (MouseState::TopRightOrBottomLeft & mouseState) {
971         setCursor(Qt::SizeBDiagCursor);
972     } else if (MouseState::TopOrBottom & mouseState) {
973         setCursor(Qt::SizeVerCursor);
974     } else if (MouseState::RightOrLeft & mouseState) {
975         setCursor(Qt::SizeHorCursor);
976     } else {
977         setCursor(Qt::OpenHandCursor);
978     }
979 }
980 
mouseLocation(const QPointF & pos)981 QuickEditor::MouseState QuickEditor::mouseLocation(const QPointF &pos)
982 {
983     auto isPointInsideCircle = [](const QPointF &circleCenter, qreal radius, const QPointF &point) {
984         return (qPow(point.x() - circleCenter.x(), 2) + qPow(point.y() - circleCenter.y(), 2) <= qPow(radius, 2)) ? true : false;
985     };
986 
987     if (isPointInsideCircle(mHandlePositions[0], mHandleRadius * increaseDragAreaFactor, pos)) {
988         return MouseState::TopLeft;
989     }
990     if (isPointInsideCircle(mHandlePositions[1], mHandleRadius * increaseDragAreaFactor, pos)) {
991         return MouseState::TopRight;
992     }
993     if (isPointInsideCircle(mHandlePositions[2], mHandleRadius * increaseDragAreaFactor, pos)) {
994         return MouseState::BottomRight;
995     }
996     if (isPointInsideCircle(mHandlePositions[3], mHandleRadius * increaseDragAreaFactor, pos)) {
997         return MouseState::BottomLeft;
998     }
999     if (isPointInsideCircle(mHandlePositions[4], mHandleRadius * increaseDragAreaFactor, pos)) {
1000         return MouseState::Top;
1001     }
1002     if (isPointInsideCircle(mHandlePositions[5], mHandleRadius * increaseDragAreaFactor, pos)) {
1003         return MouseState::Right;
1004     }
1005     if (isPointInsideCircle(mHandlePositions[6], mHandleRadius * increaseDragAreaFactor, pos)) {
1006         return MouseState::Bottom;
1007     }
1008     if (isPointInsideCircle(mHandlePositions[7], mHandleRadius * increaseDragAreaFactor, pos)) {
1009         return MouseState::Left;
1010     }
1011 
1012     auto inRange = [](qreal low, qreal high, qreal value) {
1013         return value >= low && value <= high;
1014     };
1015 
1016     auto withinThreshold = [](qreal offset, qreal threshold) {
1017         return qFabs(offset) <= threshold;
1018     };
1019 
1020     // Rectangle can be resized when border is dragged, if it's big enough
1021     if (mSelection.width() >= 100 && mSelection.height() >= 100) {
1022         if (inRange(mSelection.x(), mSelection.x() + mSelection.width(), pos.x())) {
1023             if (withinThreshold(pos.y() - mSelection.y(), borderDragAreaSize)) {
1024                 return MouseState::Top;
1025             }
1026             if (withinThreshold(pos.y() - mSelection.y() - mSelection.height(), borderDragAreaSize)) {
1027                 return MouseState::Bottom;
1028             }
1029         }
1030         if (inRange(mSelection.y(), mSelection.y() + mSelection.height(), pos.y())) {
1031             if (withinThreshold(pos.x() - mSelection.x(), borderDragAreaSize)) {
1032                 return MouseState::Left;
1033             }
1034             if (withinThreshold(pos.x() - mSelection.x() - mSelection.width(), borderDragAreaSize)) {
1035                 return MouseState::Right;
1036             }
1037         }
1038     }
1039     if (mSelection.contains(pos.toPoint())) {
1040         return MouseState::Inside;
1041     }
1042     return MouseState::Outside;
1043 }
1044