From fd83366e31b9219cb6b74620fb7bf299deda4b45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Gr=C3=A4=C3=9Flin?= Date: Tue, 15 Nov 2016 14:23:51 +0100 Subject: [PATCH] Implement interactive window selection for Wayland platforms Summary: The interactive window selection is implemented in InputRedirection through a dedicated InputEventFilter. The InputEventFilter so far takes care of pointer input and keyboard input. In addition it ensures that keyboard and pointer focus is reset on start and on end. With this change KillWindow now also works on Wayland, but only for X11 windows, as the Wayland variant is not yet implemented. Test Plan: Tested in nested setup, auto-tests still needed Reviewers: #kwin, #plasma_on_wayland Subscribers: plasma-devel, kwin Tags: #plasma_on_wayland, #kwin Differential Revision: https://phabricator.kde.org/D3365 --- autotests/integration/CMakeLists.txt | 1 + .../integration/window_selection_test.cpp | 369 ++++++++++++++++++ input.cpp | 116 ++++++ input.h | 8 + keyboard_input.cpp | 2 +- keyboard_input.h | 2 +- platform.cpp | 8 +- platform.h | 3 +- pointer_input.cpp | 90 ++++- pointer_input.h | 13 +- wayland_cursor_theme.cpp | 6 +- wayland_cursor_theme.h | 1 + 12 files changed, 607 insertions(+), 12 deletions(-) create mode 100644 autotests/integration/window_selection_test.cpp diff --git a/autotests/integration/CMakeLists.txt b/autotests/integration/CMakeLists.txt index cd86c04971..8dcd4c8e06 100644 --- a/autotests/integration/CMakeLists.txt +++ b/autotests/integration/CMakeLists.txt @@ -41,6 +41,7 @@ integrationTest(NAME testScreenChanges SRCS screen_changes_test.cpp) integrationTest(NAME testModiferOnlyShortcut SRCS modifier_only_shortcut_test.cpp) integrationTest(NAME testTabBox SRCS tabbox_test.cpp) integrationTest(NAME testGlobalShortcuts SRCS globalshortcuts_test.cpp) +integrationTest(NAME testWindowSelection SRCS window_selection_test.cpp) if (XCB_ICCCM_FOUND) integrationTest(NAME testMoveResize SRCS move_resize_window_test.cpp LIBS XCB::ICCCM) diff --git a/autotests/integration/window_selection_test.cpp b/autotests/integration/window_selection_test.cpp new file mode 100644 index 0000000000..f25d900f62 --- /dev/null +++ b/autotests/integration/window_selection_test.cpp @@ -0,0 +1,369 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License +along with this program. If not, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "cursor.h" +#include "keyboard_input.h" +#include "platform.h" +#include "pointer_input.h" +#include "shell_client.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace KWin; +using namespace KWayland::Client; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_window_selection-0"); + +class TestWindowSelection : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testSelectOnWindowPointer(); + void testSelectOnWindowKeyboard_data(); + void testSelectOnWindowKeyboard(); + void testCancelOnWindowPointer(); + void testCancelOnWindowKeyboard(); +}; + +void TestWindowSelection::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setOutputCount", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + waylandServer()->initWorkspace(); +} + +void TestWindowSelection::init() +{ + QVERIFY(Test::setupWaylandConnection(s_socketName, Test::AdditionalWaylandInterface::Seat)); + QVERIFY(Test::waitForWaylandPointer()); + + screens()->setCurrent(0); + KWin::Cursor::setPos(QPoint(1280, 512)); +} + +void TestWindowSelection::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void TestWindowSelection::testSelectOnWindowPointer() +{ + // this test verifies window 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()); + + Toplevel *selectedWindow = nullptr; + auto callback = [&selectedWindow] (Toplevel *t) { + selectedWindow = t; + }; + + // start the interaction + QCOMPARE(input()->isSelectingWindow(), false); + kwinApp()->platform()->startInteractiveWindowSelection(callback); + QCOMPARE(input()->isSelectingWindow(), true); + QVERIFY(!selectedWindow); + QCOMPARE(keyboardLeftSpy.count(), 0); + QVERIFY(pointerLeftSpy.wait()); + if (keyboardLeftSpy.isEmpty()) { + QVERIFY(keyboardLeftSpy.wait()); + } + QCOMPARE(pointerLeftSpy.count(), 1); + QCOMPARE(keyboardLeftSpy.count(), 1); + + // simulate left button press + quint32 timestamp = 0; + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); + // should not have ended the mode + QCOMPARE(input()->isSelectingWindow(), true); + QVERIFY(!selectedWindow); + 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); + QVERIFY(!selectedWindow); + // now release + kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); + QCOMPARE(input()->isSelectingWindow(), false); + QCOMPARE(selectedWindow, client); + 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); +} + +void TestWindowSelection::testSelectOnWindowKeyboard_data() +{ + QTest::addColumn("key"); + + QTest::newRow("enter") << KEY_ENTER; + QTest::newRow("keypad enter") << KEY_KPENTER; + QTest::newRow("space") << KEY_SPACE; +} + +void TestWindowSelection::testSelectOnWindowKeyboard() +{ + // this test verifies window selection through keyboard key + 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()); + QVERIFY(!client->geometry().contains(KWin::Cursor::pos())); + + Toplevel *selectedWindow = nullptr; + auto callback = [&selectedWindow] (Toplevel *t) { + selectedWindow = t; + }; + + // start the interaction + QCOMPARE(input()->isSelectingWindow(), false); + kwinApp()->platform()->startInteractiveWindowSelection(callback); + QCOMPARE(input()->isSelectingWindow(), true); + QVERIFY(!selectedWindow); + QCOMPARE(keyboardLeftSpy.count(), 0); + QVERIFY(keyboardLeftSpy.wait()); + QCOMPARE(pointerLeftSpy.count(), 0); + QCOMPARE(keyboardLeftSpy.count(), 1); + + // simulate key press + quint32 timestamp = 0; + // move cursor through keys + auto keyPress = [×tamp] (qint32 key) { + kwinApp()->platform()->keyboardKeyPressed(key, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(key, timestamp++); + }; + while (KWin::Cursor::pos().x() >= client->geometry().x() + client->geometry().width()) { + keyPress(KEY_LEFT); + } + while (KWin::Cursor::pos().x() <= client->geometry().x()) { + keyPress(KEY_RIGHT); + } + while (KWin::Cursor::pos().y() <= client->geometry().y()) { + keyPress(KEY_DOWN); + } + while (KWin::Cursor::pos().y() >= client->geometry().y() + client->geometry().height()) { + keyPress(KEY_UP); + } + QFETCH(qint32, key); + kwinApp()->platform()->keyboardKeyPressed(key, timestamp++); + QCOMPARE(input()->isSelectingWindow(), false); + QCOMPARE(selectedWindow, client); + 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(), 0); + QCOMPARE(keyboardLeftSpy.count(), 1); + QCOMPARE(pointerEnteredSpy.count(), 1); + QCOMPARE(keyboardEnteredSpy.count(), 2); + kwinApp()->platform()->keyboardKeyReleased(key, timestamp++); +} + +void TestWindowSelection::testCancelOnWindowPointer() +{ + // this test verifies that window selection cancels through right button click + 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()); + + Toplevel *selectedWindow = nullptr; + auto callback = [&selectedWindow] (Toplevel *t) { + selectedWindow = t; + }; + + // start the interaction + QCOMPARE(input()->isSelectingWindow(), false); + kwinApp()->platform()->startInteractiveWindowSelection(callback); + QCOMPARE(input()->isSelectingWindow(), true); + QVERIFY(!selectedWindow); + QCOMPARE(keyboardLeftSpy.count(), 0); + QVERIFY(pointerLeftSpy.wait()); + if (keyboardLeftSpy.isEmpty()) { + QVERIFY(keyboardLeftSpy.wait()); + } + QCOMPARE(pointerLeftSpy.count(), 1); + QCOMPARE(keyboardLeftSpy.count(), 1); + + // simulate left button press + quint32 timestamp = 0; + kwinApp()->platform()->pointerButtonPressed(BTN_RIGHT, timestamp++); + kwinApp()->platform()->pointerButtonReleased(BTN_RIGHT, timestamp++); + QCOMPARE(input()->isSelectingWindow(), false); + QVERIFY(!selectedWindow); + 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); +} + +void TestWindowSelection::testCancelOnWindowKeyboard() +{ + // this test verifies that cancel window selection through escape key 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()); + + Toplevel *selectedWindow = nullptr; + auto callback = [&selectedWindow] (Toplevel *t) { + selectedWindow = t; + }; + + // start the interaction + QCOMPARE(input()->isSelectingWindow(), false); + kwinApp()->platform()->startInteractiveWindowSelection(callback); + QCOMPARE(input()->isSelectingWindow(), true); + QVERIFY(!selectedWindow); + QCOMPARE(keyboardLeftSpy.count(), 0); + QVERIFY(pointerLeftSpy.wait()); + if (keyboardLeftSpy.isEmpty()) { + QVERIFY(keyboardLeftSpy.wait()); + } + QCOMPARE(pointerLeftSpy.count(), 1); + QCOMPARE(keyboardLeftSpy.count(), 1); + + // simulate left button press + quint32 timestamp = 0; + kwinApp()->platform()->keyboardKeyPressed(KEY_ESC, timestamp++); + QCOMPARE(input()->isSelectingWindow(), false); + QVERIFY(!selectedWindow); + 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); + kwinApp()->platform()->keyboardKeyReleased(KEY_ESC, timestamp++); +} + +WAYLANDTEST_MAIN(TestWindowSelection) +#include "window_selection_test.moc" diff --git a/input.cpp b/input.cpp index 93a7c445ce..b511156694 100644 --- a/input.cpp +++ b/input.cpp @@ -475,6 +475,105 @@ public: } }; +class WindowSelectorFilter : public InputEventFilter { +public: + bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override { + Q_UNUSED(nativeButton) + if (!m_active) { + return false; + } + switch (event->type()) { + case QEvent::MouseButtonRelease: + if (event->buttons() == Qt::NoButton) { + if (event->button() == Qt::RightButton) { + cancel(); + } else { + accept(); + } + } + break; + default: + break; + } + return true; + } + bool wheelEvent(QWheelEvent *event) override { + Q_UNUSED(event) + // filter out while selecting a window + return m_active; + } + bool keyEvent(QKeyEvent *event) override { + Q_UNUSED(event) + if (!m_active) { + return false; + } + waylandServer()->seat()->setFocusedKeyboardSurface(nullptr); + passToWaylandServer(event); + + if (event->type() == QEvent::KeyPress) { + // x11 variant does this on key press, so do the same + if (event->key() == Qt::Key_Escape) { + cancel(); + } else if (event->key() == Qt::Key_Enter || + event->key() == Qt::Key_Return || + event->key() == Qt::Key_Space) { + accept(); + } + if (input()->supportsPointerWarping()) { + int mx = 0; + int my = 0; + if (event->key() == Qt::Key_Left) { + mx = -10; + } + if (event->key() == Qt::Key_Right) { + mx = 10; + } + if (event->key() == Qt::Key_Up) { + my = -10; + } + if (event->key() == Qt::Key_Down) { + my = 10; + } + if (event->modifiers() & Qt::ControlModifier) { + mx /= 10; + my /= 10; + } + input()->warpPointer(input()->globalPointer() + QPointF(mx, my)); + } + } + // filter out while selecting a window + return true; + } + + bool isActive() const { + return m_active; + } + void start(std::function callback) { + Q_ASSERT(!m_active); + m_active = true; + m_callback = callback; + input()->keyboard()->update(); + } +private: + void deactivate() { + m_active = false; + m_callback = std::function(); + input()->pointer()->removeWindowSelectionCursor(); + input()->keyboard()->update(); + } + void cancel() { + m_callback(nullptr); + deactivate(); + } + void accept() { + // TODO: this ignores shaped windows + m_callback(input()->findToplevel(input()->globalPointer().toPoint())); + deactivate(); + } + bool m_active = false; + std::function m_callback; +}; + class GlobalShortcutFilter : public InputEventFilter { public: bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override { @@ -1307,6 +1406,8 @@ void InputRedirection::setupInputFilters() installInputEventFilter(new TerminateServerFilter); installInputEventFilter(new DragAndDropInputFilter); installInputEventFilter(new LockScreenFilter); + m_windowSelector = new WindowSelectorFilter; + installInputEventFilter(m_windowSelector); } installInputEventFilter(new ScreenEdgeInputFilter); installInputEventFilter(new EffectsFilter); @@ -1647,6 +1748,21 @@ QPointF InputRedirection::globalPointer() const return m_pointer->pos(); } +void InputRedirection::startInteractiveWindowSelection(std::function callback, const QByteArray &cursorName) +{ + if (!m_windowSelector || m_windowSelector->isActive()) { + callback(nullptr); + return; + } + m_windowSelector->start(callback); + m_pointer->setWindowSelectionCursor(cursorName); +} + +bool InputRedirection::isSelectingWindow() const +{ + return m_windowSelector ? m_windowSelector->isActive() : false; +} + InputDeviceHandler::InputDeviceHandler(InputRedirection *input) : QObject(input) , m_input(input) diff --git a/input.h b/input.h index a80009e4fd..a3579fca7b 100644 --- a/input.h +++ b/input.h @@ -28,6 +28,8 @@ along with this program. If not, see . #include +#include + class KGlobalAccelInterface; class QKeySequence; class QMouseEvent; @@ -42,6 +44,7 @@ class InputEventFilter; class KeyboardInputRedirection; class PointerInputRedirection; class TouchInputRedirection; +class WindowSelectorFilter; namespace Decoration { @@ -166,6 +169,9 @@ public: bool hasAlphaNumericKeyboard(); + void startInteractiveWindowSelection(std::function callback, const QByteArray &cursorName); + bool isSelectingWindow() const; + Q_SIGNALS: /** * @brief Emitted when the global pointer position changed @@ -223,6 +229,8 @@ private: LibInput::Connection *m_libInput = nullptr; + WindowSelectorFilter *m_windowSelector = nullptr; + QVector m_filters; KSharedConfigPtr m_inputConfig; diff --git a/keyboard_input.cpp b/keyboard_input.cpp index bb6b37739e..439c624852 100644 --- a/keyboard_input.cpp +++ b/keyboard_input.cpp @@ -613,7 +613,7 @@ void KeyboardInputRedirection::update() break; } while (it != stacking.begin()); } - } else { + } else if (!input()->isSelectingWindow()) { found = workspace()->activeClient(); } if (found && found->surface()) { diff --git a/keyboard_input.h b/keyboard_input.h index 4e16fece12..574ac20484 100644 --- a/keyboard_input.h +++ b/keyboard_input.h @@ -129,7 +129,7 @@ private: LEDs m_leds; }; -class KeyboardInputRedirection : public QObject +class KWIN_EXPORT KeyboardInputRedirection : public QObject { Q_OBJECT public: diff --git a/platform.cpp b/platform.cpp index 48bdd0f56c..aaceda09a5 100644 --- a/platform.cpp +++ b/platform.cpp @@ -365,9 +365,11 @@ void Platform::createOpenGLSafePoint(OpenGLSafePoint safePoint) void Platform::startInteractiveWindowSelection(std::function callback, const QByteArray &cursorName) { - // TODO: provide a basic implementation for Wayland platforms - Q_UNUSED(cursorName) - callback(nullptr); + if (!input()) { + callback(nullptr); + return; + } + input()->startInteractiveWindowSelection(callback, cursorName); } } diff --git a/platform.h b/platform.h index 48db560aed..55b034c33d 100644 --- a/platform.h +++ b/platform.h @@ -172,8 +172,7 @@ public: * @p cursorName is provided. The argument @p cursorName is a QByteArray instead of Qt::CursorShape * to support the "pirate" cursor for kill window which is not wrapped by Qt::CursorShape. * - * The default implementation does not support selecting windows yet and only invokes the - * @p callback with @c nullptr. + * The default implementation forwards to InputRedirection. * * @param callback The function to invoke once the interactive window selection ends * @param cursorName The optional name of the cursor shape to use, default is crosshair diff --git a/pointer_input.cpp b/pointer_input.cpp index 5659631240..360caffb40 100644 --- a/pointer_input.cpp +++ b/pointer_input.cpp @@ -181,6 +181,31 @@ void PointerInputRedirection::updateOnStartMoveResize() waylandServer()->seat()->setFocusedPointerSurface(nullptr); } +void PointerInputRedirection::updateToReset() +{ + if (m_internalWindow) { + disconnect(m_internalWindowConnection); + m_internalWindowConnection = QMetaObject::Connection(); + QEvent event(QEvent::Leave); + QCoreApplication::sendEvent(m_internalWindow.data(), &event); + m_internalWindow.clear(); + } + if (m_decoration) { + QHoverEvent event(QEvent::HoverLeave, QPointF(), QPointF()); + QCoreApplication::instance()->sendEvent(m_decoration->decoration(), &event); + m_decoration.clear(); + } + if (m_window) { + if (AbstractClient *c = qobject_cast(m_window.data())) { + c->leaveEvent(); + } + disconnect(m_windowGeometryConnection); + m_windowGeometryConnection = QMetaObject::Connection(); + m_window.clear(); + } + waylandServer()->seat()->setFocusedPointerSurface(nullptr); +} + void PointerInputRedirection::processMotion(const QPointF &pos, uint32_t time, LibInput::Device *device) { processMotion(pos, QSizeF(), QSizeF(), time, 0, device); @@ -389,6 +414,9 @@ void PointerInputRedirection::update() // ignore during drag and drop return; } + if (input()->isSelectingWindow()) { + return; + } // TODO: handle pointer grab aka popups Toplevel *t = m_input->findToplevel(m_pos.toPoint()); const auto oldDeco = m_decoration; @@ -606,6 +634,25 @@ void PointerInputRedirection::removeEffectsOverrideCursor() m_cursor->removeEffectsOverrideCursor(); } +void PointerInputRedirection::setWindowSelectionCursor(const QByteArray &shape) +{ + if (!m_inited) { + return; + } + // send leave to current pointer focus window + updateToReset(); + m_cursor->setWindowSelectionCursor(shape); +} + +void PointerInputRedirection::removeWindowSelectionCursor() +{ + if (!m_inited) { + return; + } + update(); + m_cursor->removeWindowSelectionCursor(); +} + CursorImage::CursorImage(PointerInputRedirection *parent) : QObject(parent) , m_pointer(parent) @@ -815,6 +862,24 @@ void CursorImage::removeEffectsOverrideCursor() reevaluteSource(); } +void CursorImage::setWindowSelectionCursor(const QByteArray &shape) +{ + if (shape.isEmpty()) { + loadThemeCursor(Qt::CrossCursor, &m_windowSelectionCursor); + } else { + loadThemeCursor(shape, &m_windowSelectionCursor); + } + if (m_currentSource == CursorSource::WindowSelector) { + emit changed(); + } + reevaluteSource(); +} + +void CursorImage::removeWindowSelectionCursor() +{ + reevaluteSource(); +} + void CursorImage::updateDrag() { using namespace KWayland::Server; @@ -880,13 +945,24 @@ void CursorImage::updateDragCursor() } void CursorImage::loadThemeCursor(Qt::CursorShape shape, Image *image) +{ + loadThemeCursor(shape, m_cursors, image); +} + +void CursorImage::loadThemeCursor(const QByteArray &shape, Image *image) +{ + loadThemeCursor(shape, m_cursorsByName, image); +} + +template +void CursorImage::loadThemeCursor(const T &shape, QHash &cursors, Image *image) { loadTheme(); if (!m_cursorTheme) { return; } - auto it = m_cursors.constFind(shape); - if (it == m_cursors.constEnd()) { + auto it = cursors.constFind(shape); + if (it == cursors.constEnd()) { image->image = QImage(); image->hotSpot = QPoint(); wl_cursor_image *cursor = m_cursorTheme->get(shape); @@ -903,7 +979,7 @@ void CursorImage::loadThemeCursor(Qt::CursorShape shape, Image *image) if (!buffer) { return; } - it = decltype(it)(m_cursors.insert(shape, {buffer->data().copy(), QPoint(cursor->hotspot_x, cursor->hotspot_y)})); + it = decltype(it)(cursors.insert(shape, {buffer->data().copy(), QPoint(cursor->hotspot_x, cursor->hotspot_y)})); } image->hotSpot = it.value().hotSpot; image->image = it.value().image; @@ -920,6 +996,10 @@ void CursorImage::reevaluteSource() setSource(CursorSource::LockScreen); return; } + if (input()->isSelectingWindow()) { + setSource(CursorSource::WindowSelector); + return; + } if (effects && static_cast(effects)->isMouseInterception()) { setSource(CursorSource::EffectsOverride); return; @@ -965,6 +1045,8 @@ QImage CursorImage::image() const return m_drag.cursor.image; case CursorSource::Fallback: return m_fallbackCursor.image; + case CursorSource::WindowSelector: + return m_windowSelectionCursor.image; default: Q_UNREACHABLE(); } @@ -987,6 +1069,8 @@ QPoint CursorImage::hotSpot() const return m_drag.cursor.hotSpot; case CursorSource::Fallback: return m_fallbackCursor.hotSpot; + case CursorSource::WindowSelector: + return m_windowSelectionCursor.hotSpot; default: Q_UNREACHABLE(); } diff --git a/pointer_input.h b/pointer_input.h index ce9b35e71c..1697304780 100644 --- a/pointer_input.h +++ b/pointer_input.h @@ -81,6 +81,8 @@ public: void markCursorAsRendered(); void setEffectsOverrideCursor(Qt::CursorShape shape); void removeEffectsOverrideCursor(); + void setWindowSelectionCursor(const QByteArray &shape); + void removeWindowSelectionCursor(); /** * @internal @@ -133,6 +135,7 @@ public: private: void updateOnStartMoveResize(); + void updateToReset(); void updatePosition(const QPointF &pos); void updateButton(uint32_t button, InputRedirection::PointerButtonState state); void warpXcbOnSurfaceLeft(KWayland::Server::SurfaceInterface *surface); @@ -155,6 +158,8 @@ public: void setEffectsOverrideCursor(Qt::CursorShape shape); void removeEffectsOverrideCursor(); + void setWindowSelectionCursor(const QByteArray &shape); + void removeWindowSelectionCursor(); QImage image() const; QPoint hotSpot() const; @@ -178,6 +183,9 @@ private: QPoint hotSpot; }; void loadThemeCursor(Qt::CursorShape shape, Image *image); + void loadThemeCursor(const QByteArray &shape, Image *image); + template + void loadThemeCursor(const T &shape, QHash &cursors, Image *image); enum class CursorSource { LockScreen, @@ -186,7 +194,8 @@ private: PointerSurface, Decoration, DragAndDrop, - Fallback + Fallback, + WindowSelector }; void setSource(CursorSource source); @@ -204,7 +213,9 @@ private: QMetaObject::Connection m_decorationConnection; Image m_fallbackCursor; Image m_moveResizeCursor; + Image m_windowSelectionCursor; QHash m_cursors; + QHash m_cursorsByName; QElapsedTimer m_surfaceRenderedTimer; struct { Image cursor; diff --git a/wayland_cursor_theme.cpp b/wayland_cursor_theme.cpp index 3743249452..682a5faa96 100644 --- a/wayland_cursor_theme.cpp +++ b/wayland_cursor_theme.cpp @@ -80,6 +80,11 @@ void WaylandCursorTheme::destroyTheme() } wl_cursor_image *WaylandCursorTheme::get(Qt::CursorShape shape) +{ + return get(Cursor::self()->cursorName(shape)); +} + +wl_cursor_image *WaylandCursorTheme::get(const QByteArray &name) { if (!m_theme) { loadTheme(); @@ -88,7 +93,6 @@ wl_cursor_image *WaylandCursorTheme::get(Qt::CursorShape shape) // loading cursor failed return nullptr; } - const QByteArray name = Cursor::self()->cursorName(shape); wl_cursor *c = wl_cursor_theme_get_cursor(m_theme, name.constData()); if (!c || c->image_count <= 0) { const auto &names = Cursor::self()->cursorAlternativeNames(name); diff --git a/wayland_cursor_theme.h b/wayland_cursor_theme.h index cbdbe69095..2b7dccfc13 100644 --- a/wayland_cursor_theme.h +++ b/wayland_cursor_theme.h @@ -46,6 +46,7 @@ public: virtual ~WaylandCursorTheme(); wl_cursor_image *get(Qt::CursorShape shape); + wl_cursor_image *get(const QByteArray &name); Q_SIGNALS: void themeChanged();