From 5313b856468015c732f28dfb275e98fe07345b3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Fl=C3=B6ser?= Date: Sat, 11 Nov 2017 12:28:10 +0100 Subject: [PATCH] Support modifier+mouse button on window decoration Summary: On X11 modifier+mouse button on the window decoration triggers the "special" handling thus as unrestricted move instead of passing the click to the decoration. Of course on Wayland we want to have the same functionality. BUG: 386708 FIXED-IN: 5.11.4 Test Plan: New test case added. PointerInputTest still passes. Reviewers: #kwin, #plasma, broulik Subscribers: plasma-devel, kwin Tags: #kwin Differential Revision: https://phabricator.kde.org/D8758 --- .../integration/decoration_input_test.cpp | 174 ++++++++++++++++++ input.cpp | 107 +++++++---- 2 files changed, 248 insertions(+), 33 deletions(-) diff --git a/autotests/integration/decoration_input_test.cpp b/autotests/integration/decoration_input_test.cpp index 83020d3a36..8a94fa99ae 100644 --- a/autotests/integration/decoration_input_test.cpp +++ b/autotests/integration/decoration_input_test.cpp @@ -72,6 +72,10 @@ private Q_SLOTS: void testTapToMove(); void testResizeOutsideWindow_data(); void testResizeOutsideWindow(); + void testModifierClickUnrestrictedMove_data(); + void testModifierClickUnrestrictedMove(); + void testModifierScrollOpacity_data(); + void testModifierScrollOpacity(); private: AbstractClient *showWindow(Test::ShellSurfaceType type); @@ -563,6 +567,176 @@ void DecorationInputTest::testResizeOutsideWindow() QVERIFY(!c->isResize()); } +void DecorationInputTest::testModifierClickUnrestrictedMove_data() +{ + QTest::addColumn("modifierKey"); + QTest::addColumn("mouseButton"); + QTest::addColumn("modKey"); + QTest::addColumn("capsLock"); + QTest::addColumn("surfaceType"); + + const QString alt = QStringLiteral("Alt"); + const QString meta = QStringLiteral("Meta"); + + const QVector> surfaceTypes{ + {Test::ShellSurfaceType::WlShell, QByteArrayLiteral("WlShell")}, + {Test::ShellSurfaceType::XdgShellV5, QByteArrayLiteral("XdgShellV5")}, + {Test::ShellSurfaceType::XdgShellV6, QByteArrayLiteral("XdgShellV6")}, + }; + + for (const auto &type: surfaceTypes) { + QTest::newRow("Left Alt + Left Click" + type.second) << KEY_LEFTALT << BTN_LEFT << alt << false << type.first; + QTest::newRow("Left Alt + Right Click" + type.second) << KEY_LEFTALT << BTN_RIGHT << alt << false << type.first; + QTest::newRow("Left Alt + Middle Click" + type.second) << KEY_LEFTALT << BTN_MIDDLE << alt << false << type.first; + QTest::newRow("Right Alt + Left Click" + type.second) << KEY_RIGHTALT << BTN_LEFT << alt << false << type.first; + QTest::newRow("Right Alt + Right Click" + type.second) << KEY_RIGHTALT << BTN_RIGHT << alt << false << type.first; + QTest::newRow("Right Alt + Middle Click" + type.second) << KEY_RIGHTALT << BTN_MIDDLE << alt << false << type.first; + // now everything with meta + QTest::newRow("Left Meta + Left Click" + type.second) << KEY_LEFTMETA << BTN_LEFT << meta << false << type.first; + QTest::newRow("Left Meta + Right Click" + type.second) << KEY_LEFTMETA << BTN_RIGHT << meta << false << type.first; + QTest::newRow("Left Meta + Middle Click" + type.second) << KEY_LEFTMETA << BTN_MIDDLE << meta << false << type.first; + QTest::newRow("Right Meta + Left Click" + type.second) << KEY_RIGHTMETA << BTN_LEFT << meta << false << type.first; + QTest::newRow("Right Meta + Right Click" + type.second) << KEY_RIGHTMETA << BTN_RIGHT << meta << false << type.first; + QTest::newRow("Right Meta + Middle Click" + type.second) << KEY_RIGHTMETA << BTN_MIDDLE << meta << false << type.first; + + // and with capslock + QTest::newRow("Left Alt + Left Click/CapsLock" + type.second) << KEY_LEFTALT << BTN_LEFT << alt << true << type.first; + QTest::newRow("Left Alt + Right Click/CapsLock" + type.second) << KEY_LEFTALT << BTN_RIGHT << alt << true << type.first; + QTest::newRow("Left Alt + Middle Click/CapsLock" + type.second) << KEY_LEFTALT << BTN_MIDDLE << alt << true << type.first; + QTest::newRow("Right Alt + Left Click/CapsLock" + type.second) << KEY_RIGHTALT << BTN_LEFT << alt << true << type.first; + QTest::newRow("Right Alt + Right Click/CapsLock" + type.second) << KEY_RIGHTALT << BTN_RIGHT << alt << true << type.first; + QTest::newRow("Right Alt + Middle Click/CapsLock" + type.second) << KEY_RIGHTALT << BTN_MIDDLE << alt << true << type.first; + // now everything with meta + QTest::newRow("Left Meta + Left Click/CapsLock" + type.second) << KEY_LEFTMETA << BTN_LEFT << meta << true << type.first; + QTest::newRow("Left Meta + Right Click/CapsLock" + type.second) << KEY_LEFTMETA << BTN_RIGHT << meta << true << type.first; + QTest::newRow("Left Meta + Middle Click/CapsLock" + type.second) << KEY_LEFTMETA << BTN_MIDDLE << meta << true << type.first; + QTest::newRow("Right Meta + Left Click/CapsLock" + type.second) << KEY_RIGHTMETA << BTN_LEFT << meta << true << type.first; + QTest::newRow("Right Meta + Right Click/CapsLock" + type.second) << KEY_RIGHTMETA << BTN_RIGHT << meta << true << type.first; + QTest::newRow("Right Meta + Middle Click/CapsLock" + type.second) << KEY_RIGHTMETA << BTN_MIDDLE << meta << true << type.first; + } +} + +void DecorationInputTest::testModifierClickUnrestrictedMove() +{ + // this test ensures that Alt+mouse button press triggers unrestricted move + + // first modify the config for this run + QFETCH(QString, modKey); + KConfigGroup group = kwinApp()->config()->group("MouseBindings"); + group.writeEntry("CommandAllKey", modKey); + group.writeEntry("CommandAll1", "Move"); + group.writeEntry("CommandAll2", "Move"); + group.writeEntry("CommandAll3", "Move"); + group.sync(); + workspace()->slotReconfigure(); + QCOMPARE(options->commandAllModifier(), modKey == QStringLiteral("Alt") ? Qt::AltModifier : Qt::MetaModifier); + QCOMPARE(options->commandAll1(), Options::MouseUnrestrictedMove); + QCOMPARE(options->commandAll2(), Options::MouseUnrestrictedMove); + QCOMPARE(options->commandAll3(), Options::MouseUnrestrictedMove); + + // create a window + QFETCH(Test::ShellSurfaceType, surfaceType); + AbstractClient *c = showWindow(surfaceType); + QVERIFY(c); + QVERIFY(c->isDecorated()); + QVERIFY(!c->noBorder()); + c->move(screens()->geometry(0).center() - QPoint(c->width()/2, c->height()/2)); + // move cursor on window + Cursor::setPos(QPoint(c->geometry().center().x(), c->y() + c->clientPos().y() / 2)); + + // simulate modifier+click + quint32 timestamp = 1; + QFETCH(bool, capsLock); + if (capsLock) { + kwinApp()->platform()->keyboardKeyPressed(KEY_CAPSLOCK, timestamp++); + } + QFETCH(int, modifierKey); + QFETCH(int, mouseButton); + kwinApp()->platform()->keyboardKeyPressed(modifierKey, timestamp++); + QVERIFY(!c->isMove()); + kwinApp()->platform()->pointerButtonPressed(mouseButton, timestamp++); + QVERIFY(c->isMove()); + // release modifier should not change it + kwinApp()->platform()->keyboardKeyReleased(modifierKey, timestamp++); + QVERIFY(c->isMove()); + // but releasing the key should end move/resize + kwinApp()->platform()->pointerButtonReleased(mouseButton, timestamp++); + QVERIFY(!c->isMove()); + if (capsLock) { + kwinApp()->platform()->keyboardKeyReleased(KEY_CAPSLOCK, timestamp++); + } +} + +void DecorationInputTest::testModifierScrollOpacity_data() +{ + QTest::addColumn("modifierKey"); + QTest::addColumn("modKey"); + QTest::addColumn("capsLock"); + QTest::addColumn("surfaceType"); + + const QString alt = QStringLiteral("Alt"); + const QString meta = QStringLiteral("Meta"); + + const QVector> surfaceTypes{ + {Test::ShellSurfaceType::WlShell, QByteArrayLiteral("WlShell")}, + {Test::ShellSurfaceType::XdgShellV5, QByteArrayLiteral("XdgShellV5")}, + {Test::ShellSurfaceType::XdgShellV6, QByteArrayLiteral("XdgShellV6")}, + }; + + for (const auto &type: surfaceTypes) { + QTest::newRow("Left Alt" + type.second) << KEY_LEFTALT << alt << false << type.first; + QTest::newRow("Right Alt" + type.second) << KEY_RIGHTALT << alt << false << type.first; + QTest::newRow("Left Meta" + type.second) << KEY_LEFTMETA << meta << false << type.first; + QTest::newRow("Right Meta" + type.second) << KEY_RIGHTMETA << meta << false << type.first; + QTest::newRow("Left Alt/CapsLock" + type.second) << KEY_LEFTALT << alt << true << type.first; + QTest::newRow("Right Alt/CapsLock" + type.second) << KEY_RIGHTALT << alt << true << type.first; + QTest::newRow("Left Meta/CapsLock" + type.second) << KEY_LEFTMETA << meta << true << type.first; + QTest::newRow("Right Meta/CapsLock" + type.second) << KEY_RIGHTMETA << meta << true << type.first; + } +} + +void DecorationInputTest::testModifierScrollOpacity() +{ + // this test verifies that mod+wheel performs a window operation + + // first modify the config for this run + QFETCH(QString, modKey); + KConfigGroup group = kwinApp()->config()->group("MouseBindings"); + group.writeEntry("CommandAllKey", modKey); + group.writeEntry("CommandAllWheel", "change opacity"); + group.sync(); + workspace()->slotReconfigure(); + + QFETCH(Test::ShellSurfaceType, surfaceType); + AbstractClient *c = showWindow(surfaceType); + QVERIFY(c); + QVERIFY(c->isDecorated()); + QVERIFY(!c->noBorder()); + c->move(screens()->geometry(0).center() - QPoint(c->width()/2, c->height()/2)); + // move cursor on window + Cursor::setPos(QPoint(c->geometry().center().x(), c->y() + c->clientPos().y() / 2)); + // set the opacity to 0.5 + c->setOpacity(0.5); + QCOMPARE(c->opacity(), 0.5); + + // simulate modifier+wheel + quint32 timestamp = 1; + QFETCH(bool, capsLock); + if (capsLock) { + kwinApp()->platform()->keyboardKeyPressed(KEY_CAPSLOCK, timestamp++); + } + QFETCH(int, modifierKey); + kwinApp()->platform()->keyboardKeyPressed(modifierKey, timestamp++); + kwinApp()->platform()->pointerAxisVertical(-5, timestamp++); + QCOMPARE(c->opacity(), 0.6); + kwinApp()->platform()->pointerAxisVertical(5, timestamp++); + QCOMPARE(c->opacity(), 0.5); + kwinApp()->platform()->keyboardKeyReleased(modifierKey, timestamp++); + if (capsLock) { + kwinApp()->platform()->keyboardKeyReleased(KEY_CAPSLOCK, timestamp++); + } +} + } WAYLANDTEST_MAIN(KWin::DecorationInputTest) diff --git a/input.cpp b/input.cpp index 2199acd6a5..945d8d9d7e 100644 --- a/input.cpp +++ b/input.cpp @@ -920,6 +920,63 @@ private: QPointF m_lastLocalTouchPos; }; +namespace { + +enum class MouseAction { + ModifierOnly, + ModifierAndWindow +}; +std::pair performClientMouseAction(QMouseEvent *event, AbstractClient *client, MouseAction action = MouseAction::ModifierOnly) +{ + Options::MouseCommand command = Options::MouseNothing; + bool wasAction = false; + if (static_cast(event)->modifiersRelevantForGlobalShortcuts() == options->commandAllModifier()) { + wasAction = true; + switch (event->button()) { + case Qt::LeftButton: + command = options->commandAll1(); + break; + case Qt::MiddleButton: + command = options->commandAll2(); + break; + case Qt::RightButton: + command = options->commandAll3(); + break; + default: + // nothing + break; + } + } else { + if (action == MouseAction::ModifierAndWindow) { + command = client->getMouseCommand(event->button(), &wasAction); + } + } + if (wasAction) { + return std::make_pair(wasAction, !client->performMouseCommand(command, event->globalPos())); + } + return std::make_pair(wasAction, false); +} + +std::pair performClientWheelAction(QWheelEvent *event, AbstractClient *c, MouseAction action = MouseAction::ModifierOnly) +{ + bool wasAction = false; + Options::MouseCommand command = Options::MouseNothing; + if (static_cast(event)->modifiersRelevantForGlobalShortcuts() == options->commandAllModifier()) { + wasAction = true; + command = options->operationWindowMouseWheel(-1 * event->angleDelta().y()); + } else { + if (action == MouseAction::ModifierAndWindow) { + command = c->getWheelCommand(Qt::Vertical, &wasAction); + } + } + if (wasAction) { + return std::make_pair(wasAction, !c->performMouseCommand(command, event->globalPos())); + } + return std::make_pair(wasAction, false); +} + +} + class DecorationEventFilter : public InputEventFilter { public: bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override { @@ -941,6 +998,10 @@ public: } case QEvent::MouseButtonPress: case QEvent::MouseButtonRelease: { + const auto actionResult = performClientMouseAction(event, decoration->client()); + if (actionResult.first) { + return actionResult.second; + } QMouseEvent e(event->type(), p, event->globalPos(), event->button(), event->buttons(), event->modifiers()); e.setAccepted(false); QCoreApplication::sendEvent(decoration->decoration(), &e); @@ -962,6 +1023,13 @@ public: if (!decoration) { return false; } + if (event->angleDelta().y() != 0) { + // client window action only on vertical scrolling + const auto actionResult = performClientWheelAction(event, decoration->client()); + if (actionResult.first) { + return actionResult.second; + } + } const QPointF localPos = event->globalPosF() - decoration->client()->pos(); const Qt::Orientation orientation = (event->angleDelta().x() != 0) ? Qt::Horizontal : Qt::Vertical; const int delta = event->angleDelta().x() != 0 ? event->angleDelta().x() : event->angleDelta().y(); @@ -1161,29 +1229,9 @@ public: if (!c) { return false; } - bool wasAction = false; - Options::MouseCommand command = Options::MouseNothing; - if (static_cast(event)->modifiersRelevantForGlobalShortcuts() == options->commandAllModifier()) { - wasAction = true; - switch (event->button()) { - case Qt::LeftButton: - command = options->commandAll1(); - break; - case Qt::MiddleButton: - command = options->commandAll2(); - break; - case Qt::RightButton: - command = options->commandAll3(); - break; - default: - // nothing - break; - } - } else { - command = c->getMouseCommand(event->button(), &wasAction); - } - if (wasAction) { - return !c->performMouseCommand(command, event->globalPos()); + const auto actionResult = performClientMouseAction(event, c, MouseAction::ModifierAndWindow); + if (actionResult.first) { + return actionResult.second; } return false; } @@ -1196,16 +1244,9 @@ public: if (!c) { return false; } - bool wasAction = false; - Options::MouseCommand command = Options::MouseNothing; - if (static_cast(event)->modifiersRelevantForGlobalShortcuts() == options->commandAllModifier()) { - wasAction = true; - command = options->operationWindowMouseWheel(-1 * event->angleDelta().y()); - } else { - command = c->getWheelCommand(Qt::Vertical, &wasAction); - } - if (wasAction) { - return !c->performMouseCommand(command, event->globalPos()); + const auto actionResult = performClientWheelAction(event, c, MouseAction::ModifierAndWindow); + if (actionResult.first) { + return actionResult.second; } return false; }