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
This commit is contained in:
Martin Gräßlin 2016-11-23 15:53:17 +01:00
parent 0b47b84816
commit f9f7b84cb4
11 changed files with 295 additions and 9 deletions

View file

@ -55,6 +55,8 @@ private Q_SLOTS:
void testSelectOnWindowKeyboard(); void testSelectOnWindowKeyboard();
void testCancelOnWindowPointer(); void testCancelOnWindowPointer();
void testCancelOnWindowKeyboard(); void testCancelOnWindowKeyboard();
void testSelectPointPointer();
}; };
void TestWindowSelection::initTestCase() void TestWindowSelection::initTestCase()
@ -365,5 +367,89 @@ void TestWindowSelection::testCancelOnWindowKeyboard()
kwinApp()->platform()->keyboardKeyReleased(KEY_ESC, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_ESC, timestamp++);
} }
void TestWindowSelection::testSelectPointPointer()
{
// this test verifies point selection through pointer works
QScopedPointer<Surface> surface(Test::createSurface());
QScopedPointer<ShellSurface> shellSurface(Test::createShellSurface(surface.data()));
QScopedPointer<Pointer> pointer(Test::waylandSeat()->createPointer());
QScopedPointer<Keyboard> 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) WAYLANDTEST_MAIN(TestWindowSelection)
#include "window_selection_test.moc" #include "window_selection_test.moc"

View file

@ -248,6 +248,9 @@ public:
void startInteractiveWindowSelection(std::function<void(KWin::EffectWindow*)> callback) override { void startInteractiveWindowSelection(std::function<void(KWin::EffectWindow*)> callback) override {
callback(nullptr); callback(nullptr);
} }
void startInteractivePositionSelection(std::function<void (const QPoint &)> callback) override {
callback(QPoint(-1, -1));
}
private: private:
bool m_animationsSuported = true; bool m_animationsSuported = true;

View file

@ -1585,6 +1585,11 @@ void EffectsHandlerImpl::startInteractiveWindowSelection(std::function<void(KWin
); );
} }
void EffectsHandlerImpl::startInteractivePositionSelection(std::function<void(const QPoint&)> callback)
{
kwinApp()->platform()->startInteractivePositionSelection(callback);
}
//**************************************** //****************************************
// EffectWindowImpl // EffectWindowImpl
//**************************************** //****************************************

View file

@ -232,6 +232,7 @@ public:
void showCursor() override; void showCursor() override;
void startInteractiveWindowSelection(std::function<void(KWin::EffectWindow*)> callback) override; void startInteractiveWindowSelection(std::function<void(KWin::EffectWindow*)> callback) override;
void startInteractivePositionSelection(std::function<void(const QPoint &)> callback) override;
Scene *scene() const { Scene *scene() const {
return m_scene; return m_scene;

View file

@ -251,7 +251,22 @@ void ScreenShotEffect::postPaintScreen()
void ScreenShotEffect::sendReplyImage(const QImage &img) 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_scheduledGeometry = QRect();
m_multipleOutputsImage = QImage(); m_multipleOutputsImage = QImage();
m_multipleOutputsRendered = QRegion(); m_multipleOutputsRendered = QRegion();
@ -335,7 +350,7 @@ QString ScreenShotEffect::interactive(int mask)
} }
}); });
showInfoMessage(); showInfoMessage(InfoMessageMode::Window);
return QString(); 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()) { if (!m_infoFrame.isNull()) {
return; return;
@ -384,7 +399,14 @@ void ScreenShotEffect::showInfoMessage()
m_infoFrame->setFont(font); m_infoFrame->setFont(font);
QRect area = effects->clientArea(ScreenArea, effects->activeScreen(), effects->currentDesktop()); 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->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(); effects->addRepaintFull();
} }
@ -411,6 +433,38 @@ QString ScreenShotEffect::screenshotFullscreen(bool captureCursor)
return QString(); 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) QString ScreenShotEffect::screenshotScreen(int screen, bool captureCursor)
{ {
if (!calledFromDBus()) { if (!calledFromDBus()) {
@ -433,6 +487,43 @@ QString ScreenShotEffect::screenshotScreen(int screen, bool captureCursor)
return QString(); 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) QString ScreenShotEffect::screenshotArea(int x, int y, int width, int height, bool captureCursor)
{ {
if (!calledFromDBus()) { if (!calledFromDBus()) {

View file

@ -85,6 +85,20 @@ public Q_SLOTS:
* @returns Path to stored screenshot, or null string in failure case. * @returns Path to stored screenshot, or null string in failure case.
**/ **/
Q_SCRIPTABLE QString screenshotFullscreen(bool captureCursor = false); 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. * 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. * 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. * @returns Path to stored screenshot, or null string in failure case.
**/ **/
Q_SCRIPTABLE QString screenshotScreen(int screen, bool captureCursor = false); 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. * 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. * Functionality requires hardware support, if not available a null string is returned.
@ -116,7 +143,11 @@ private:
QImage blitScreenshot(const QRect &geometry); QImage blitScreenshot(const QRect &geometry);
QString saveTempImage(const QImage &img); QString saveTempImage(const QImage &img);
void sendReplyImage(const QImage &img); void sendReplyImage(const QImage &img);
void showInfoMessage(); enum class InfoMessageMode {
Window,
Screen
};
void showInfoMessage(InfoMessageMode mode);
void hideInfoMessage(); void hideInfoMessage();
EffectWindow *m_scheduledScreenshot; EffectWindow *m_scheduledScreenshot;
ScreenShotType m_type; ScreenShotType m_type;

View file

@ -554,24 +554,42 @@ public:
m_callback = callback; m_callback = callback;
input()->keyboard()->update(); input()->keyboard()->update();
} }
void start(std::function<void(const QPoint &)> callback) {
Q_ASSERT(!m_active);
m_active = true;
m_pointSelectionFallback = callback;
input()->keyboard()->update();
}
private: private:
void deactivate() { void deactivate() {
m_active = false; m_active = false;
m_callback = std::function<void(KWin::Toplevel*)>(); m_callback = std::function<void(KWin::Toplevel*)>();
m_pointSelectionFallback = std::function<void(const QPoint &)>();
input()->pointer()->removeWindowSelectionCursor(); input()->pointer()->removeWindowSelectionCursor();
input()->keyboard()->update(); input()->keyboard()->update();
} }
void cancel() { void cancel() {
m_callback(nullptr); if (m_callback) {
m_callback(nullptr);
}
if (m_pointSelectionFallback) {
m_pointSelectionFallback(QPoint(-1, -1));
}
deactivate(); deactivate();
} }
void accept() { void accept() {
// TODO: this ignores shaped windows if (m_callback) {
m_callback(input()->findToplevel(input()->globalPointer().toPoint())); // TODO: this ignores shaped windows
m_callback(input()->findToplevel(input()->globalPointer().toPoint()));
}
if (m_pointSelectionFallback) {
m_pointSelectionFallback(input()->globalPointer().toPoint());
}
deactivate(); deactivate();
} }
bool m_active = false; bool m_active = false;
std::function<void(KWin::Toplevel*)> m_callback; std::function<void(KWin::Toplevel*)> m_callback;
std::function<void(const QPoint &)> m_pointSelectionFallback;
}; };
class GlobalShortcutFilter : public InputEventFilter { class GlobalShortcutFilter : public InputEventFilter {
@ -1757,6 +1775,16 @@ void InputRedirection::startInteractiveWindowSelection(std::function<void(KWin::
m_pointer->setWindowSelectionCursor(cursorName); m_pointer->setWindowSelectionCursor(cursorName);
} }
void InputRedirection::startInteractivePositionSelection(std::function<void(const QPoint &)> callback)
{
if (!m_windowSelector || m_windowSelector->isActive()) {
callback(QPoint(-1, -1));
return;
}
m_windowSelector->start(callback);
m_pointer->setWindowSelectionCursor(QByteArray());
}
bool InputRedirection::isSelectingWindow() const bool InputRedirection::isSelectingWindow() const
{ {
return m_windowSelector ? m_windowSelector->isActive() : false; return m_windowSelector ? m_windowSelector->isActive() : false;

View file

@ -170,6 +170,7 @@ public:
bool hasAlphaNumericKeyboard(); bool hasAlphaNumericKeyboard();
void startInteractiveWindowSelection(std::function<void(KWin::Toplevel*)> callback, const QByteArray &cursorName); void startInteractiveWindowSelection(std::function<void(KWin::Toplevel*)> callback, const QByteArray &cursorName);
void startInteractivePositionSelection(std::function<void(const QPoint &)> callback);
bool isSelectingWindow() const; bool isSelectingWindow() const;
Q_SIGNALS: Q_SIGNALS:

View file

@ -1214,6 +1214,21 @@ public:
**/ **/
virtual void startInteractiveWindowSelection(std::function<void(KWin::EffectWindow*)> callback) = 0; virtual void startInteractiveWindowSelection(std::function<void(KWin::EffectWindow*)> 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<void(const QPoint &)> callback) = 0;
Q_SIGNALS: Q_SIGNALS:
/** /**
* Signal emitted when the current desktop changed. * Signal emitted when the current desktop changed.

View file

@ -377,4 +377,13 @@ void Platform::startInteractiveWindowSelection(std::function<void(KWin::Toplevel
input()->startInteractiveWindowSelection(callback, cursorName); input()->startInteractiveWindowSelection(callback, cursorName);
} }
void Platform::startInteractivePositionSelection(std::function<void(const QPoint &)> callback)
{
if (!input()) {
callback(QPoint(-1, -1));
return;
}
input()->startInteractivePositionSelection(callback);
}
} }

View file

@ -179,6 +179,22 @@ public:
**/ **/
virtual void startInteractiveWindowSelection(std::function<void(KWin::Toplevel*)> callback, const QByteArray &cursorName = QByteArray()); virtual void startInteractiveWindowSelection(std::function<void(KWin::Toplevel*)> 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<void(const QPoint &)> callback);
bool usesSoftwareCursor() const { bool usesSoftwareCursor() const {
return m_softWareCursor; return m_softWareCursor;
} }