diff --git a/autotests/integration/window_selection_test.cpp b/autotests/integration/window_selection_test.cpp --- a/autotests/integration/window_selection_test.cpp +++ b/autotests/integration/window_selection_test.cpp @@ -55,6 +55,8 @@ void testSelectOnWindowKeyboard(); void testCancelOnWindowPointer(); void testCancelOnWindowKeyboard(); + + void testSelectPointPointer(); }; void TestWindowSelection::initTestCase() @@ -365,5 +367,89 @@ 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 --- a/autotests/mock_effectshandler.h +++ b/autotests/mock_effectshandler.h @@ -248,6 +248,9 @@ 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.h b/effects.h --- a/effects.h +++ b/effects.h @@ -232,6 +232,7 @@ 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.cpp b/effects.cpp --- a/effects.cpp +++ b/effects.cpp @@ -1585,6 +1585,11 @@ ); } +void EffectsHandlerImpl::startInteractivePositionSelection(std::function callback) +{ + kwinApp()->platform()->startInteractivePositionSelection(callback); +} + //**************************************** // EffectWindowImpl //**************************************** diff --git a/effects/screenshot/screenshot.h b/effects/screenshot/screenshot.h --- a/effects/screenshot/screenshot.h +++ b/effects/screenshot/screenshot.h @@ -86,14 +86,41 @@ **/ 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. * @param screen Number of screen as numbered by QDesktopWidget * @param captureCursor Whether to include the cursor in the image * @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. * @param x Left upper x coord of region @@ -116,7 +143,11 @@ 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/effects/screenshot/screenshot.cpp b/effects/screenshot/screenshot.cpp --- a/effects/screenshot/screenshot.cpp +++ b/effects/screenshot/screenshot.cpp @@ -251,7 +251,22 @@ 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 @@ } }); - showInfoMessage(); + showInfoMessage(InfoMessageMode::Window); return QString(); } @@ -370,10 +385,10 @@ } }); - showInfoMessage(); + showInfoMessage(InfoMessageMode::Window); } -void ScreenShotEffect::showInfoMessage() +void ScreenShotEffect::showInfoMessage(InfoMessageMode mode) { if (!m_infoFrame.isNull()) { return; @@ -384,7 +399,14 @@ 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 @@ 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 @@ 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/input.h b/input.h --- a/input.h +++ b/input.h @@ -170,6 +170,7 @@ bool hasAlphaNumericKeyboard(); void startInteractiveWindowSelection(std::function callback, const QByteArray &cursorName); + void startInteractivePositionSelection(std::function callback); bool isSelectingWindow() const; Q_SIGNALS: diff --git a/input.cpp b/input.cpp --- a/input.cpp +++ b/input.cpp @@ -554,24 +554,42 @@ 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 @@ m_pointer->setWindowSelectionCursor(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/libkwineffects/kwineffects.h b/libkwineffects/kwineffects.h --- a/libkwineffects/kwineffects.h +++ b/libkwineffects/kwineffects.h @@ -1214,6 +1214,21 @@ **/ 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.h b/platform.h --- a/platform.h +++ b/platform.h @@ -179,6 +179,22 @@ **/ 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; } diff --git a/platform.cpp b/platform.cpp --- a/platform.cpp +++ b/platform.cpp @@ -377,4 +377,13 @@ input()->startInteractiveWindowSelection(callback, cursorName); } +void Platform::startInteractivePositionSelection(std::function callback) +{ + if (!input()) { + callback(QPoint(-1, -1)); + return; + } + input()->startInteractivePositionSelection(callback); +} + }