diff --git a/autotests/integration/CMakeLists.txt b/autotests/integration/CMakeLists.txt --- a/autotests/integration/CMakeLists.txt +++ b/autotests/integration/CMakeLists.txt @@ -41,6 +41,7 @@ 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 --- /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.h b/input.h --- a/input.h +++ b/input.h @@ -28,6 +28,8 @@ #include +#include + class KGlobalAccelInterface; class QKeySequence; class QMouseEvent; @@ -42,6 +44,7 @@ class KeyboardInputRedirection; class PointerInputRedirection; class TouchInputRedirection; +class WindowSelectorFilter; namespace Decoration { @@ -166,6 +169,9 @@ 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 @@ LibInput::Connection *m_libInput = nullptr; + WindowSelectorFilter *m_windowSelector = nullptr; + QVector m_filters; KSharedConfigPtr m_inputConfig; diff --git a/input.cpp b/input.cpp --- a/input.cpp +++ b/input.cpp @@ -475,6 +475,105 @@ } }; +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 @@ 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 @@ 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/keyboard_input.h b/keyboard_input.h --- a/keyboard_input.h +++ b/keyboard_input.h @@ -129,7 +129,7 @@ LEDs m_leds; }; -class KeyboardInputRedirection : public QObject +class KWIN_EXPORT KeyboardInputRedirection : public QObject { Q_OBJECT public: diff --git a/keyboard_input.cpp b/keyboard_input.cpp --- a/keyboard_input.cpp +++ b/keyboard_input.cpp @@ -613,7 +613,7 @@ break; } while (it != stacking.begin()); } - } else { + } else if (!input()->isSelectingWindow()) { found = workspace()->activeClient(); } if (found && found->surface()) { diff --git a/platform.h b/platform.h --- a/platform.h +++ b/platform.h @@ -172,8 +172,7 @@ * @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/platform.cpp b/platform.cpp --- a/platform.cpp +++ b/platform.cpp @@ -365,9 +365,11 @@ 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/pointer_input.h b/pointer_input.h --- a/pointer_input.h +++ b/pointer_input.h @@ -81,6 +81,8 @@ void markCursorAsRendered(); void setEffectsOverrideCursor(Qt::CursorShape shape); void removeEffectsOverrideCursor(); + void setWindowSelectionCursor(const QByteArray &shape); + void removeWindowSelectionCursor(); /** * @internal @@ -133,6 +135,7 @@ 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 @@ void setEffectsOverrideCursor(Qt::CursorShape shape); void removeEffectsOverrideCursor(); + void setWindowSelectionCursor(const QByteArray &shape); + void removeWindowSelectionCursor(); QImage image() const; QPoint hotSpot() const; @@ -178,15 +183,19 @@ 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, EffectsOverride, MoveResize, PointerSurface, Decoration, DragAndDrop, - Fallback + Fallback, + WindowSelector }; void setSource(CursorSource source); @@ -204,7 +213,9 @@ 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/pointer_input.cpp b/pointer_input.cpp --- a/pointer_input.cpp +++ b/pointer_input.cpp @@ -181,6 +181,31 @@ 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 @@ // 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 @@ 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 @@ 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; @@ -881,12 +946,23 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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/wayland_cursor_theme.h b/wayland_cursor_theme.h --- a/wayland_cursor_theme.h +++ b/wayland_cursor_theme.h @@ -46,6 +46,7 @@ virtual ~WaylandCursorTheme(); wl_cursor_image *get(Qt::CursorShape shape); + wl_cursor_image *get(const QByteArray &name); Q_SIGNALS: void themeChanged(); diff --git a/wayland_cursor_theme.cpp b/wayland_cursor_theme.cpp --- a/wayland_cursor_theme.cpp +++ b/wayland_cursor_theme.cpp @@ -81,14 +81,18 @@ 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(); } if (!m_theme) { // 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);