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 ) if(XCB_FOUND) diff --git a/src/Gui/ExportMenu.cpp b/src/Gui/ExportMenu.cpp --- a/src/Gui/ExportMenu.cpp +++ b/src/Gui/ExportMenu.cpp @@ -82,7 +82,7 @@ Q_FOREACH (auto service, services) { QString name = service->name().replace(QLatin1Char('&'), QLatin1String("&&")); - QAction *action = new QAction(QIcon::fromTheme(service->icon()), name, nullptr); + QAction *action = new QAction(QIcon::fromTheme(service->icon()), name, this); connect(action, &QAction::triggered, [=]() { QList whereIs({ mExportManager->tempSave() }); diff --git a/src/Gui/KSMainWindow.cpp b/src/Gui/KSMainWindow.cpp --- a/src/Gui/KSMainWindow.cpp +++ b/src/Gui/KSMainWindow.cpp @@ -59,8 +59,8 @@ mClipboardButton(new QToolButton), mSaveButton(new QToolButton), mSaveMenu(new QMenu), - mSaveAsAction(new QAction(nullptr)), - mSaveAction(new QAction(nullptr)), + mSaveAsAction(new QAction(this)), + mSaveAction(new QAction(this)), mMessageWidget(new KMessageWidget), mToolsMenu(new QMenu), mScreenRecorderToolsMenu(new QMenu), diff --git a/src/Gui/KSWidget.cpp b/src/Gui/KSWidget.cpp --- a/src/Gui/KSWidget.cpp +++ b/src/Gui/KSWidget.cpp @@ -32,7 +32,6 @@ #include #include #include -#include KSWidget::KSWidget(QWidget *parent) : diff --git a/src/PlatformBackends/X11ImageGrabber.h b/src/PlatformBackends/X11ImageGrabber.h --- a/src/PlatformBackends/X11ImageGrabber.h +++ b/src/PlatformBackends/X11ImageGrabber.h @@ -65,7 +65,7 @@ private slots: void KWinDBusScreenshotHelper(quint64 window); - void rectangleSelectionConfirmed(const QPixmap &pixmap, const QRect ®ion); + void rectangleSelectionConfirmed(const QPixmap &pixmap); void rectangleSelectionCancelled(); public slots: diff --git a/src/PlatformBackends/X11ImageGrabber.cpp b/src/PlatformBackends/X11ImageGrabber.cpp --- a/src/PlatformBackends/X11ImageGrabber.cpp +++ b/src/PlatformBackends/X11ImageGrabber.cpp @@ -417,17 +417,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); } @@ -665,7 +661,7 @@ void X11ImageGrabber::grabRectangularRegion() { - QuickEditor *editor = new QuickEditor(getToplevelPixmap(QRect(), false)); + QuickEditor *editor = new QuickEditor(getToplevelPixmap(QRect(), mCapturePointer)); connect(editor, &QuickEditor::grabDone, this, &X11ImageGrabber::rectangleSelectionConfirmed); connect(editor, &QuickEditor::grabCancelled, this, &X11ImageGrabber::rectangleSelectionCancelled); diff --git a/src/QuickEditor/EditorRoot.qml b/src/QuickEditor/EditorRoot.qml deleted file mode 100644 --- a/src/QuickEditor/EditorRoot.qml +++ /dev/null @@ -1,298 +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 - -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: Qt.rgba(0.114, 0.6, 0.953, 1); - 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(); - } - - // 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); - } else { - midHelpText.visible = true; - bottomHelpText.visible = false; - } - } - - TextMetrics { - id: selectionTextMetrics - } - - Rectangle { - id: midHelpText; - objectName: "midHelpText"; - - height: midHelpTextElement.height + 40; - width: midHelpTextElement.width + 40; - radius: 10; - border.width: 2; - border.color: Qt.rgba(0, 0, 0, 1); - color: Qt.rgba(1, 1, 1, 0.85); - - anchors.centerIn: parent; - - Text { - id: midHelpTextElement; - text: i18n("Click anywhere on the screen (including here) to start drawing a selection rectangle, or press Esc to quit"); - font.pointSize: 12; - - anchors.centerIn: parent; - } - } - - Rectangle { - id: bottomHelpText; - objectName: "bottomHelpText"; - - height: bottomHelpTextElement.height + 16; - width: bottomHelpTextElement.width + 24; - border.width: 1; - border.color: Qt.rgba(0, 0, 0, 1); - color: Qt.rgba(1, 1, 1, 0.85); - - anchors.bottom: parent.bottom; - anchors.horizontalCenter: parent.horizontalCenter; - - Text { - id: bottomHelpTextElement; - text: i18n("To take the screenshot, double-click or press Enter. Right-click to reset the selection, or press Esc to quit"); - font.pointSize: 9; - - anchors.centerIn: parent; - } - } - } - - 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; - - cropDisplayCanvas.requestPaint(); - } - - onClicked: { - if ((mouse.button == Qt.RightButton) && (selection)) { - selection.destroy(); - 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,6 +0,0 @@ - - - EditorRoot.qml - SelectionRectangle.qml - - diff --git a/src/QuickEditor/QuickEditor.h b/src/QuickEditor/QuickEditor.h --- a/src/QuickEditor/QuickEditor.h +++ b/src/QuickEditor/QuickEditor.h @@ -20,34 +20,68 @@ #ifndef QUICKEDITOR_H #define QUICKEDITOR_H -#include +#include +#include +#include +#include +#include -class QuickEditor : public QObject +class QuickEditor : public QWidget { Q_OBJECT - public: +public: + explicit QuickEditor(const QPixmap &pixmap, QObject* parent = nullptr); - explicit QuickEditor(const QPixmap &pixmap, QObject *parent = 0); - virtual ~QuickEditor(); +private: + enum MouseState : short { + None = 0, // 0000 + Inside = 1, // 0001 + Outside = 2, // 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 = 4, // 100 + TopRightOrBottomLeft = 8, // 1000 + TopOrBottom = 16, // 10000 + RightOrLeft = 32, // 100000 + }; - signals: + inline void acceptSelection(); + void keyPressEvent(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; + inline void setMouseCursor(const QPoint& pos); + MouseState whereIsTheMouse(const QPoint& pos); - void grabDone(const QPixmap &pixmap, const QRect &cropRegion); - void grabCancelled(); - - private slots: - - void acceptImageHandler(int x, int y, int width, int height); + static const int mouseAreaSize; + static const qreal cornerHandleRadius; + static const qreal midHandleRadius; + static const int selectionSizeThreshold; - private: + QColor mMaskColour; + QColor mStrokeColor; + QRect mSelection; + QPoint mStartPos; + QPoint mInitialTopLeft; + QStaticText mMidHelpText; + QFont mMidHelpTextFont; + QStaticText mBottomHelpText; + QFont mBottomHelpTextFont; + MouseState mMouseDragState; + QPixmap mPixmap; - struct ImageStore; - ImageStore *mImageStore; - - struct QuickEditorPrivate; - Q_DECLARE_PRIVATE(QuickEditor); - QuickEditorPrivate *d_ptr; +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 @@ -21,129 +21,430 @@ #include "SpectacleConfig.h" -#include -#include +const int QuickEditor::mouseAreaSize = 20; +const qreal QuickEditor::cornerHandleRadius = 8.0; +const qreal QuickEditor::midHandleRadius = 5.0; +const int QuickEditor::selectionSizeThreshold = 100; -#include -#include -#include -#include -#include -#include - -struct QuickEditor::ImageStore : public QQuickImageProvider +QuickEditor::QuickEditor(const QPixmap& pixmap, QObject* parent) : + mMaskColour(QColor::fromRgbF(0, 0, 0, 0.15)), + mStrokeColor(QColor::fromRgbF(0.114, 0.6, 0.953, 1)), + mMidHelpText(tr("Click anywhere on the screen (including here) to start drawing a selection rectangle, or press Esc to quit")), + mMidHelpTextFont(font()), + mBottomHelpText(tr("To take the screenshot, double-click or press Enter. Right-click to reset the selection, or press Esc to quit")), + mBottomHelpTextFont(font()), + mMouseDragState(MouseState::None), + mPixmap(pixmap) { - ImageStore(const QPixmap &pixmap) : - QQuickImageProvider(QQuickImageProvider::Pixmap), - mPixmap(pixmap) - {} + Q_UNUSED(parent); - QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) Q_DECL_OVERRIDE - { - Q_UNUSED(id); + SpectacleConfig *config = SpectacleConfig::instance(); + if (config->useLightRegionMaskColour()) { + mMaskColour = QColor(255, 255, 255, 100); + mStrokeColor = QColor(96, 96, 96, 255); + } - if (size) { - *size = mPixmap.size(); - } + setMouseTracking(true); + setAttribute(Qt::WA_StaticContents); + setWindowFlags(Qt::WindowStaysOnTopHint); + setAttribute(Qt::WA_TranslucentBackground); + showFullScreen(); - if (requestedSize.isEmpty()) { - return mPixmap; - } + const qreal dpr = devicePixelRatioF(); + setGeometry(0, 0, mPixmap.width() / dpr, mPixmap.height() / dpr); - return mPixmap.scaled(requestedSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); + if (config->rememberLastRectangularRegion()) { + QRect cropRegion = config->cropRegion(); + if (!cropRegion.isEmpty()) { + mSelection = QRect( + cropRegion.x() / dpr, + cropRegion.y() / dpr, + cropRegion.width() / dpr, + cropRegion.height() / dpr + ).intersected(rect()); + } + setMouseCursor(QCursor::pos()); + } else { + setCursor(Qt::CrossCursor); } - QPixmap mPixmap; -}; + mMidHelpTextFont.setPointSize(12); + mMidHelpText.prepare(QTransform(), mMidHelpTextFont); + mMidHelpText.setPerformanceHint(QStaticText::AggressiveCaching); -struct QuickEditor::QuickEditorPrivate -{ - KDeclarative::KDeclarative *mDecl; - QQuickView *mQuickView; - QQmlEngine *mQmlEngine; - QRect mGrabRect; - QSharedPointer mCurrentGrabResult; -}; - -QuickEditor::QuickEditor(const QPixmap &pixmap, QObject *parent) : - QObject(parent), - mImageStore(new ImageStore(pixmap)), - d_ptr(new QuickEditorPrivate) + mBottomHelpTextFont.setPointSize(9); + mBottomHelpText.prepare(QTransform(), mBottomHelpTextFont); + mBottomHelpText.setPerformanceHint(QStaticText::AggressiveCaching); + + QPalette pal = palette(); + setAutoFillBackground(true); + pal.setBrush(QPalette::Window, Qt::transparent); + setPalette(pal); + update(); +} + +inline void QuickEditor::acceptSelection() { + if (mSelection.isEmpty()) { + SpectacleConfig::instance()->setCropRegion(mSelection); + emit grabCancelled(); + } else { + const qreal dpr = devicePixelRatioF(); + QRect scaledCropRegion = QRect( + mSelection.x() * dpr, + mSelection.y() * dpr, + mSelection.width() * dpr, + mSelection.height() * dpr + ); + SpectacleConfig::instance()->setCropRegion(scaledCropRegion); + emit grabDone(mPixmap.copy(scaledCropRegion)); + } + delete this; +} + +void QuickEditor::keyPressEvent(QKeyEvent* event) { - Q_D(QuickEditor); + switch(event->key()) { + case Qt::Key_Escape: + emit grabCancelled(); + delete this; + break; + case Qt::Key_Return: + case Qt::Key_Enter: + acceptSelection(); + default: + break; + } +} - d->mQmlEngine = new QQmlEngine(); - d->mDecl = new KDeclarative::KDeclarative; - d->mDecl->setDeclarativeEngine(d->mQmlEngine); +void QuickEditor::mousePressEvent(QMouseEvent* event) +{ + if (event->button() & Qt::LeftButton) { + const QPoint& pos = event->pos(); + mMouseDragState = whereIsTheMouse(pos); + switch(mMouseDragState) { + case MouseState::Outside: + mStartPos = event->pos(); + break; + case MouseState::Inside: + mStartPos = event->pos(); + 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(); + default: + break; + } + } + event->accept(); +} -#if KDECLARATIVE_VERSION >= QT_VERSION_CHECK(5, 45, 0) - d->mDecl->setupEngine(d->mQmlEngine); - d->mDecl->setupContext(); -#else - d->mDecl->setupBindings(); -#endif +void QuickEditor::mouseMoveEvent(QMouseEvent* event) +{ + const QPoint& pos = event->pos(); + switch (mMouseDragState) { + case MouseState::None: { + setMouseCursor(pos); + break; + } + case MouseState::TopLeft: + case MouseState::TopRight: + case MouseState::BottomRight: + case MouseState::BottomLeft: + case MouseState::Outside: + mSelection.setRect( + qMin(pos.x(), mStartPos.x()), + qMin(pos.y(), mStartPos.y()), + qAbs(pos.x() - mStartPos.x()), + qAbs(pos.y() - mStartPos.y()) + ); + update(); + break; + case MouseState::Top: + case MouseState::Bottom: + mSelection.setRect( + mSelection.x(), + qMin(pos.y(), mStartPos.y()), + mSelection.width(), + qAbs(pos.y() - mStartPos.y()) + ); + update(); + break; + case MouseState::Right: + case MouseState::Left: + mSelection.setRect( + qMin(pos.x(), mStartPos.x()), + mSelection.y(), + qAbs(pos.x() - mStartPos.x()), + mSelection.height() + ); + update(); + break; + case MouseState::Inside: { + // 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 - d->mQmlEngine->addImageProvider(QStringLiteral("snapshot"), mImageStore); + // new top left point of the rectangle + QPoint newTopLeft = pos - mStartPos + mInitialTopLeft; - d->mQuickView = new QQuickView(d->mQmlEngine, 0); - d->mQuickView->setClearBeforeRendering(false); - d->mQuickView->setSource(QUrl(QStringLiteral("qrc:///QuickEditor/EditorRoot.qml"))); + // the max coordinates of the top left point + const int maxX = size().width() - mSelection.width(); + const int maxY = size().height() - mSelection.height(); - d->mQuickView->setFlags(Qt::BypassWindowManagerHint | Qt::WindowStaysOnTopHint | Qt::FramelessWindowHint | Qt::Tool); - d->mQuickView->setGeometry(0, 0, pixmap.width(), pixmap.height()); - d->mQuickView->showFullScreen(); + if (newTopLeft.x() < 0) { + // tweak startPos to prevent rectangle from getting stuck + mStartPos.setX(mStartPos.x() + newTopLeft.x()); + newTopLeft.setX(0); + } else { + const int xOffset = newTopLeft.x() - maxX; + if (xOffset > 0) { + mStartPos.setX(mStartPos.x() + xOffset); + newTopLeft.setX(maxX); + } + } - // 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())); + if (newTopLeft.y() < 0) { + mStartPos.setY(mStartPos.y() + newTopLeft.y()); + newTopLeft.setY(0); + } else { + const int yOffset = newTopLeft.y() - maxY; + if (yOffset > 0) { + mStartPos.setY(mStartPos.y() + yOffset); + newTopLeft.setY(maxY); + } + } - // 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) - ); + // only move if the newTopLeft is different from the old one + newTopLeft -= mSelection.topLeft(); + if (!newTopLeft.isNull()) { + mSelection.translate(newTopLeft); + update(); } } + default: + break; + } + event->accept(); +} - if (config->useLightRegionMaskColour()) { - rootItem->setProperty("maskColour", QColor(255, 255, 255, 100)); - rootItem->setProperty("strokeColour", QColor(96, 96, 96, 255)); +void QuickEditor::mouseReleaseEvent(QMouseEvent* event) +{ + 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(); } -QuickEditor::~QuickEditor() +void QuickEditor::mouseDoubleClickEvent(QMouseEvent* event) { - Q_D(QuickEditor); - delete d->mQuickView; - delete d->mDecl; - delete d->mQmlEngine; + if (event->button() == Qt::LeftButton && !mSelection.isEmpty() && mSelection.contains(event->pos()), true) { + event->accept(); + acceptSelection(); + } +} + +void QuickEditor::paintEvent(QPaintEvent*) { + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + const QRect& fullRect = geometry(); + painter.drawPixmap(fullRect, mPixmap); + if (!mSelection.size().isNull()) { + painter.setPen(mStrokeColor); + painter.drawRect(mSelection); + painter.setClipRegion(QRegion(fullRect).subtracted(mSelection)); + painter.fillRect(fullRect, mMaskColour); + painter.setClipping(false); + + if ((mSelection.width() > 20) && (mSelection.height() > 20)) { + const qreal leftX = mSelection.x(); + const qreal width = mSelection.width(); + const qreal centerX = leftX + width / 2.0; + const qreal rightX = leftX + width; + + const qreal topY = mSelection.y(); + const qreal height = mSelection.height(); + const qreal centerY = topY + height / 2.0; + const qreal bottomY = topY + height; + + QPainterPath path; + + const qreal cornerHandleDiameter = 2 * cornerHandleRadius; + // top-left handle + path.moveTo(leftX, topY); + path.arcTo(leftX - cornerHandleRadius, topY - cornerHandleRadius, cornerHandleDiameter, cornerHandleDiameter, 0, -90); + painter.fillPath(path, mStrokeColor); + + // top-right handle + path.moveTo(rightX, topY); + path.arcTo(rightX - cornerHandleRadius, topY - cornerHandleRadius, cornerHandleDiameter, cornerHandleDiameter, 180, 90); + painter.fillPath(path, mStrokeColor); + + // bottom-left handle + path.moveTo(leftX, bottomY); + path.arcTo(leftX - cornerHandleRadius, bottomY - cornerHandleRadius, cornerHandleDiameter, cornerHandleDiameter, 0, 90); + painter.fillPath(path, mStrokeColor); + + // bottom-right handle + path.moveTo(rightX, bottomY); + path.arcTo(rightX - cornerHandleRadius, bottomY - cornerHandleRadius, cornerHandleDiameter, cornerHandleDiameter, 180, -90); + painter.fillPath(path, mStrokeColor); + + const qreal midHandleDiameter = 2 * midHandleRadius; + // top-center handle + path.moveTo(centerX, topY); + path.arcTo(centerX - midHandleRadius, topY - midHandleRadius, midHandleDiameter, midHandleDiameter, 0, -180); + painter.fillPath(path, mStrokeColor); - delete d_ptr; + // right-center handle + path.moveTo(rightX, centerY); + path.arcTo(rightX - midHandleRadius, centerY - midHandleRadius, midHandleDiameter, midHandleDiameter, 90, 180); + painter.fillPath(path, mStrokeColor); + + // bottom-center handle + path.moveTo(centerX, bottomY); + path.arcTo(centerX - midHandleRadius, bottomY - midHandleRadius, midHandleDiameter, midHandleDiameter, 0, 180); + painter.fillPath(path, mStrokeColor); + + // left-center handle + path.moveTo(leftX, centerY); + path.arcTo(leftX - midHandleRadius, centerY - midHandleRadius, midHandleDiameter, midHandleDiameter, 90, -180); + painter.fillPath(path, mStrokeColor); + } + // 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! + const qreal dpr = devicePixelRatioF(); + QString selectionText = QString(tr("%1x%2")).arg(qRound(mSelection.width() * dpr)).arg(qRound(mSelection.height() * dpr)); + const QRect selectionTextRect = painter.boundingRect(rect(), 0, selectionText); + int selectionBoxX = qMax(0, mSelection.x() + (mSelection.width() - selectionTextRect.width()) / 2); + int selectionBoxY; + if ((mSelection.width() > selectionSizeThreshold) && (mSelection.height() > selectionSizeThreshold)) { + // show inside the box + selectionBoxY = mSelection.y() + (mSelection.height() + selectionTextRect.height()) / 2; + } else { + // show on top by default + selectionBoxY = mSelection.y() - 8; + if (selectionBoxY < selectionTextRect.height()) { + // show at the bottom + selectionBoxY = mSelection.y() + mSelection.height() + selectionTextRect.height() + 6; + } + } + // Now do the actual box, border and text drawing + QPalette pal; + painter.setBrush(pal.window()); + painter.setPen(pal.windowText().color()); + painter.drawRect(selectionBoxX - 4, selectionBoxY - selectionTextRect.height() - 2, selectionTextRect.width() + 10, selectionTextRect.height() + 8); + painter.drawText(selectionBoxX - 4, selectionBoxY - selectionTextRect.height() - 2, selectionTextRect.width() + 10, selectionTextRect.height() + 8, Qt::AlignCenter, selectionText); + + QSize textSize = mBottomHelpText.size().toSize(); + QPoint pos((size().width() - textSize.width()) / 2, size().height() - textSize.height() - 8); + QRect bottomHelp(pos.x() - 12, pos.y() - 8, textSize.width() + 24, textSize.height() + 16); + + // display bottom help text only if it does not intersect with the selection + if (!mSelection.intersects(bottomHelp)) { + painter.setPen(Qt::black); + painter.setBrush(QColor::fromRgbF(1, 1, 1, 0.85)); + painter.drawRect(bottomHelp); + painter.setFont(mBottomHelpTextFont); + painter.drawStaticText(pos, mBottomHelpText); + } + } else { + painter.fillRect(fullRect, mMaskColour); + QSize textSize = mMidHelpText.size().toSize(); + QPoint pos((fullRect.width() - textSize.width()) / 2, (fullRect.height() - textSize.height()) / 2); + painter.setBrush(QColor::fromRgbF(1, 1, 1, 0.85)); + QPen rectBorder(Qt::black); + rectBorder.setWidth(2); + painter.setPen(rectBorder); + painter.drawRoundedRect(QRect(pos.x() - 20, pos.y() - 20, textSize.width() + 40, textSize.height() + 40), 10, 10); + painter.setPen(Qt::black); + painter.setFont(mMidHelpTextFont); + painter.drawStaticText(pos, mMidHelpText); + } } -void QuickEditor::acceptImageHandler(int x, int y, int width, int height) +inline void QuickEditor::setMouseCursor(const QPoint& pos) { + MouseState mouseState = whereIsTheMouse(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::whereIsTheMouse(const QPoint& pos) { - Q_D(QuickEditor); + if (mSelection.contains(pos, true)) { + const int topEdgeOffset = pos.y() - mSelection.top(); + const int rightEdgeOffset = mSelection.right() - pos.x(); + const int bottomEdgeOffset = mSelection.bottom() - pos.y(); + const int leftEdgeOffset = pos.x() - mSelection.left(); + const int verSize = qMin(mouseAreaSize, mSelection.height()); + const int horSize = qMin(mouseAreaSize, mSelection.width()); - if ((x == -1) && (y == -1) && (width == -1) && (height == -1)) { - SpectacleConfig::instance()->setCropRegion(QRect()); - emit grabCancelled(); - return; - } + auto withinThreshold = [](const int offset, const int size) { + return offset < size && offset > 0; + }; - auto pixelRatio = d->mQuickView->devicePixelRatio(); - d->mGrabRect = QRect(x * pixelRatio, y * pixelRatio, width * pixelRatio, height * pixelRatio); - SpectacleConfig::instance()->setCropRegion(d->mGrabRect); + const bool withinTopEdge = withinThreshold(topEdgeOffset, verSize); + const bool withinRightEdge = withinThreshold(rightEdgeOffset, horSize); + const bool withinBottomEdge = withinThreshold(bottomEdgeOffset, verSize); + const bool withinLeftEdge = withinThreshold(leftEdgeOffset, horSize); - d->mQuickView->hide(); - emit grabDone(mImageStore->mPixmap.copy(d->mGrabRect), d->mGrabRect); + if (withinTopEdge) { + if (withinLeftEdge) { + return MouseState::TopLeft; + } else if (withinRightEdge) { + return MouseState::TopRight; + } 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,292 +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; - - 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; - } - - onPositionChanged: { - if ((parent.x + mouse.x) < brxLimit) { - parent.x = parent.x + mouse.x; - parent.width = parent.width - mouse.x; - } - - if ((parent.y + mouse.y) < bryLimit) { - parent.y = parent.y + mouse.y; - parent.height = parent.height - mouse.y; - } - - drawCanvas.requestPaint(); - } - } - - 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; - } - - onPositionChanged: { - if ((parent.x + parent.width + mouse.x) > brxLimit) { - parent.width = parent.width + mouse.x - mouseAreaSize + 1; - } - - if ((parent.y + mouse.y) < bryLimit) { - parent.y = parent.y + mouse.y; - parent.height = parent.height - mouse.y; - } - - drawCanvas.requestPaint(); - } - } - - 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; - } - - onPositionChanged: { - if ((parent.x + mouse.x) < brxLimit) { - parent.x = parent.x + mouse.x; - parent.width = parent.width - mouse.x; - } - - if ((parent.y + parent.height + mouse.y) > bryLimit) { - parent.height = parent.height + mouse.y - mouseAreaSize + 1; - } - - drawCanvas.requestPaint(); - } - } - - 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; - } - - onPositionChanged: { - if ((parent.x + parent.width + mouse.x) > brxLimit) { - parent.width = parent.width + mouse.x - mouseAreaSize + 1; - } - - if ((parent.y + parent.height + mouse.y) > bryLimit) { - parent.height = parent.height + mouse.y - mouseAreaSize + 1; - } - drawCanvas.requestPaint(); - } - } - - 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; - } - - onPositionChanged: { - if ((parent.y + mouse.y) < limit) { - parent.y = parent.y + mouse.y; - parent.height = parent.height - mouse.y; - } - - drawCanvas.requestPaint(); - } - } - - 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; - } - - onPositionChanged: { - if ((parent.y + parent.height + mouse.y) > limit) { - parent.height = parent.height + mouse.y - mouseAreaSize + 1; - } - - drawCanvas.requestPaint(); - } - } - - 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; - } - - onPositionChanged: { - if ((parent.x + mouse.x) < limit) { - parent.x = parent.x + mouse.x; - parent.width = parent.width - mouse.x; - } - - drawCanvas.requestPaint(); - } - } - - 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; - } - - onPositionChanged: { - if ((parent.x + parent.width + mouse.x) > limit) { - parent.width = parent.width + mouse.x - mouseAreaSize + 1; - } - - drawCanvas.requestPaint(); - } - } -} diff --git a/src/SpectacleCore.cpp b/src/SpectacleCore.cpp --- a/src/SpectacleCore.cpp +++ b/src/SpectacleCore.cpp @@ -66,12 +66,12 @@ mImageGrabber = new KWinWaylandImageGrabber; } #ifdef XCB_FOUND - if (!mImageGrabber && KWindowSystem::isPlatformX11()) { + else if (KWindowSystem::isPlatformX11()) { mImageGrabber = new X11ImageGrabber; } #endif - if (!mImageGrabber) { + else { mImageGrabber = new DummyImageGrabber; } @@ -110,6 +110,7 @@ if (mMainWindow) { delete mMainWindow; } + delete mImageGrabber; } // Q_PROPERTY stuff