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
This commit is contained in:
Martin Flöser 2017-11-11 12:28:10 +01:00
parent 57ea28a216
commit 5313b85646
2 changed files with 248 additions and 33 deletions

View file

@ -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<int>("modifierKey");
QTest::addColumn<int>("mouseButton");
QTest::addColumn<QString>("modKey");
QTest::addColumn<bool>("capsLock");
QTest::addColumn<Test::ShellSurfaceType>("surfaceType");
const QString alt = QStringLiteral("Alt");
const QString meta = QStringLiteral("Meta");
const QVector<std::pair<Test::ShellSurfaceType, QByteArray>> 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<int>("modifierKey");
QTest::addColumn<QString>("modKey");
QTest::addColumn<bool>("capsLock");
QTest::addColumn<Test::ShellSurfaceType>("surfaceType");
const QString alt = QStringLiteral("Alt");
const QString meta = QStringLiteral("Meta");
const QVector<std::pair<Test::ShellSurfaceType, QByteArray>> 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)

107
input.cpp
View file

@ -920,6 +920,63 @@ private:
QPointF m_lastLocalTouchPos;
};
namespace {
enum class MouseAction {
ModifierOnly,
ModifierAndWindow
};
std::pair<bool, bool> performClientMouseAction(QMouseEvent *event, AbstractClient *client, MouseAction action = MouseAction::ModifierOnly)
{
Options::MouseCommand command = Options::MouseNothing;
bool wasAction = false;
if (static_cast<MouseEvent*>(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<bool, bool> performClientWheelAction(QWheelEvent *event, AbstractClient *c, MouseAction action = MouseAction::ModifierOnly)
{
bool wasAction = false;
Options::MouseCommand command = Options::MouseNothing;
if (static_cast<WheelEvent*>(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<MouseEvent*>(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<WheelEvent*>(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;
}