diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -25,7 +25,6 @@ Gui/SettingsDialog/SaveOptionsPage.cpp Gui/SettingsDialog/GeneralOptionsPage.cpp QuickEditor/QuickEditor.cpp - QuickEditor/QmlResources.qrc ) ecm_qt_declare_logging_category(SPECTACLE_SRCS_DEFAULT HEADER spectacle_core_debug.h IDENTIFIER SPECTACLE_CORE_LOG CATEGORY_NAME org.kde.spectacle.core) diff --git a/src/Gui/KSMainWindow.cpp b/src/Gui/KSMainWindow.cpp --- a/src/Gui/KSMainWindow.cpp +++ b/src/Gui/KSMainWindow.cpp @@ -299,15 +299,15 @@ void KSMainWindow::openScreenshotsFolder() { - // Highlight last screenshot in file manager if user saved at least once ever - // (since last save and saveas file names are stored in spectaclerc), otherwise, + // Highlight last screenshot in file manager if user saved at least once ever + // (since last save and saveas file names are stored in spectaclerc), otherwise, // if in save mode, open default save location from configure > save > location // if in save as mode, open last save as files location // failsafe for either option is default save location from configure > save > location SpectacleConfig *cfgManager = SpectacleConfig::instance(); ExportManager *exportManager = ExportManager::instance(); QUrl location; - + switch(cfgManager->lastUsedSaveMode()) { case SaveMode::Save: location = cfgManager->lastSaveFile(); diff --git a/src/PlatformBackends/X11ImageGrabber.h b/src/PlatformBackends/X11ImageGrabber.h --- a/src/PlatformBackends/X11ImageGrabber.h +++ b/src/PlatformBackends/X11ImageGrabber.h @@ -66,7 +66,7 @@ private Q_SLOTS: void KWinDBusScreenshotHelper(quint64 window); - void rectangleSelectionConfirmed(const QPixmap &pixmap, const QRect ®ion); + void rectangleSelectionConfirmed(const QPixmap &pixmap); void rectangleSelectionCancelled(); public Q_SLOTS: diff --git a/src/PlatformBackends/X11ImageGrabber.cpp b/src/PlatformBackends/X11ImageGrabber.cpp --- a/src/PlatformBackends/X11ImageGrabber.cpp +++ b/src/PlatformBackends/X11ImageGrabber.cpp @@ -416,17 +416,13 @@ emit imageGrabFailed(); } -void X11ImageGrabber::rectangleSelectionConfirmed(const QPixmap &pixmap, const QRect ®ion) +void X11ImageGrabber::rectangleSelectionConfirmed(const QPixmap &pixmap) { QObject *sender = QObject::sender(); sender->disconnect(); sender->deleteLater(); - if (mCapturePointer) { - mPixmap = blendCursorImage(pixmap, region.x(), region.y(), region.width(), region.height()); - } else { - mPixmap = pixmap; - } + mPixmap = pixmap; emit pixmapChanged(mPixmap); } @@ -664,7 +660,7 @@ void X11ImageGrabber::grabRectangularRegion() { - const auto pixmap = getToplevelPixmap(QRect(), false); + const auto pixmap = getToplevelPixmap(QRect(), mCapturePointer); if (!pixmap.isNull()) { QuickEditor *editor = new QuickEditor(pixmap); diff --git a/src/QuickEditor/EditorRoot.qml b/src/QuickEditor/EditorRoot.qml deleted file mode 100644 --- a/src/QuickEditor/EditorRoot.qml +++ /dev/null @@ -1,615 +0,0 @@ -/* - * Copyright (C) 2016 Boudhayan Gupta - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -import QtQuick 2.5 -import QtQuick.Window 2.2 -import QtQuick.Layouts 1.3 -import QtQuick.Controls 1.4 - -Item { - id: editorRoot; - objectName: "editorRoot"; - - // properties and setters - - property var selection: undefined; - property color maskColour: Qt.rgba(0, 0, 0, 0.15); - property color strokeColour: systemPalette.highlight; - property color crossColour: Qt.rgba(strokeColour.r, strokeColour.g, strokeColour.b, 0.7); - property color labelBackgroundColour: Qt.rgba(systemPalette.light.r, systemPalette.light.g, systemPalette.light.b, 0.85); - property bool showMagnifier: false; - property bool toggleMagnifier: false; - property int magZoom: 5; - property int magPixels: 16; - property int magOffset: 32; - property double largeChange: 15; - property double smallChange: 1 / Screen.devicePixelRatio; - property bool resize: false; // toggle for resize versus move actions - - SystemPalette { - id: systemPalette; - } - - function setInitialSelection(xx, yy, ww, hh) { - if (selection) { - selection.destroy(); - } - - selection = cropRectangle.createObject(parent, { - "x": xx, - "y": yy, - "height": hh, - "width": ww - }); - - cropDisplayCanvas.requestPaint(); - } - - function accept() { - if (selection) { - acceptImage(selection.x, selection.y, selection.width, selection.height); - } else { - acceptImage(-1, -1, -1, -1); - } - } - - // key handlers - - focus: true; - - Keys.onReturnPressed: accept() - Keys.onEnterPressed: accept() - - Keys.onEscapePressed: { - cancelImage(); - } - - Keys.onPressed: { - - const screenMaxX = cropDisplayCanvas.width; - const screenMaxY = cropDisplayCanvas.height; - const minRectHeight = 20; - const minRectWidth = 20; - - // shift key alone = magnifier toggle - if (event.modifiers & Qt.ShiftModifier) { - toggleMagnifier = true; - } - - // nested switches for arrow keys based on modifier keys - switch(event.modifiers) { - - case Qt.NoModifier: - resize = false; - switch (event.key) { - - case Qt.Key_Left: - moveSizeRect(-largeChange, 0.0, "left"); - break; - case Qt.Key_Right: - moveSizeRect(largeChange, 0.0, "right"); - break; - case Qt.Key_Up: - moveSizeRect(0.0, -largeChange, "up"); - break; - case Qt.Key_Down: - moveSizeRect(0.0, largeChange, "down"); - break; - } - - break; // end no modifier (just arrows - large move) - - case Qt.ShiftModifier: - resize = false; - switch (event.key) { - - case Qt.Key_Left: - moveSizeRect(-smallChange, 0.0, "left"); - break; - - case Qt.Key_Right: - moveSizeRect(smallChange, 0.0, "right"); - break; - - case Qt.Key_Up: - moveSizeRect(0.0, -smallChange, "up"); - break; - - case Qt.Key_Down: - moveSizeRect(0.0, smallChange, "down"); - break; - } - - break; // end Shift + arrows (small move) - - case Qt.AltModifier: - resize = true; - switch (event.key) { - - case Qt.Key_Left: - moveSizeRect(-largeChange, 0.0, "left"); - break; - - case Qt.Key_Right: - moveSizeRect(largeChange, 0.0, "right"); - break; - - case Qt.Key_Up: - moveSizeRect(0.0, -largeChange, "up"); - break; - - case Qt.Key_Down: - moveSizeRect(0.0, largeChange, "down"); - break; - } - - break; // end ALT + arrows (resize rectangle - large change) - - case (Qt.ShiftModifier + Qt.AltModifier): - resize = true; - switch (event.key) { - - case Qt.Key_Left: - moveSizeRect(-smallChange, 0.0, "left"); - break; - - case Qt.Key_Right: - moveSizeRect(smallChange, 0.0, "right"); - break; - - case Qt.Key_Up: - moveSizeRect(0.0, -smallChange, "up"); - break; - - case Qt.Key_Down: - moveSizeRect(0.0, smallChange, "down"); - break; - } - - break; // end Shift + ALT + arrows (small resize rectangle) - - } - - - function moveSizeRect(changeX, changeY, direction) { - - var action; - var newX; - var newY; - var newRight; - var newBottom; - - switch (resize) { - case false: // move - - switch (direction) { - case "left": - newX = selection.x += changeX; - action = (newX >= 0.0) ? selection.x = newX : selection.x = 0.0; - break; - case "right": - newRight = (selection.x + selection.width) + changeX; - action = (newRight <= screenMaxX) ? selection.x += changeX : selection.x = screenMaxX - selection.width; - break; - case "up": - newY = selection.y += changeY; - action = (newY >= 0.0) ? selection.y = newY : selection.y = 0.0; - break; - case "down": - newBottom = selection.y + selection.height + changeY; - action = (newBottom <= screenMaxY) ? selection.y = selection.y + changeY : selection.y = screenMaxY - selection.height; - break; - } - - break; - // end of movement switch cases - - - case true: // resizing rectangle - - switch (direction) { - case "left": - newX = selection.width += changeX; - if (newX <= minRectWidth) { - selection.width = minRectWidth; - break; - } - action = (newX >= 0.0) ? selection.width + changeX : selection.x = 0.0; - break; - - case "right": - newX = selection.width += changeX - action = (newX <= screenMaxX) ? selection.width + changeX : selection.width = (selection.x + screenMaxX); - break; - - case "up": - newY = selection.height += changeY; - if (newY <= minRectHeight) { - selection.height = minRectHeight; - break; - } - action = (newY >= 0.0) ? selection.height = newY : selection.y = 0.0; - break; - - case "down": - newY = selection.height += changeY; - action = (newY <= screenMaxY) ? selection.height + changeY : selection.height = (selection.y + screenMaxY); - break; - } - - } - } - - // all switches done; repaint on any keypress - cropDisplayCanvas.requestPaint(); - - } // end Keys.onPressed - - - Keys.onReleased: { - if (toggleMagnifier && !(event.modifiers & Qt.ShiftModifier)) { - toggleMagnifier = false; - cropDisplayCanvas.requestPaint(); - } - } - - // signals - - signal acceptImage(int x, int y, int width, int height); - signal cancelImage(); - - Image { - id: imageBackground; - objectName: "imageBackground"; - - source: "image://snapshot/rawimage"; - cache: false; - - height: Window.height / Screen.devicePixelRatio; - width: Window.width / Screen.devicePixelRatio; - fillMode: Image.PreserveAspectFit; - } - - Canvas { - id: cropDisplayCanvas; - objectName: "cropDisplayCanvas"; - anchors.fill: imageBackground; - - renderTarget: Canvas.FramebufferObject; - renderStrategy: Canvas.Cooperative; - - onPaint: { - // start by getting a context on the canvas and clearing it - var ctx = cropDisplayCanvas.getContext("2d"); - ctx.clearRect(0, 0, cropDisplayCanvas.width, cropDisplayCanvas.height); - - // set up the colours - ctx.strokeStyle = strokeColour; - ctx.fillStyle = maskColour; - - // draw a sheet over the whole screen - ctx.fillRect(0, 0, cropDisplayCanvas.width, cropDisplayCanvas.height); - - if (selection) { - midHelpText.visible = false; - // display bottom help text only if it does not intersect with the selection - bottomHelpText.visible = (selection.y + selection.height < bottomHelpText.y) || (selection.x > bottomHelpText.x + bottomHelpText.width) || (selection.x + selection.width < bottomHelpText.x); - - // if we have a selection polygon, cut it out - ctx.fillStyle = strokeColour; - ctx.fillRect(selection.x, selection.y, selection.width, selection.height); - ctx.clearRect(selection.x + 1, selection.y + 1, selection.width - 2, selection.height - 2); - - if ((selection.width > 20) && (selection.height > 20)) { - // top-left handle - ctx.beginPath(); - ctx.arc(selection.x, selection.y, 8, 0, 0.5 * Math.PI); - ctx.lineTo(selection.x, selection.y); - ctx.fill(); - - // top-right handle - ctx.beginPath(); - ctx.arc(selection.x + selection.width, selection.y, 8, 0.5 * Math.PI, Math.PI); - ctx.lineTo(selection.x + selection.width, selection.y); - ctx.fill(); - - // bottom-left handle - ctx.beginPath(); - ctx.arc(selection.x + selection.width, selection.y + selection.height, 8, Math.PI, 1.5 * Math.PI); - ctx.lineTo(selection.x + selection.width, selection.y + selection.height); - ctx.fill(); - - // bottom-right handle - ctx.beginPath(); - ctx.arc(selection.x, selection.y + selection.height, 8, 1.5 * Math.PI, 2 * Math.PI); - ctx.lineTo(selection.x, selection.y + selection.height); - ctx.fill(); - - // top-center handle - ctx.beginPath(); - ctx.arc(selection.x + selection.width / 2, selection.y, 5, 0, Math.PI); - ctx.fill(); - - // right-center handle - ctx.beginPath(); - ctx.arc(selection.x + selection.width, selection.y + selection.height / 2, 5, 0.5 * Math.PI, 1.5 * Math.PI); - ctx.fill(); - - // bottom-center handle - ctx.beginPath(); - ctx.arc(selection.x + selection.width / 2, selection.y + selection.height, 5, Math.PI, 2 * Math.PI); - ctx.fill(); - - // left-center handle - ctx.beginPath(); - ctx.arc(selection.x, selection.y + selection.height / 2, 5, 1.5 * Math.PI, 0.5 * Math.PI); - ctx.fill(); - } - - // Set the selection size and finds the most appropriate position: - // - vertically centered inside the selection if the box is not covering the a large part of selection - // - on top of the selection if the selection x position fits the box height plus some margin - // - at the bottom otherwise - // Note that text is drawn starting from the left bottom! - var selectionText = Math.round(selection.width * Screen.devicePixelRatio) + "x" + Math.round(selection.height * Screen.devicePixelRatio); - selectionTextMetrics.font = ctx.font; - selectionTextMetrics.text = selectionText; - var selectionTextRect = selectionTextMetrics.boundingRect; - var selectionBoxX = Math.max(0, selection.x + (selection.width - selectionTextRect.width) / 2); - var selectionBoxY; - if ((selection.width > 100) && (selection.height > 100)) { - // show inside the box - selectionBoxY = selection.y + (selection.height + selectionTextRect.height) / 2; - } else if (selection.y >= selectionTextRect.height + 8) { - // show on top - selectionBoxY = selection.y - 8; - } else { - // show at the bottom - selectionBoxY = selection.y + selection.height + selectionTextRect.height + 4; - } - // Now do the actual box, border and text drawing - ctx.fillStyle = systemPalette.window; - ctx.strokeStyle = systemPalette.windowText; - ctx.fillRect(selectionBoxX - 4, selectionBoxY - selectionTextRect.height - 2, selectionTextRect.width + 10, selectionTextRect.height + 8); - ctx.strokeRect(selectionBoxX - 4, selectionBoxY - selectionTextRect.height - 2, selectionTextRect.width + 10, selectionTextRect.height + 8); - ctx.fillStyle = systemPalette.windowText; - ctx.fillText(selectionText, selectionBoxX, selectionBoxY); - if (selection.zoomCenterX >= 0 && selection.zoomCenterY >= 0 && (showMagnifier ^ toggleMagnifier)) { - var offsetX = magOffset; - var offsetY = magOffset; - var magX = selection.zoomCenterX; - var magY = selection.zoomCenterY; - var magWidth = crossMagnifier.width; - var magHeight = crossMagnifier.height; - - if (magX + offsetX + magWidth >= Window.width / Screen.devicePixelRatio) { - offsetX -= offsetX * 2 + magWidth; - } - - if (magY + offsetY + magHeight >= Window.height / Screen.devicePixelRatio) { - offsetY -= offsetY * 2 + magHeight; - } - - magX += offsetX; - magY += offsetY; - crossMagnifier.visible = true; - crossMagnifier.x = magX; - crossMagnifier.y = magY; - crossBackground.x = -selection.zoomCenterX * Screen.devicePixelRatio * magZoom + magPixels * magZoom; - crossBackground.y = -selection.zoomCenterY * Screen.devicePixelRatio * magZoom + magPixels * magZoom; - ctx.strokeRect(magX, magY, magWidth, magHeight); - } else { - crossMagnifier.visible = false; - } - } else { - midHelpText.visible = true; - bottomHelpText.visible = false; - crossMagnifier.visible = false; - } - } - - TextMetrics { - id: selectionTextMetrics - } - - Rectangle { - id: midHelpText; - objectName: "midHelpText"; - - height: midHelpTextElement.height + 40; - width: midHelpTextElement.width + 40; - radius: 4; - border.color: systemPalette.windowText; - color: labelBackgroundColour; - - visible: false; - anchors.centerIn: parent; - - TextLabel { - id: midHelpTextElement; - text: i18n("Click anywhere to start drawing a selection rectangle,\nor press Esc to cancel."); - font.pixelSize: Qt.application.font.pixelSize * 1.2; - - anchors.centerIn: parent; - } - } - - Rectangle { - id: bottomHelpText; - objectName: "bottomHelpText"; - - height: bottomHelpTextElement.height + 20; - width: bottomHelpTextElement.width + 20; - radius: 4; - border.color: systemPalette.windowText; - color: labelBackgroundColour; - - visible: false; - anchors.bottom: parent.bottom; - anchors.horizontalCenter: parent.horizontalCenter; - - GridLayout { - id: bottomHelpTextElement; - columns: 2 - anchors.centerIn: parent; - - TextLabel { - text: i18n("Enter, double-click:"); - Layout.alignment: Qt.AlignRight; - } - TextLabel { text: i18n("Take screenshot"); } - - TextLabel { - text: i18n("Shift:"); - Layout.alignment: Qt.AlignRight | Qt.AlignTop; - } - TextLabel { text: i18n("Hold to toggle magnifier\nwhile dragging selection handles"); } - - TextLabel { - text: i18n("Arrow keys:"); - Layout.alignment: Qt.AlignRight | Qt.AlignTop; - } - TextLabel { text: i18n("Move selection rectangle.\nHold Alt to resize, Shift to fine-tune"); } - - TextLabel { - text: i18n("Right-click:"); - Layout.alignment: Qt.AlignRight; - } - TextLabel { text: i18n("Reset selection"); } - - TextLabel { - text: i18n("Esc:"); - Layout.alignment: Qt.AlignRight; - } - TextLabel { text: i18n("Cancel"); } - } - } - - // Use Rectangle so that the background is white when cursor nearby edge - Rectangle { - id: crossMagnifier; - - height: (magPixels * 2 + 1) * magZoom; - width: height; - border.width: 0; - visible: false; - clip: true - - Image { - id: crossBackground; - source: "image://snapshot/rawimage"; - smooth: false; - height: Window.height * magZoom; - width: Window.width * magZoom; - } - - Rectangle { - x: magPixels * magZoom; - y: 0; - width: magZoom; - height: magPixels * magZoom; - color: crossColour; - } - - Rectangle { - x: magPixels * magZoom; - y: (magPixels + 1) * magZoom; - width: magZoom; - height: magPixels * magZoom; - color: crossColour; - } - - Rectangle { - x: 0; - y: magPixels * magZoom; - width: magPixels * magZoom; - height: magZoom; - color: crossColour; - } - - Rectangle { - x: (magPixels + 1) * magZoom; - y: magPixels * magZoom; - width: magPixels * magZoom; - height: magZoom; - color: crossColour; - } - } - } - - MouseArea { - anchors.fill: imageBackground; - - property int startx: 0; - property int starty: 0; - - cursorShape: Qt.CrossCursor; - acceptedButtons: Qt.LeftButton | Qt.RightButton; - - onPressed: { - if (selection) { - selection.destroy(); - } - - startx = mouse.x; - starty = mouse.y; - - selection = cropRectangle.createObject(parent, { - "x": startx, - "y": starty, - "height": 0, - "width": 0 - }); - } - - onPositionChanged: { - selection.x = Math.min(startx, mouse.x); - selection.y = Math.min(starty, mouse.y); - selection.width = Math.abs(startx - mouse.x) + 1; - selection.height = Math.abs(starty - mouse.y) + 1; - selection.zoomCenterX = mouse.x; - selection.zoomCenterY = mouse.y; - cropDisplayCanvas.requestPaint(); - } - - onClicked: { - if ((mouse.button == Qt.RightButton) && (selection)) { - selection.destroy(); - cropDisplayCanvas.requestPaint(); - } - } - - onReleased: { - selection.zoomCenterX = -1; - selection.zoomCenterY = -1; - cropDisplayCanvas.requestPaint(); - } - } - - Component { - id: cropRectangle; - - SelectionRectangle { - drawCanvas: cropDisplayCanvas; - imageElement: imageBackground; - - onDoubleClicked: editorRoot.accept() - } - } -} diff --git a/src/QuickEditor/QmlResources.qrc b/src/QuickEditor/QmlResources.qrc deleted file mode 100644 --- a/src/QuickEditor/QmlResources.qrc +++ /dev/null @@ -1,7 +0,0 @@ - - - EditorRoot.qml - SelectionRectangle.qml - TextLabel.qml - - diff --git a/src/QuickEditor/QuickEditor.h b/src/QuickEditor/QuickEditor.h --- a/src/QuickEditor/QuickEditor.h +++ b/src/QuickEditor/QuickEditor.h @@ -1,5 +1,6 @@ /* * Copyright (C) 2016 Boudhayan Gupta + * Copyright (C) 2018 Ambareesh "Amby" Balaji * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by @@ -20,34 +21,111 @@ #ifndef QUICKEDITOR_H #define QUICKEDITOR_H -#include +#include +#include +#include +#include +#include +#include -class QuickEditor : public QObject +class QMouseEvent; + +class QuickEditor : public QWidget { Q_OBJECT - public: +public: + explicit QuickEditor(const QPixmap &pixmap); - explicit QuickEditor(const QPixmap &pixmap, QObject *parent = nullptr); - virtual ~QuickEditor(); +private: + enum MouseState : short { + None = 0, // 0000 + Inside = 1 << 0, // 0001 + Outside = 1 << 1, // 0010 + TopLeft = 5, //101 + Top = 17, // 10001 + TopRight = 9, // 1001 + Right = 33, // 100001 + BottomRight = 6, // 110 + Bottom = 18, // 10010 + BottomLeft = 10, // 1010 + Left = 34, // 100010 + TopLeftOrBottomRight = TopLeft & BottomRight, // 100 + TopRightOrBottomLeft = TopRight & BottomLeft, // 1000 + TopOrBottom = Top & Bottom, // 10000 + RightOrLeft = Right & Left, // 100000 + }; - Q_SIGNALS: + void acceptSelection(); + int boundsLeft(int newTopLeftX, const bool mouse = true); + int boundsRight(int newTopLeftX, const bool mouse = true); + int boundsUp(int newTopLeftY, const bool mouse = true); + int boundsDown(int newTopLeftY, const bool mouse = true); + void keyPressEvent(QKeyEvent* event) override; + void keyReleaseEvent(QKeyEvent* event) override; + void mousePressEvent(QMouseEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; + void mouseReleaseEvent(QMouseEvent* event) override; + void mouseDoubleClickEvent(QMouseEvent* event) override; + void paintEvent(QPaintEvent*) override; + void drawBottomHelpText(QPainter& painter); + void drawDragHandles(QPainter& painter); + void drawMagnifier(QPainter& painter); + void drawMidHelpText(QPainter& painter); + void drawSelectionSizeTooltip(QPainter& painter); + void layoutBottomHelpText(); + void setMouseCursor(const QPointF& pos); + MouseState mouseLocation(const QPointF& pos); - void grabDone(const QPixmap &pixmap, const QRect &cropRegion); - void grabCancelled(); + static const qreal mouseAreaSize; + static const qreal cornerHandleRadius; + static const qreal midHandleRadius; + static const int selectionSizeThreshold; - private Q_SLOTS: + static const int selectionBoxPaddingX; + static const int selectionBoxPaddingY; + static const int selectionBoxMarginY; - void acceptImageHandler(int x, int y, int width, int height); + static const int bottomHelpLength = 5; + static std::pair> bottomHelpText[bottomHelpLength]; + static bool bottomHelpTextPrepared; + static const int bottomHelpBoxPaddingX; + static const int bottomHelpBoxPaddingY; + static const int bottomHelpBoxPairSpacing; + static const int bottomHelpBoxMarginBottom; + static const int midHelpTextFontSize; - private: + static const int magnifierLargeStep; - struct ImageStore; - ImageStore *mImageStore; + static const int magZoom; + static const int magPixels; + static const int magOffset; - class QuickEditorPrivate; - Q_DECLARE_PRIVATE(QuickEditor) - QuickEditorPrivate *d_ptr; + QColor mMaskColor; + QColor mStrokeColor; + QColor mCrossColor; + QColor mLabelBackgroundColor; + QColor mLabelForegroundColor; + QRectF mSelection; + QPointF mStartPos; + QPointF mInitialTopLeft; + QString mMidHelpText; + QFont mMidHelpTextFont; + QFont mBottomHelpTextFont; + QRect mBottomHelpBorderBox; + QPoint mBottomHelpContentPos; + int mBottomHelpGridLeftWidth; + MouseState mMouseDragState; + QPixmap mPixmap; + qreal dprI; + QPointF mMousePos; + bool mMagnifierAllowed; + bool mShowMagnifier; + bool mToggleMagnifier; + +Q_SIGNALS: + void grabDone(const QPixmap &pixmap); + void grabCancelled(); }; #endif // QUICKEDITOR_H diff --git a/src/QuickEditor/QuickEditor.cpp b/src/QuickEditor/QuickEditor.cpp --- a/src/QuickEditor/QuickEditor.cpp +++ b/src/QuickEditor/QuickEditor.cpp @@ -1,5 +1,6 @@ /* * Copyright (C) 2016 Boudhayan Gupta + * Copyright (C) 2018 Ambareesh "Amby" Balaji * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by @@ -17,135 +18,756 @@ * Boston, MA 02110-1301, USA. */ -#include "QuickEditor.h" +#include +#include "QuickEditor.h" #include "SpectacleConfig.h" -#include -#include +const qreal QuickEditor::mouseAreaSize = 20.0; +const qreal QuickEditor::cornerHandleRadius = 8.0; +const qreal QuickEditor::midHandleRadius = 5.0; +const int QuickEditor::selectionSizeThreshold = 100; + +const int QuickEditor::selectionBoxPaddingX = 5; +const int QuickEditor::selectionBoxPaddingY = 4; +const int QuickEditor::selectionBoxMarginY = 2; + +std::pair> QuickEditor::bottomHelpText[]{ + {QStaticText(i18n("Enter, double-click:")), {QStaticText(i18n("Take screenshot"))}}, + {QStaticText(i18n("Shift:")), { + QStaticText(i18n("Hold to toggle magnifier")), + QStaticText(i18n("while dragging selection handles")) + }}, + {QStaticText(i18n("Arrow keys:")), { + QStaticText(i18n("Move seletion rectangle")), + QStaticText(i18n("Hold Alt to resize, Shift to fine‑tune")) + }}, + {QStaticText(i18n("Right-click:")), {QStaticText(i18n("Reset selection"))}}, + {QStaticText(i18n("Esc:")), {QStaticText(i18n("Cancel"))}}, +}; +bool QuickEditor::bottomHelpTextPrepared = false; +const int QuickEditor::bottomHelpBoxPaddingX = 12; +const int QuickEditor::bottomHelpBoxPaddingY = 8; +const int QuickEditor::bottomHelpBoxPairSpacing = 6; +const int QuickEditor::bottomHelpBoxMarginBottom = 5; +const int QuickEditor::midHelpTextFontSize = 12; + +const int QuickEditor::magnifierLargeStep = 15; -#include -#include -#include -#include -#include -#include +const int QuickEditor::magZoom = 5; +const int QuickEditor::magPixels = 16; +const int QuickEditor::magOffset = 32; -struct QuickEditor::ImageStore : public QQuickImageProvider +QuickEditor::QuickEditor(const QPixmap& pixmap) : + mMaskColor(QColor::fromRgbF(0, 0, 0, 0.15)), + mStrokeColor(palette().highlight().color()), + mCrossColor(QColor::fromRgbF(mStrokeColor.redF(), mStrokeColor.greenF(), mStrokeColor.blueF(), 0.7)), + mLabelBackgroundColor(QColor::fromRgbF( + palette().light().color().redF(), + palette().light().color().greenF(), + palette().light().color().blueF(), + 0.85 + )), + mLabelForegroundColor(palette().windowText().color()), + mMidHelpText(i18n("Click and drag to draw a selection rectangle,\nor press Esc to quit")), + mMidHelpTextFont(font()), + mBottomHelpTextFont(font()), + mBottomHelpGridLeftWidth(0), + mMouseDragState(MouseState::None), + mPixmap(pixmap), + mMagnifierAllowed(false), + mShowMagnifier(SpectacleConfig::instance()->showMagnifierChecked()), + mToggleMagnifier(false) { - ImageStore(const QPixmap &pixmap) : - QQuickImageProvider(QQuickImageProvider::Pixmap), - mPixmap(pixmap) - {} + SpectacleConfig *config = SpectacleConfig::instance(); + if (config->useLightRegionMaskColour()) { + mMaskColor = QColor(255, 255, 255, 100); + } + + setMouseTracking(true); + setAttribute(Qt::WA_StaticContents); + setWindowFlags(Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint | Qt::Popup | Qt::WindowStaysOnTopHint); + show(); - QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) Q_DECL_OVERRIDE - { - Q_UNUSED(id); + dprI = 1.0 / devicePixelRatioF(); + setGeometry(0, 0, static_cast(mPixmap.width() * dprI), static_cast(mPixmap.height() * dprI)); - if (size) { - *size = mPixmap.size(); + if (config->rememberLastRectangularRegion()) { + QRect cropRegion = config->cropRegion(); + if (!cropRegion.isEmpty()) { + mSelection = QRectF( + cropRegion.x() * dprI, + cropRegion.y() * dprI, + cropRegion.width() * dprI, + cropRegion.height() * dprI + ).intersected(geometry()); } + setMouseCursor(QCursor::pos()); + } else { + setCursor(Qt::CrossCursor); + } - if (requestedSize.isEmpty()) { - return mPixmap; + mMidHelpTextFont.setPointSize(midHelpTextFontSize); + if (!bottomHelpTextPrepared) { + bottomHelpTextPrepared = true; + const auto prepare = [this](QStaticText& item) { + item.prepare(QTransform(), mBottomHelpTextFont); + item.setPerformanceHint(QStaticText::AggressiveCaching); + }; + for (auto& pair : bottomHelpText) { + prepare(pair.first); + for (auto item : pair.second) { + prepare(item); + } } + } + layoutBottomHelpText(); + + update(); +} + +void QuickEditor::acceptSelection() +{ + if (!mSelection.isEmpty()) { + const qreal dpr = devicePixelRatioF(); + QRect scaledCropRegion = QRect( + qRound(mSelection.x() * dpr), + qRound(mSelection.y() * dpr), + qRound(mSelection.width() * dpr), + qRound(mSelection.height() * dpr) + ); + SpectacleConfig::instance()->setCropRegion(scaledCropRegion); + emit grabDone(mPixmap.copy(scaledCropRegion)); + } +} - return mPixmap.scaled(requestedSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); +void QuickEditor::keyPressEvent(QKeyEvent* event) +{ + const auto modifiers = event->modifiers(); + const bool shiftPressed = modifiers & Qt::ShiftModifier; + if (shiftPressed) { + mToggleMagnifier = true; + } + switch(event->key()) { + case Qt::Key_Escape: + emit grabCancelled(); + break; + case Qt::Key_Return: + case Qt::Key_Enter: + acceptSelection(); + break; + case Qt::Key_Up: { + const qreal step = (shiftPressed ? 1 : magnifierLargeStep); + const int newPos = boundsUp(qRound(mSelection.top() * devicePixelRatioF() - step), false); + if (modifiers & Qt::AltModifier) { + mSelection.setBottom(dprI * newPos + mSelection.height()); + mSelection = mSelection.normalized(); + } else { + mSelection.moveTop(dprI * newPos); + } + update(); + break; + } + case Qt::Key_Right: { + const qreal step = (shiftPressed ? 1 : magnifierLargeStep); + const int newPos = boundsRight(qRound(mSelection.left() * devicePixelRatioF() + step), false); + if (modifiers & Qt::AltModifier) { + mSelection.setRight(dprI * newPos + mSelection.width()); + } else { + mSelection.moveLeft(dprI * newPos); + } + update(); + break; + } + case Qt::Key_Down: { + const qreal step = (shiftPressed ? 1 : magnifierLargeStep); + const int newPos = boundsDown(qRound(mSelection.top() * devicePixelRatioF() + step), false); + if (modifiers & Qt::AltModifier) { + mSelection.setBottom(dprI * newPos + mSelection.height()); + } else { + mSelection.moveTop(dprI * newPos); + } + update(); + break; } + case Qt::Key_Left: { + const qreal step = (shiftPressed ? 1 : magnifierLargeStep); + const int newPos = boundsLeft(qRound(mSelection.left() * devicePixelRatioF() - step), false); + if (modifiers & Qt::AltModifier) { + mSelection.setRight(dprI * newPos + mSelection.width()); + mSelection = mSelection.normalized(); + } else { + mSelection.moveLeft(dprI * newPos); + } + update(); + break; + } + default: + break; + } + event->accept(); +} - QPixmap mPixmap; -}; +void QuickEditor::keyReleaseEvent(QKeyEvent* event) +{ + if (mToggleMagnifier && !(event->modifiers() & Qt::ShiftModifier)) { + mToggleMagnifier = false; + update(); + } + event->accept(); +} -class QuickEditor::QuickEditorPrivate +int QuickEditor::boundsLeft(int newTopLeftX, const bool mouse) { -public: - KDeclarative::KDeclarative *mDecl; - QQuickView *mQuickView; - QQmlEngine *mQmlEngine; - QRect mGrabRect; - QSharedPointer mCurrentGrabResult; -}; + if (newTopLeftX < 0) { + if (mouse) { + // tweak startPos to prevent rectangle from getting stuck + mStartPos.setX(mStartPos.x() + newTopLeftX * dprI); + } + newTopLeftX = 0; + } -QuickEditor::QuickEditor(const QPixmap &pixmap, QObject *parent) : - QObject(parent), - mImageStore(new ImageStore(pixmap)), - d_ptr(new QuickEditorPrivate) + return newTopLeftX; +} + +int QuickEditor::boundsRight(int newTopLeftX, const bool mouse) { - Q_D(QuickEditor); + // the max X coordinate of the top left point + const int realMaxX = qRound((width() - mSelection.width()) * devicePixelRatioF()); + const int xOffset = newTopLeftX - realMaxX; + if (xOffset > 0) { + if (mouse) { + mStartPos.setX(mStartPos.x() + xOffset * dprI); + } + newTopLeftX = realMaxX; + } - d->mQmlEngine = new QQmlEngine(); - d->mDecl = new KDeclarative::KDeclarative; - d->mDecl->setDeclarativeEngine(d->mQmlEngine); + return newTopLeftX; -#if KDECLARATIVE_VERSION >= QT_VERSION_CHECK(5, 45, 0) - d->mDecl->setupEngine(d->mQmlEngine); - d->mDecl->setupContext(); -#else - d->mDecl->setupBindings(); -#endif - d->mQmlEngine->addImageProvider(QStringLiteral("snapshot"), mImageStore); - d->mQuickView = new QQuickView(d->mQmlEngine, nullptr); - d->mQuickView->setClearBeforeRendering(false); - d->mQuickView->setSource(QUrl(QStringLiteral("qrc:///QuickEditor/EditorRoot.qml"))); +} - d->mQuickView->setFlags(Qt::BypassWindowManagerHint | Qt::WindowStaysOnTopHint | Qt::FramelessWindowHint | Qt::Tool); - d->mQuickView->setGeometry(0, 0, pixmap.width(), pixmap.height()); - d->mQuickView->showFullScreen(); +int QuickEditor::boundsUp(int newTopLeftY, const bool mouse) +{ + if (newTopLeftY < 0) { + if (mouse) { + mStartPos.setY(mStartPos.y() + newTopLeftY * dprI); + } + newTopLeftY = 0; + } - // connect up the signals - QQuickItem *rootItem = d->mQuickView->rootObject(); - connect(rootItem, SIGNAL(acceptImage(int, int, int, int)), this, SLOT(acceptImageHandler(int, int, int, int))); - connect(rootItem, SIGNAL(cancelImage()), this, SIGNAL(grabCancelled())); + return newTopLeftY; +} - // set up initial config - SpectacleConfig *config = SpectacleConfig::instance(); - if (config->rememberLastRectangularRegion()) { - auto pixelRatio = d->mQuickView->devicePixelRatio(); - QRect cropRegion = config->cropRegion(); - if (!cropRegion.isEmpty()) { - QMetaObject::invokeMethod( - rootItem, "setInitialSelection", - Q_ARG(QVariant, cropRegion.x() / pixelRatio), - Q_ARG(QVariant, cropRegion.y() / pixelRatio), - Q_ARG(QVariant, cropRegion.width() / pixelRatio), - Q_ARG(QVariant, cropRegion.height() / pixelRatio) - ); +int QuickEditor::boundsDown(int newTopLeftY, const bool mouse) +{ + // the max Y coordinate of the top left point + const int realMaxY = qRound((height() - mSelection.height()) * devicePixelRatioF()); + const int yOffset = newTopLeftY - realMaxY; + if (yOffset > 0) { + if (mouse) { + mStartPos.setY(mStartPos.y() + yOffset * dprI); } + newTopLeftY = realMaxY; } - rootItem->setProperty("showMagnifier", config->showMagnifierChecked()); + return newTopLeftY; +} - if (config->useLightRegionMaskColour()) { - rootItem->setProperty("maskColour", QColor(255, 255, 255, 100)); +void QuickEditor::mousePressEvent(QMouseEvent* event) +{ + if (event->button() & Qt::LeftButton) { + const QPointF& pos = event->pos(); + mMousePos = pos; + mMagnifierAllowed = true; + mMouseDragState = mouseLocation(pos); + switch(mMouseDragState) { + case MouseState::Outside: + mStartPos = pos; + break; + case MouseState::Inside: + mStartPos = pos; + mMagnifierAllowed = false; + mInitialTopLeft = mSelection.topLeft(); + setCursor(Qt::ClosedHandCursor); + break; + case MouseState::Top: + case MouseState::Left: + case MouseState::TopLeft: + mStartPos = mSelection.bottomRight(); + break; + case MouseState::Bottom: + case MouseState::Right: + case MouseState::BottomRight: + mStartPos = mSelection.topLeft(); + break; + case MouseState::TopRight: + mStartPos = mSelection.bottomLeft(); + break; + case MouseState::BottomLeft: + mStartPos = mSelection.topRight(); + break; + default: + break; + } + } + if (mMagnifierAllowed) { + update(); } + event->accept(); } -QuickEditor::~QuickEditor() +void QuickEditor::mouseMoveEvent(QMouseEvent* event) { - Q_D(QuickEditor); - delete d->mQuickView; - delete d->mDecl; - delete d->mQmlEngine; + const QPointF& pos = event->pos(); + mMousePos = pos; + mMagnifierAllowed = true; + switch (mMouseDragState) { + case MouseState::None: { + setMouseCursor(pos); + mMagnifierAllowed = false; + break; + } + case MouseState::TopLeft: + case MouseState::TopRight: + case MouseState::BottomRight: + case MouseState::BottomLeft: { + const bool afterX = pos.x() >= mStartPos.x(); + const bool afterY = pos.y() >= mStartPos.y(); + mSelection.setRect( + afterX ? mStartPos.x() : pos.x(), + afterY ? mStartPos.y() : pos.y(), + qAbs(pos.x() - mStartPos.x()) + (afterX ? dprI : 0), + qAbs(pos.y() - mStartPos.y()) + (afterY ? dprI : 0) + ); + update(); + break; + } + case MouseState::Outside: { + mSelection.setRect( + qMin(pos.x(), mStartPos.x()), + qMin(pos.y(), mStartPos.y()), + qAbs(pos.x() - mStartPos.x()) + dprI, + qAbs(pos.y() - mStartPos.y()) + dprI + ); + update(); + break; + } + case MouseState::Top: + case MouseState::Bottom: { + const bool afterY = pos.y() >= mStartPos.y(); + mSelection.setRect( + mSelection.x(), + afterY ? mStartPos.y() : pos.y(), + mSelection.width(), + qAbs(pos.y() - mStartPos.y()) + (afterY ? dprI : 0) + ); + update(); + break; + } + case MouseState::Right: + case MouseState::Left: { + const bool afterX = pos.x() >= mStartPos.x(); + mSelection.setRect( + afterX ? mStartPos.x() : pos.x(), + mSelection.y(), + qAbs(pos.x() - mStartPos.x()) + (afterX ? dprI : 0), + mSelection.height() + ); + update(); + break; + } + case MouseState::Inside: { + mMagnifierAllowed = false; + // We use some math here to figure out if the diff with which we + // move the rectangle with moves it out of bounds, + // in which case we adjust the diff to not let that happen + + const qreal dpr = devicePixelRatioF(); + // new top left point of the rectangle + QPoint newTopLeft = ((pos - mStartPos + mInitialTopLeft) * dpr).toPoint(); + + int newTopLeftX = boundsLeft(newTopLeft.x()); + if (newTopLeftX != 0) { + newTopLeftX = boundsRight(newTopLeftX); + } - delete d_ptr; + int newTopLeftY = boundsUp(newTopLeft.y()); + if (newTopLeftY != 0) { + newTopLeftY = boundsDown(newTopLeftY); + } + + const auto newTopLeftF = QPointF(newTopLeftX * dprI, newTopLeftY * dprI); + + mSelection.moveTo(newTopLeftF); + update(); + break; + } + default: + break; + } + + event->accept(); } -void QuickEditor::acceptImageHandler(int x, int y, int width, int height) +void QuickEditor::mouseReleaseEvent(QMouseEvent* event) { - Q_D(QuickEditor); + const auto button = event->button(); + if (button == Qt::LeftButton && mMouseDragState == MouseState::Inside) { + setCursor(Qt::OpenHandCursor); + } else if (button == Qt::RightButton) { + mSelection.setWidth(0); + mSelection.setHeight(0); + } + event->accept(); + mMouseDragState = MouseState::None; + update(); +} - if ((x == -1) && (y == -1) && (width == -1) && (height == -1)) { - SpectacleConfig::instance()->setCropRegion(QRect()); - emit grabCancelled(); +void QuickEditor::mouseDoubleClickEvent(QMouseEvent* event) +{ + event->accept(); + if (event->button() == Qt::LeftButton && mSelection.contains(event->pos())) { + acceptSelection(); + } +} + +void QuickEditor::paintEvent(QPaintEvent*) +{ + QPainter painter(this); + painter.setRenderHints(QPainter::Antialiasing); + QBrush brush(mPixmap); + brush.setTransform(QTransform().scale(dprI, dprI)); + painter.setBackground(brush); + painter.eraseRect(geometry()); + if (!mSelection.size().isEmpty() || mMouseDragState != MouseState::None) { + painter.fillRect(mSelection, mStrokeColor); + const QRectF innerRect = mSelection.adjusted(1, 1, -1, -1); + if (innerRect.width() > 0 && innerRect.height() > 0) { + painter.eraseRect(mSelection.adjusted(1, 1, -1, -1)); + } + + QRectF top(0, 0, width(), mSelection.top()); + QRectF right(mSelection.right(), mSelection.top(), width() - mSelection.right(), mSelection.height()); + QRectF bottom(0, mSelection.bottom(), width(), height() - mSelection.bottom()); + QRectF left(0, mSelection.top(), mSelection.left(), mSelection.height()); + for (const auto& rect : { top, right, bottom, left }) { + painter.fillRect(rect, mMaskColor); + } + + drawSelectionSizeTooltip(painter); + if (mMouseDragState == MouseState::None) { // mouse is up + if ((mSelection.width() > 20) && (mSelection.height() > 20)) { + drawDragHandles(painter); + } + + } else if (mMagnifierAllowed && (mShowMagnifier ^ mToggleMagnifier)) { + drawMagnifier(painter); + } + drawBottomHelpText(painter); + } else { + drawMidHelpText(painter); + } +} + +void QuickEditor::layoutBottomHelpText() +{ + int maxRightWidth = 0; + int contentWidth = 0; + int contentHeight = 0; + mBottomHelpGridLeftWidth = 0; + int i = 0; + for (const auto& item : bottomHelpText) { + const auto& left = item.first; + const auto& right = item.second; + const auto leftSize = left.size().toSize(); + mBottomHelpGridLeftWidth = qMax(mBottomHelpGridLeftWidth, leftSize.width()); + for (const auto& item : right) { + const auto rightItemSize = item.size().toSize(); + maxRightWidth = qMax(maxRightWidth, rightItemSize.width()); + contentHeight += rightItemSize.height(); + } + contentWidth = qMax(contentWidth, mBottomHelpGridLeftWidth + maxRightWidth + bottomHelpBoxPairSpacing); + contentHeight += (++i != bottomHelpLength ? bottomHelpBoxMarginBottom : 0); + } + mBottomHelpContentPos.setX((width() - contentWidth) / 2); + mBottomHelpContentPos.setY(height() - contentHeight - 8); + mBottomHelpGridLeftWidth += mBottomHelpContentPos.x(); + mBottomHelpBorderBox.setRect( + mBottomHelpContentPos.x() - bottomHelpBoxPaddingX, + mBottomHelpContentPos.y() - bottomHelpBoxPaddingY, + contentWidth + bottomHelpBoxPaddingX * 2, + contentHeight + bottomHelpBoxPaddingY * 2 - 1 + ); +} + +void QuickEditor::drawBottomHelpText(QPainter &painter) +{ + if (mSelection.intersects(mBottomHelpBorderBox)) { return; } - auto pixelRatio = d->mQuickView->devicePixelRatio(); - d->mGrabRect = QRect(x * pixelRatio, y * pixelRatio, width * pixelRatio, height * pixelRatio); - SpectacleConfig::instance()->setCropRegion(d->mGrabRect); + painter.setBrush(mLabelBackgroundColor); + painter.setPen(mLabelForegroundColor); + painter.setFont(mBottomHelpTextFont); + painter.setRenderHint(QPainter::Antialiasing, false); + painter.drawRect(mBottomHelpBorderBox); + painter.setRenderHint(QPainter::Antialiasing, true); + + int topOffset = mBottomHelpContentPos.y(); + int i = 0; + for (const auto& item : bottomHelpText) { + const auto& left = item.first; + const auto& right = item.second; + const auto leftSize = left.size().toSize(); + painter.drawStaticText(mBottomHelpGridLeftWidth - leftSize.width(), topOffset, left); + for (const auto& item : right) { + const auto rightItemSize = item.size().toSize(); + painter.drawStaticText(mBottomHelpGridLeftWidth + bottomHelpBoxPairSpacing, topOffset, item); + topOffset += rightItemSize.height(); + } + if (++i != bottomHelpLength) { + topOffset += bottomHelpBoxMarginBottom; + } + } +} + +void QuickEditor::drawDragHandles(QPainter& painter) +{ + const qreal left = mSelection.x(); + const qreal width = mSelection.width(); + const qreal centerX = left + width / 2.0; + const qreal right = left + width; + + const qreal top = mSelection.y(); + const qreal height = mSelection.height(); + const qreal centerY = top + height / 2.0; + const qreal bottom = top + height; + + // start a path + QPainterPath path; + + const qreal cornerHandleDiameter = 2 * cornerHandleRadius; + + // x and y coordinates of handle arcs + const qreal leftHandle = left - cornerHandleRadius; + const qreal topHandle = top - cornerHandleRadius; + const qreal rightHandle = right - cornerHandleRadius; + const qreal bottomHandle = bottom - cornerHandleRadius; + const qreal centerHandleX = centerX - midHandleRadius; + const qreal centerHandleY = centerY - midHandleRadius; + + // top-left handle + path.moveTo(left, top); + path.arcTo(leftHandle, topHandle, cornerHandleDiameter, cornerHandleDiameter, 0, -90); + + // top-right handle + path.moveTo(right, top); + path.arcTo(rightHandle, topHandle, cornerHandleDiameter, cornerHandleDiameter, 180, 90); + + // bottom-left handle + path.moveTo(left, bottom); + path.arcTo(leftHandle, bottomHandle, cornerHandleDiameter, cornerHandleDiameter, 0, 90); + + // bottom-right handle + path.moveTo(right, bottom); + path.arcTo(rightHandle, bottomHandle, cornerHandleDiameter, cornerHandleDiameter, 180, -90); + + const qreal midHandleDiameter = 2 * midHandleRadius; + // top-center handle + path.moveTo(centerX, top); + path.arcTo(centerHandleX, top - midHandleRadius, midHandleDiameter, midHandleDiameter, 0, -180); + + // right-center handle + path.moveTo(right, centerY); + path.arcTo(right - midHandleRadius, centerHandleY, midHandleDiameter, midHandleDiameter, 90, 180); + + // bottom-center handle + path.moveTo(centerX, bottom); + path.arcTo(centerHandleX, bottom - midHandleRadius, midHandleDiameter, midHandleDiameter, 0, 180); + + // left-center handle + path.moveTo(left, centerY); + path.arcTo(left - midHandleRadius, centerHandleY, midHandleDiameter, midHandleDiameter, 90, -180); + + // draw the path + painter.fillPath(path, mStrokeColor); +} + +void QuickEditor::drawMagnifier(QPainter &painter) +{ + const int pixels = 2 * magPixels + 1; + int magX = static_cast(mMousePos.x() * devicePixelRatioF() - magPixels); + int offsetX = 0; + if (magX < 0) { + offsetX = magX; + magX = 0; + } else { + const int maxX = mPixmap.width() - pixels; + if (magX > maxX) { + offsetX = magX - maxX; + magX = maxX; + } + } + int magY = static_cast(mMousePos.y() * devicePixelRatioF() - magPixels); + int offsetY = 0; + if (magY < 0) { + offsetY = magY; + magY = 0; + } else { + const int maxY = mPixmap.height() - pixels; + if (magY > maxY) { + offsetY = magY - maxY; + magY = maxY; + } + } + QRectF magniRect(magX, magY, pixels, pixels); + + qreal drawPosX = mMousePos.x() + magOffset + pixels * magZoom / 2; + if (drawPosX > width() - pixels * magZoom / 2) { + drawPosX = mMousePos.x() - magOffset - pixels * magZoom / 2; + } + qreal drawPosY = mMousePos.y() + magOffset + pixels * magZoom / 2; + if (drawPosY > height() - pixels * magZoom / 2) { + drawPosY = mMousePos.y() - magOffset - pixels * magZoom / 2; + } + QPointF drawPos(drawPosX, drawPosY); + QRectF crossHairTop(drawPos.x() + magZoom * (offsetX - 0.5), drawPos.y() - magZoom * (magPixels + 0.5), magZoom, magZoom * (magPixels + offsetY)); + QRectF crossHairRight(drawPos.x() + magZoom * (0.5 + offsetX), drawPos.y() + magZoom * (offsetY - 0.5), magZoom * (magPixels - offsetX), magZoom); + QRectF crossHairBottom(drawPos.x() + magZoom * (offsetX - 0.5), drawPos.y() + magZoom * (0.5 + offsetY), magZoom, magZoom * (magPixels - offsetY)); + QRectF crossHairLeft(drawPos.x() - magZoom * (magPixels + 0.5), drawPos.y() + magZoom * (offsetY - 0.5), magZoom * (magPixels + offsetX), magZoom); + QRectF crossHairBorder(drawPos.x() - magZoom * (magPixels + 0.5) - 1, drawPos.y() - magZoom * (magPixels + 0.5) - 1, pixels * magZoom + 2, pixels * magZoom + 2); + const auto frag = QPainter::PixmapFragment::create(drawPos, magniRect, magZoom, magZoom); + + painter.fillRect(crossHairBorder, mLabelForegroundColor); + painter.drawPixmapFragments(&frag, 1, mPixmap, QPainter::OpaqueHint); + painter.setCompositionMode(QPainter::CompositionMode_SourceOver); + for (auto& rect : { crossHairTop, crossHairRight, crossHairBottom, crossHairLeft }) { + painter.fillRect(rect, mCrossColor); + } +} + +void QuickEditor::drawMidHelpText(QPainter &painter) +{ + painter.fillRect(geometry(), mMaskColor); + painter.setFont(mMidHelpTextFont); + QRect textSize = painter.boundingRect(QRect(), Qt::AlignCenter, mMidHelpText); + QPoint pos((width() - textSize.width()) / 2, (height() - textSize.height()) / 2); + + painter.setBrush(mLabelBackgroundColor); + QPen pen(mLabelForegroundColor); + pen.setWidth(2); + painter.setPen(pen); + painter.drawRoundedRect(QRect(pos.x() - 20, pos.y() - 20, textSize.width() + 40, textSize.height() + 40), 4, 4); + + painter.setCompositionMode(QPainter::CompositionMode_Source); + painter.drawText(QRect(pos, textSize.size()), Qt::AlignCenter, mMidHelpText); +} + +void QuickEditor::drawSelectionSizeTooltip(QPainter &painter) +{ + // Set the selection size and finds the most appropriate position: + // - vertically centered inside the selection if the box is not covering the a large part of selection + // - on top of the selection if the selection x position fits the box height plus some margin + // - at the bottom otherwise + const qreal dpr = devicePixelRatioF(); + QString selectionSizeText = ki18n("%1×%2").subs(qRound(mSelection.width() * dpr)).subs(qRound(mSelection.height() * dpr)).toString(); + const QRect selectionSizeTextRect = painter.boundingRect(QRect(), 0, selectionSizeText); + + const int selectionBoxWidth = selectionSizeTextRect.width() + selectionBoxPaddingX * 2; + const int selectionBoxHeight = selectionSizeTextRect.height() + selectionBoxPaddingY * 2; + const int selectionBoxX = qBound( + 0, + static_cast(mSelection.x()) + (static_cast(mSelection.width()) - selectionSizeTextRect.width()) / 2 - selectionBoxPaddingX, + width() - selectionBoxWidth + ); + int selectionBoxY; + if ((mSelection.width() >= selectionSizeThreshold) && (mSelection.height() >= selectionSizeThreshold)) { + // show inside the box + selectionBoxY = static_cast(mSelection.y() + (mSelection.height() - selectionSizeTextRect.height()) / 2); + } else { + // show on top by default + selectionBoxY = static_cast(mSelection.y() - selectionBoxHeight - selectionBoxMarginY); + if (selectionBoxY < 0) { + // show at the bottom + selectionBoxY = static_cast(mSelection.y() + mSelection.height() + selectionBoxMarginY); + } + } + + // Now do the actual box, border, and text drawing + painter.setBrush(mLabelBackgroundColor); + painter.setPen(mLabelForegroundColor); + const QRect selectionBoxRect( + selectionBoxX, + selectionBoxY, + selectionBoxWidth, + selectionBoxHeight + ); - d->mQuickView->hide(); - emit grabDone(mImageStore->mPixmap.copy(d->mGrabRect), d->mGrabRect); + painter.setRenderHint(QPainter::Antialiasing, false); + painter.drawRect(selectionBoxRect); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.drawText(selectionBoxRect, Qt::AlignCenter, selectionSizeText); +} + +void QuickEditor::setMouseCursor(const QPointF& pos) +{ + MouseState mouseState = mouseLocation(pos); + if (mouseState == MouseState::Outside) { + setCursor(Qt::CrossCursor); + } else if (MouseState::TopLeftOrBottomRight & mouseState) { + setCursor(Qt::SizeFDiagCursor); + } else if (MouseState::TopRightOrBottomLeft & mouseState) { + setCursor(Qt::SizeBDiagCursor); + } else if (MouseState::TopOrBottom & mouseState) { + setCursor(Qt::SizeVerCursor); + } else if (MouseState::RightOrLeft & mouseState) { + setCursor(Qt::SizeHorCursor); + } else { + setCursor(Qt::OpenHandCursor); + } +} + +QuickEditor::MouseState QuickEditor::mouseLocation(const QPointF& pos) +{ + if (mSelection.contains(pos)) { + const qreal verSize = qMin(mouseAreaSize, mSelection.height() / 2); + const qreal horSize = qMin(mouseAreaSize, mSelection.width() / 2); + + auto withinThreshold = [](const qreal offset, const qreal size) { + return offset <= size && offset >= 0; + }; + + const bool withinTopEdge = withinThreshold(pos.y() - mSelection.top(), verSize); + const bool withinRightEdge = withinThreshold(mSelection.right() - pos.x(), horSize); + const bool withinBottomEdge = !withinTopEdge && withinThreshold(mSelection.bottom() - pos.y(), verSize); + const bool withinLeftEdge = !withinRightEdge && withinThreshold(pos.x() - mSelection.left(), horSize); + + if (withinTopEdge) { + if (withinRightEdge) { + return MouseState::TopRight; + } else if (withinLeftEdge) { + return MouseState::TopLeft; + } else { + return MouseState::Top; + } + } else if (withinBottomEdge) { + if (withinRightEdge) { + return MouseState::BottomRight; + } else if (withinLeftEdge) { + return MouseState::BottomLeft; + } else { + return MouseState::Bottom; + } + } else if (withinRightEdge) { + return MouseState::Right; + } else if (withinLeftEdge) { + return MouseState::Left; + } else { + return MouseState::Inside; + } + } else { + return MouseState::Outside; + } } diff --git a/src/QuickEditor/SelectionRectangle.qml b/src/QuickEditor/SelectionRectangle.qml deleted file mode 100644 --- a/src/QuickEditor/SelectionRectangle.qml +++ /dev/null @@ -1,360 +0,0 @@ -/* - * Copyright (C) 2016 Boudhayan Gupta - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, - * Boston, MA 02110-1301, USA. - */ - -import QtQuick 2.5 - -Item { - id: cropRectItem; - objectName: "cropRectItem"; - - property var drawCanvas: null; - property var imageElement: null; - property int minRectSize: 20; - property int mouseAreaSize: 20; - property double zoomCenterX: -1; - property double zoomCenterY: -1; - - function resetZoomCenter() { - zoomCenterX = -1; - zoomCenterY = -1; - drawCanvas.requestPaint(); - } - - signal doubleClicked(); - - onWidthChanged: { - var maxWidth = imageElement.width - x; - if (width > maxWidth) { - width = maxWidth; - } - } - - onHeightChanged: { - var maxHeight = imageElement.height - y; - if (height > maxHeight) { - height = maxHeight; - } - } - - MouseArea { - anchors.fill: parent; - cursorShape: Qt.OpenHandCursor; - - drag.target: parent; - drag.minimumX: 0; - drag.maximumX: imageElement.width - parent.width; - drag.minimumY: 0; - drag.maximumY: imageElement.height - parent.height; - drag.smoothed: true; - - onPressed: { cursorShape = Qt.ClosedHandCursor; } - onPositionChanged: { drawCanvas.requestPaint(); } - onReleased: { cursorShape = Qt.OpenHandCursor; } - onDoubleClicked: { cropRectItem.doubleClicked(); } - } - - MouseArea { - id: hTopLeft; - - property int brxLimit: 0; - property int bryLimit: 0; - - anchors.top: parent.top; - anchors.left: parent.left; - - width: mouseAreaSize; - height: mouseAreaSize; - cursorShape: Qt.SizeFDiagCursor; - - onPressed: { - brxLimit = (parent.x + parent.width) - minRectSize; - bryLimit = (parent.y + parent.height) - minRectSize; - parent.zoomCenterX = parent.x; - parent.zoomCenterY = parent.y; - } - - onPositionChanged: { - if ((parent.x + mouse.x) < brxLimit) { - parent.x = parent.x + mouse.x; - parent.width = parent.width - mouse.x; - parent.zoomCenterX = parent.zoomCenterX + mouse.x; - } - - if ((parent.y + mouse.y) < bryLimit) { - parent.y = parent.y + mouse.y; - parent.height = parent.height - mouse.y; - parent.zoomCenterY = parent.zoomCenterY + mouse.y; - } - - drawCanvas.requestPaint(); - } - - onReleased: { - resetZoomCenter(); - } - } - - MouseArea { - id: hTopRight; - - property int brxLimit: 0; - property int bryLimit: 0; - - anchors.top: parent.top; - anchors.right: parent.right; - - width: mouseAreaSize; - height: mouseAreaSize; - cursorShape: Qt.SizeBDiagCursor; - - onPressed: { - brxLimit = parent.x + mouseAreaSize + minRectSize; - bryLimit = (parent.y + parent.height) - minRectSize; - parent.zoomCenterX = parent.x + parent.width - 1; - parent.zoomCenterY = parent.y; - } - - onPositionChanged: { - if ((parent.x + parent.width + mouse.x) > brxLimit) { - parent.width = parent.width + mouse.x - mouseAreaSize + 1; - parent.zoomCenterX = parent.zoomCenterX + mouse.x - mouseAreaSize + 1; - } - - if ((parent.y + mouse.y) < bryLimit) { - parent.y = parent.y + mouse.y; - parent.height = parent.height - mouse.y; - parent.zoomCenterY = parent.zoomCenterY + mouse.y; - } - - drawCanvas.requestPaint(); - } - - onReleased: { - resetZoomCenter(); - } - } - - MouseArea { - id: hBottomLeft; - - property int brxLimit: 0; - property int bryLimit: 0; - - anchors.bottom: parent.bottom; - anchors.left: parent.left; - - width: mouseAreaSize; - height: mouseAreaSize; - cursorShape: Qt.SizeBDiagCursor; - - onPressed: { - brxLimit = (parent.x + parent.width) - minRectSize; - bryLimit = parent.y + mouseAreaSize + minRectSize; - parent.zoomCenterX = parent.x; - parent.zoomCenterY = parent.y + parent.height - 1; - } - - onPositionChanged: { - if ((parent.x + mouse.x) < brxLimit) { - parent.x = parent.x + mouse.x; - parent.width = parent.width - mouse.x; - parent.zoomCenterX = parent.zoomCenterX + mouse.x; - } - - if ((parent.y + parent.height + mouse.y) > bryLimit) { - parent.height = parent.height + mouse.y - mouseAreaSize + 1; - parent.zoomCenterY = parent.zoomCenterY + mouse.y - mouseAreaSize + 1; - } - - drawCanvas.requestPaint(); - } - - onReleased: { - resetZoomCenter(); - } - } - - MouseArea { - id: hBottomRight; - - property int brxLimit: 0; - property int bryLimit: 0; - - anchors.bottom: parent.bottom; - anchors.right: parent.right; - - width: mouseAreaSize; - height: mouseAreaSize; - cursorShape: Qt.SizeFDiagCursor; - - onPressed: { - brxLimit = parent.x + mouseAreaSize + minRectSize; - bryLimit = parent.y + mouseAreaSize + minRectSize; - parent.zoomCenterX = parent.x + parent.width - 1; - parent.zoomCenterY = parent.y + parent.height - 1; - } - - onPositionChanged: { - if ((parent.x + parent.width + mouse.x) > brxLimit) { - parent.width = parent.width + mouse.x - mouseAreaSize + 1; - parent.zoomCenterX = parent.zoomCenterX + mouse.x - mouseAreaSize + 1; - } - - if ((parent.y + parent.height + mouse.y) > bryLimit) { - parent.height = parent.height + mouse.y - mouseAreaSize + 1; - parent.zoomCenterY = parent.zoomCenterY + mouse.y - mouseAreaSize + 1; - } - drawCanvas.requestPaint(); - } - - onReleased: { - resetZoomCenter(); - } - } - - MouseArea { - id: hTop; - - property int limit: 0; - - anchors.horizontalCenter: parent.horizontalCenter; - anchors.top: parent.top; - - width: mouseAreaSize; - height: mouseAreaSize; - cursorShape: Qt.SizeVerCursor; - - onPressed: { - limit = (parent.y + parent.height) - minRectSize; - parent.zoomCenterX = parent.x + (parent.width >> 1) - mouseAreaSize / 2; - parent.zoomCenterY = parent.y; - } - - onPositionChanged: { - if ((parent.y + mouse.y) < limit) { - parent.y = parent.y + mouse.y; - parent.height = parent.height - mouse.y; - parent.zoomCenterY = parent.zoomCenterY + mouse.y; - } - parent.zoomCenterX = parent.x + (parent.width >> 1) - mouseAreaSize / 2 + mouse.x + 1; - drawCanvas.requestPaint(); - } - - onReleased: { - resetZoomCenter(); - } - } - - MouseArea { - id: hBottom; - - property int limit: 0; - - anchors.horizontalCenter: parent.horizontalCenter; - anchors.bottom: parent.bottom; - - width: mouseAreaSize; - height: mouseAreaSize; - cursorShape: Qt.SizeVerCursor; - - onPressed: { - limit = parent.y + mouseAreaSize + minRectSize; - parent.zoomCenterX = parent.x + (parent.width >> 1) - mouseAreaSize / 2; - parent.zoomCenterY = parent.y + parent.height - 1; - } - - onPositionChanged: { - if ((parent.y + parent.height + mouse.y) > limit) { - parent.height = parent.height + mouse.y - mouseAreaSize + 1; - parent.zoomCenterY = parent.zoomCenterY + mouse.y - mouseAreaSize + 1; - } - parent.zoomCenterX = parent.x + (parent.width >> 1) - mouseAreaSize / 2 + mouse.x + 1; - drawCanvas.requestPaint(); - } - - onReleased: { - resetZoomCenter(); - } - } - - MouseArea { - id: hLeft; - - property int limit: 0; - - anchors.verticalCenter: parent.verticalCenter; - anchors.left: parent.left; - - width: mouseAreaSize; - height: mouseAreaSize; - cursorShape: Qt.SizeHorCursor; - - onPressed: { - limit = (parent.x + parent.width) - minRectSize; - parent.zoomCenterX = parent.x; - parent.zoomCenterY = parent.y + (parent.height >> 1) - mouseAreaSize / 2; - } - - onPositionChanged: { - if ((parent.x + mouse.x) < limit) { - parent.x = parent.x + mouse.x; - parent.width = parent.width - mouse.x; - parent.zoomCenterX = parent.zoomCenterX + mouse.x; - } - parent.zoomCenterY = parent.y + (parent.height >> 1) - mouseAreaSize / 2 + mouse.y + 1; - drawCanvas.requestPaint(); - } - - onReleased: { - resetZoomCenter(); - } - } - - MouseArea { - id: hRight; - - property int limit: 0; - - anchors.verticalCenter: parent.verticalCenter; - anchors.right: parent.right; - - width: mouseAreaSize; - height: mouseAreaSize; - cursorShape: Qt.SizeHorCursor; - - onPressed: { - limit = parent.x + mouseAreaSize + minRectSize; - parent.zoomCenterX = parent.x + parent.width - 1; - parent.zoomCenterY = parent.y + (parent.height >> 1) - mouseAreaSize / 2; - } - - onPositionChanged: { - if ((parent.x + parent.width + mouse.x) > limit) { - parent.width = parent.width + mouse.x - mouseAreaSize + 1; - parent.zoomCenterX = parent.zoomCenterX + mouse.x - mouseAreaSize + 1; - } - parent.zoomCenterY = parent.y + (parent.height >> 1) - mouseAreaSize / 2 + mouse.y + 1; - drawCanvas.requestPaint(); - } - - onReleased: { - resetZoomCenter(); - } - } -}