From f9f7b84cb4361522c8fef7a32187df1751bc0860 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Gr=C3=A4=C3=9Flin?= Date: Wed, 23 Nov 2016 15:53:17 +0100 Subject: [PATCH] Add interactive position selection to screenshot screen under cursor Summary: A second interactive selection mode gets added to select a position on the screen. This is handled by the same input event filter as for the window selection. Just that instead of returning a window, it returns a QPoint. This allows to pick a point on the screen which we need to screenshot the screen under the mouse cursor and in future for color picking. The screenshot effect provides two new dbus methods to (interactively) select a screen or fullscreen. This allows spectacle to screenshot the (full) screen with still having the user in control. Reviewers: #kwin, #plasma_on_wayland, bgupta Subscribers: plasma-devel, kwin Tags: #plasma_on_wayland, #kwin Differential Revision: https://phabricator.kde.org/D3475 --- .../integration/window_selection_test.cpp | 86 +++++++++++++++ autotests/mock_effectshandler.h | 3 + effects.cpp | 5 + effects.h | 1 + effects/screenshot/screenshot.cpp | 101 +++++++++++++++++- effects/screenshot/screenshot.h | 33 +++++- input.cpp | 34 +++++- input.h | 1 + libkwineffects/kwineffects.h | 15 +++ platform.cpp | 9 ++ platform.h | 16 +++ 11 files changed, 295 insertions(+), 9 deletions(-) diff --git a/autotests/integration/window_selection_test.cpp b/autotests/integration/window_selection_test.cpp index f25d900f62..18472f6dfa 100644 --- a/autotests/integration/window_selection_test.cpp +++ b/autotests/integration/window_selection_test.cpp @@ -55,6 +55,8 @@ private Q_SLOTS: void testSelectOnWindowKeyboard(); void testCancelOnWindowPointer(); void testCancelOnWindowKeyboard(); + + void testSelectPointPointer(); }; void TestWindowSelection::initTestCase() @@ -365,5 +367,89 @@ void TestWindowSelection::testCancelOnWindowKeyboard() kwinApp()->platform()->keyboardKeyReleased(KEY_ESC, timestamp++); } +void TestWindowSelection::testSelectPointPointer() +{ + // this test verifies point selection through pointer works + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createShellSurface(surface.data())); + QScopedPointer pointer(Test::waylandSeat()->createPointer()); + QScopedPointer keyboard(Test::waylandSeat()->createKeyboard()); + QSignalSpy pointerEnteredSpy(pointer.data(), &Pointer::entered); + QVERIFY(pointerEnteredSpy.isValid()); + QSignalSpy pointerLeftSpy(pointer.data(), &Pointer::left); + QVERIFY(pointerLeftSpy.isValid()); + QSignalSpy keyboardEnteredSpy(keyboard.data(), &Keyboard::entered); + QVERIFY(keyboardEnteredSpy.isValid()); + QSignalSpy keyboardLeftSpy(keyboard.data(), &Keyboard::left); + QVERIFY(keyboardLeftSpy.isValid()); + + auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(keyboardEnteredSpy.wait()); + KWin::Cursor::setPos(client->geometry().center()); + QCOMPARE(input()->pointer()->window().data(), client); + QVERIFY(pointerEnteredSpy.wait()); + + QPoint point; + auto callback = [&point] (const QPoint &p) { + point = p; + }; + + // start the interaction + QCOMPARE(input()->isSelectingWindow(), false); + kwinApp()->platform()->startInteractivePositionSelection(callback); + QCOMPARE(input()->isSelectingWindow(), true); + QCOMPARE(point, QPoint()); + QCOMPARE(keyboardLeftSpy.count(), 0); + QVERIFY(pointerLeftSpy.wait()); + if (keyboardLeftSpy.isEmpty()) { + QVERIFY(keyboardLeftSpy.wait()); + } + QCOMPARE(pointerLeftSpy.count(), 1); + QCOMPARE(keyboardLeftSpy.count(), 1); + + // trying again should not be allowed + QPoint point2; + kwinApp()->platform()->startInteractivePositionSelection([&point2] (const QPoint &p) { + point2 = p; + }); + QCOMPARE(point2, QPoint(-1, -1)); + + // simulate left button press + quint32 timestamp = 0; + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); + // should not have ended the mode + QCOMPARE(input()->isSelectingWindow(), true); + QCOMPARE(point, QPoint()); + QVERIFY(input()->pointer()->window().isNull()); + + // updating the pointer should not change anything + input()->pointer()->update(); + QVERIFY(input()->pointer()->window().isNull()); + // updating keyboard should also not change + input()->keyboard()->update(); + + // perform a right button click + kwinApp()->platform()->pointerButtonPressed(BTN_RIGHT, timestamp++); + kwinApp()->platform()->pointerButtonReleased(BTN_RIGHT, timestamp++); + // should not have ended the mode + QCOMPARE(input()->isSelectingWindow(), true); + QCOMPARE(point, QPoint()); + // now release + kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); + QCOMPARE(input()->isSelectingWindow(), false); + QCOMPARE(point, input()->globalPointer().toPoint()); + QCOMPARE(input()->pointer()->window().data(), client); + // should give back keyboard and pointer + QVERIFY(pointerEnteredSpy.wait()); + if (keyboardEnteredSpy.count() != 2) { + QVERIFY(keyboardEnteredSpy.wait()); + } + QCOMPARE(pointerLeftSpy.count(), 1); + QCOMPARE(keyboardLeftSpy.count(), 1); + QCOMPARE(pointerEnteredSpy.count(), 2); + QCOMPARE(keyboardEnteredSpy.count(), 2); +} + WAYLANDTEST_MAIN(TestWindowSelection) #include "window_selection_test.moc" diff --git a/autotests/mock_effectshandler.h b/autotests/mock_effectshandler.h index 1923c7b336..1a1e057332 100644 --- a/autotests/mock_effectshandler.h +++ b/autotests/mock_effectshandler.h @@ -248,6 +248,9 @@ public: void startInteractiveWindowSelection(std::function callback) override { callback(nullptr); } + void startInteractivePositionSelection(std::function callback) override { + callback(QPoint(-1, -1)); + } private: bool m_animationsSuported = true; diff --git a/effects.cpp b/effects.cpp index bb5bddaea5..7c51236a63 100644 --- a/effects.cpp +++ b/effects.cpp @@ -1585,6 +1585,11 @@ void EffectsHandlerImpl::startInteractiveWindowSelection(std::function callback) +{ + kwinApp()->platform()->startInteractivePositionSelection(callback); +} + //**************************************** // EffectWindowImpl //**************************************** diff --git a/effects.h b/effects.h index b4ed46963d..58e4f7ce36 100644 --- a/effects.h +++ b/effects.h @@ -232,6 +232,7 @@ public: void showCursor() override; void startInteractiveWindowSelection(std::function callback) override; + void startInteractivePositionSelection(std::function callback) override; Scene *scene() const { return m_scene; diff --git a/effects/screenshot/screenshot.cpp b/effects/screenshot/screenshot.cpp index 9f7da1cd73..0eae92bce2 100644 --- a/effects/screenshot/screenshot.cpp +++ b/effects/screenshot/screenshot.cpp @@ -251,7 +251,22 @@ void ScreenShotEffect::postPaintScreen() void ScreenShotEffect::sendReplyImage(const QImage &img) { - m_replyConnection.send(m_replyMessage.createReply(saveTempImage(img))); + if (m_fd != -1) { + QtConcurrent::run( + [] (int fd, const QImage &img) { + QFile file; + if (file.open(fd, QIODevice::WriteOnly, QFileDevice::AutoCloseHandle)) { + QDataStream ds(&file); + ds << img; + file.close(); + } else { + close(fd); + } + }, m_fd, img); + m_fd = -1; + } else { + m_replyConnection.send(m_replyMessage.createReply(saveTempImage(img))); + } m_scheduledGeometry = QRect(); m_multipleOutputsImage = QImage(); m_multipleOutputsRendered = QRegion(); @@ -335,7 +350,7 @@ QString ScreenShotEffect::interactive(int mask) } }); - showInfoMessage(); + showInfoMessage(InfoMessageMode::Window); return QString(); } @@ -370,10 +385,10 @@ void ScreenShotEffect::interactive(QDBusUnixFileDescriptor fd, int mask) } }); - showInfoMessage(); + showInfoMessage(InfoMessageMode::Window); } -void ScreenShotEffect::showInfoMessage() +void ScreenShotEffect::showInfoMessage(InfoMessageMode mode) { if (!m_infoFrame.isNull()) { return; @@ -384,7 +399,14 @@ void ScreenShotEffect::showInfoMessage() m_infoFrame->setFont(font); QRect area = effects->clientArea(ScreenArea, effects->activeScreen(), effects->currentDesktop()); m_infoFrame->setPosition(QPoint(area.x() + area.width() / 2, area.y() + area.height() / 3)); - m_infoFrame->setText(i18n("Select window to screen shot with left click or enter.\nEscape or right click to cancel.")); + switch (mode) { + case InfoMessageMode::Window: + m_infoFrame->setText(i18n("Select window to screen shot with left click or enter.\nEscape or right click to cancel.")); + break; + case InfoMessageMode::Screen: + m_infoFrame->setText(i18n("Create screen shot with left click or enter.\nEscape or right click to cancel.")); + break; + } effects->addRepaintFull(); } @@ -411,6 +433,38 @@ QString ScreenShotEffect::screenshotFullscreen(bool captureCursor) return QString(); } +void ScreenShotEffect::screenshotFullscreen(QDBusUnixFileDescriptor fd, bool captureCursor) +{ + if (!calledFromDBus()) { + return; + } + if (!m_scheduledGeometry.isNull()) { + sendErrorReply(QDBusError::Failed, "A screenshot is already been taken"); + return; + } + m_fd = dup(fd.fileDescriptor()); + if (m_fd == -1) { + sendErrorReply(QDBusError::Failed, "No valid file descriptor"); + return; + } + m_captureCursor = captureCursor; + + showInfoMessage(InfoMessageMode::Screen); + effects->startInteractivePositionSelection( + [this] (const QPoint &p) { + hideInfoMessage(); + if (p == QPoint(-1, -1)) { + // error condition + close(m_fd); + m_fd = -1; + } else { + m_scheduledGeometry = effects->virtualScreenGeometry(); + effects->addRepaint(m_scheduledGeometry); + } + } + ); +} + QString ScreenShotEffect::screenshotScreen(int screen, bool captureCursor) { if (!calledFromDBus()) { @@ -433,6 +487,43 @@ QString ScreenShotEffect::screenshotScreen(int screen, bool captureCursor) return QString(); } +void ScreenShotEffect::screenshotScreen(QDBusUnixFileDescriptor fd, bool captureCursor) +{ + if (!calledFromDBus()) { + return; + } + if (!m_scheduledGeometry.isNull()) { + sendErrorReply(QDBusError::Failed, "A screenshot is already been taken"); + return; + } + m_fd = dup(fd.fileDescriptor()); + if (m_fd == -1) { + sendErrorReply(QDBusError::Failed, "No valid file descriptor"); + return; + } + m_captureCursor = captureCursor; + + showInfoMessage(InfoMessageMode::Screen); + effects->startInteractivePositionSelection( + [this] (const QPoint &p) { + hideInfoMessage(); + if (p == QPoint(-1, -1)) { + // error condition + close(m_fd); + m_fd = -1; + } else { + m_scheduledGeometry = effects->clientArea(FullScreenArea, effects->screenNumber(p), 0); + if (m_scheduledGeometry.isNull()) { + close(m_fd); + m_fd = -1; + return; + } + effects->addRepaint(m_scheduledGeometry); + } + } + ); +} + QString ScreenShotEffect::screenshotArea(int x, int y, int width, int height, bool captureCursor) { if (!calledFromDBus()) { diff --git a/effects/screenshot/screenshot.h b/effects/screenshot/screenshot.h index 8a54aa5639..8a2b5c84df 100644 --- a/effects/screenshot/screenshot.h +++ b/effects/screenshot/screenshot.h @@ -85,6 +85,20 @@ public Q_SLOTS: * @returns Path to stored screenshot, or null string in failure case. **/ Q_SCRIPTABLE QString screenshotFullscreen(bool captureCursor = false); + /** + * Starts an interactive screenshot session. + * + * The user is asked to confirm that a screenshot is taken by having to actively + * click and giving the possibility to cancel. + * + * Once the screenshot is taken it gets saved into the @p fd passed to the + * method. It is intended to be used with a pipe, so that the invoking side can just + * read from the pipe. The image gets written into the fd using a QDataStream. + * + * @param fd File descriptor into which the screenshot should be saved + * @param captureCursor Whether to include the mouse cursor + **/ + Q_SCRIPTABLE void screenshotFullscreen(QDBusUnixFileDescriptor fd, bool captureCursor = false); /** * Saves a screenshot of the screen identified by @p screen into a file and returns the path to the file. * Functionality requires hardware support, if not available a null string is returned. @@ -93,6 +107,19 @@ public Q_SLOTS: * @returns Path to stored screenshot, or null string in failure case. **/ Q_SCRIPTABLE QString screenshotScreen(int screen, bool captureCursor = false); + /** + * Starts an interactive screenshot of a screen session. + * + * The user is asked to select the screen to screenshot. + * + * Once the screenshot is taken it gets saved into the @p fd passed to the + * method. It is intended to be used with a pipe, so that the invoking side can just + * read from the pipe. The image gets written into the fd using a QDataStream. + * + * @param fd File descriptor into which the screenshot should be saved + * @param captureCursor Whether to include the mouse cursor + **/ + Q_SCRIPTABLE void screenshotScreen(QDBusUnixFileDescriptor fd, bool captureCursor = false); /** * Saves a screenshot of the selected geometry into a file and returns the path to the file. * Functionality requires hardware support, if not available a null string is returned. @@ -116,7 +143,11 @@ private: QImage blitScreenshot(const QRect &geometry); QString saveTempImage(const QImage &img); void sendReplyImage(const QImage &img); - void showInfoMessage(); + enum class InfoMessageMode { + Window, + Screen + }; + void showInfoMessage(InfoMessageMode mode); void hideInfoMessage(); EffectWindow *m_scheduledScreenshot; ScreenShotType m_type; diff --git a/input.cpp b/input.cpp index c55ac3a8dd..bdce5000b8 100644 --- a/input.cpp +++ b/input.cpp @@ -554,24 +554,42 @@ public: m_callback = callback; input()->keyboard()->update(); } + void start(std::function callback) { + Q_ASSERT(!m_active); + m_active = true; + m_pointSelectionFallback = callback; + input()->keyboard()->update(); + } private: void deactivate() { m_active = false; m_callback = std::function(); + m_pointSelectionFallback = std::function(); input()->pointer()->removeWindowSelectionCursor(); input()->keyboard()->update(); } void cancel() { - m_callback(nullptr); + if (m_callback) { + m_callback(nullptr); + } + if (m_pointSelectionFallback) { + m_pointSelectionFallback(QPoint(-1, -1)); + } deactivate(); } void accept() { - // TODO: this ignores shaped windows - m_callback(input()->findToplevel(input()->globalPointer().toPoint())); + if (m_callback) { + // TODO: this ignores shaped windows + m_callback(input()->findToplevel(input()->globalPointer().toPoint())); + } + if (m_pointSelectionFallback) { + m_pointSelectionFallback(input()->globalPointer().toPoint()); + } deactivate(); } bool m_active = false; std::function m_callback; + std::function m_pointSelectionFallback; }; class GlobalShortcutFilter : public InputEventFilter { @@ -1757,6 +1775,16 @@ void InputRedirection::startInteractiveWindowSelection(std::functionsetWindowSelectionCursor(cursorName); } +void InputRedirection::startInteractivePositionSelection(std::function callback) +{ + if (!m_windowSelector || m_windowSelector->isActive()) { + callback(QPoint(-1, -1)); + return; + } + m_windowSelector->start(callback); + m_pointer->setWindowSelectionCursor(QByteArray()); +} + bool InputRedirection::isSelectingWindow() const { return m_windowSelector ? m_windowSelector->isActive() : false; diff --git a/input.h b/input.h index a3579fca7b..5a33551a94 100644 --- a/input.h +++ b/input.h @@ -170,6 +170,7 @@ public: bool hasAlphaNumericKeyboard(); void startInteractiveWindowSelection(std::function callback, const QByteArray &cursorName); + void startInteractivePositionSelection(std::function callback); bool isSelectingWindow() const; Q_SIGNALS: diff --git a/libkwineffects/kwineffects.h b/libkwineffects/kwineffects.h index ceb4daab7f..63e63dc667 100644 --- a/libkwineffects/kwineffects.h +++ b/libkwineffects/kwineffects.h @@ -1214,6 +1214,21 @@ public: **/ virtual void startInteractiveWindowSelection(std::function callback) = 0; + /** + * Starts an interactive position selection process. + * + * Once the user selected a position on the screen the @p callback is invoked with + * the selected point as argument. In case the user cancels the interactive position selection + * or selecting a position is currently not possible (e.g. screen locked) the @p callback + * is invoked with a point at @c -1 as x and y argument. + * + * During the interactive window selection the cursor is turned into a crosshair cursor. + * + * @param callback The function to invoke once the interactive position selection ends + * @since 5.9 + **/ + virtual void startInteractivePositionSelection(std::function callback) = 0; + Q_SIGNALS: /** * Signal emitted when the current desktop changed. diff --git a/platform.cpp b/platform.cpp index de43251266..57f24c80f1 100644 --- a/platform.cpp +++ b/platform.cpp @@ -377,4 +377,13 @@ void Platform::startInteractiveWindowSelection(std::functionstartInteractiveWindowSelection(callback, cursorName); } +void Platform::startInteractivePositionSelection(std::function callback) +{ + if (!input()) { + callback(QPoint(-1, -1)); + return; + } + input()->startInteractivePositionSelection(callback); +} + } diff --git a/platform.h b/platform.h index 55b034c33d..87ba3658d7 100644 --- a/platform.h +++ b/platform.h @@ -179,6 +179,22 @@ public: **/ virtual void startInteractiveWindowSelection(std::function callback, const QByteArray &cursorName = QByteArray()); + /** + * Starts an interactive position selection process. + * + * Once the user selected a position on the screen the @p callback is invoked with + * the selected point as argument. In case the user cancels the interactive position selection + * or selecting a position is currently not possible (e.g. screen locked) the @p callback + * is invoked with a point at @c -1 as x and y argument. + * + * During the interactive window selection the cursor is turned into a crosshair cursor. + * + * The default implementation forwards to InputRedirection. + * + * @param callback The function to invoke once the interactive position selection ends + **/ + virtual void startInteractivePositionSelection(std::function callback); + bool usesSoftwareCursor() const { return m_softWareCursor; }