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