ButtonRebindsFilter: Support keyboard modifiers with mouse buttons

This is needed to better support emitting mouse button events when
pressing tablet buttons. It's common in many art programs that an action
is tied to a mouse button + a modifier, such as panning the canvas. So
being able to send keyboard modifiers in tandem with mouse buttons is
very useful when rebinding.

Tests are added for this new feature.

CCBUG: 469232
This commit is contained in:
Joshua Goins 2024-07-12 13:18:24 -04:00 committed by Joshua Goins
parent d01e20b6a9
commit 34c2a36000
3 changed files with 103 additions and 2 deletions

View file

@ -37,6 +37,9 @@ private Q_SLOTS:
void testMouse_data();
void testMouse();
void testMouseKeyboardMod_data();
void testMouseKeyboardMod();
void testDisabled();
// NOTE: Mouse buttons are not tested because those are used in the other tests
@ -157,6 +160,67 @@ void TestButtonRebind::testMouse()
Test::pointerButtonReleased(0x119, timestamp++);
}
void TestButtonRebind::testMouseKeyboardMod_data()
{
QTest::addColumn<Qt::KeyboardModifiers>("modifiers");
QTest::addColumn<QList<quint32>>("expectedKeys");
QTest::newRow("single ctrl") << Qt::KeyboardModifiers(Qt::ControlModifier) << QList<quint32>{KEY_LEFTCTRL};
QTest::newRow("single alt") << Qt::KeyboardModifiers(Qt::AltModifier) << QList<quint32>{KEY_LEFTALT};
QTest::newRow("single shift") << Qt::KeyboardModifiers(Qt::ShiftModifier) << QList<quint32>{KEY_LEFTSHIFT};
// We have to test Meta with another key, because it will most likely trigger KWin to do some window operation.
QTest::newRow("meta + alt") << Qt::KeyboardModifiers(Qt::MetaModifier | Qt::AltModifier) << QList<quint32>{KEY_LEFTALT, KEY_LEFTMETA};
QTest::newRow("ctrl + alt + shift + meta") << Qt::KeyboardModifiers(Qt::ControlModifier | Qt::AltModifier | Qt::ShiftModifier | Qt::MetaModifier) << QList<quint32>{KEY_LEFTSHIFT, KEY_LEFTCTRL, KEY_LEFTALT, KEY_LEFTMETA};
}
void TestButtonRebind::testMouseKeyboardMod()
{
QFETCH(Qt::KeyboardModifiers, modifiers);
KConfigGroup buttonGroup = KSharedConfig::openConfig(QStringLiteral("kcminputrc"))->group(QStringLiteral("ButtonRebinds")).group(QStringLiteral("TabletTool")).group(QStringLiteral("Virtual Tablet Tool 1"));
buttonGroup.writeEntry(QString::number(BTN_STYLUS), QStringList{"MouseButton", QString::number(BTN_LEFT), QString::number(modifiers.toInt())}, KConfig::Notify);
buttonGroup.sync();
std::unique_ptr<KWayland::Client::Keyboard> keyboard(Test::waylandSeat()->createKeyboard());
QSignalSpy keyboardEnteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered);
QSignalSpy keyboardKeyChangedSpy(keyboard.get(), &KWayland::Client::Keyboard::keyChanged);
std::unique_ptr<KWayland::Client::Surface> surface = Test::createSurface();
std::unique_ptr<Test::XdgToplevel> shellSurface = Test::createXdgToplevelSurface(surface.get());
auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
QVERIFY(keyboardEnteredSpy.wait());
std::unique_ptr<KWayland::Client::Pointer> pointer(Test::waylandSeat()->createPointer());
QSignalSpy pointerEnteredSpy(pointer.get(), &KWayland::Client::Pointer::entered);
QSignalSpy pointerButtonChangedSpy(pointer.get(), &KWayland::Client::Pointer::buttonStateChanged);
const QRectF startGeometry = window->frameGeometry();
input()->pointer()->warp(startGeometry.center());
QVERIFY(pointerEnteredSpy.wait());
// Send the tablet button event so it can be processed by the filter
Test::tabletToolButtonPressed(BTN_STYLUS, timestamp++);
// The keyboard modifier is sent first
QVERIFY(keyboardKeyChangedSpy.wait());
QFETCH(QList<quint32>, expectedKeys);
QCOMPARE(keyboardKeyChangedSpy.count(), expectedKeys.count());
for (int i = 0; i < keyboardKeyChangedSpy.count(); i++) {
QCOMPARE(keyboardKeyChangedSpy.at(i).at(0).value<quint32>(), expectedKeys.at(i));
QCOMPARE(keyboardKeyChangedSpy.at(i).at(1).value<KWayland::Client::Keyboard::KeyState>(), KWayland::Client::Keyboard::KeyState::Pressed);
}
// Then the mouse button is
QCOMPARE(pointerButtonChangedSpy.count(), 1);
QCOMPARE(pointerButtonChangedSpy.at(0).at(2).value<qint32>(), BTN_LEFT);
Test::tabletToolButtonReleased(BTN_STYLUS, timestamp++);
}
void TestButtonRebind::testDisabled()
{
KConfigGroup buttonGroup = KSharedConfig::openConfig(QStringLiteral("kcminputrc"))->group(QStringLiteral("ButtonRebinds")).group(QStringLiteral("Mouse"));

View file

@ -248,13 +248,20 @@ void ButtonRebindsFilter::insert(TriggerType type, const Trigger &trigger, const
m_actions.at(type).insert(trigger, keys);
}
} else if (entry.first() == QLatin1String("MouseButton")) {
if (entry.size() != 2) {
if (entry.size() < 2) {
qCWarning(KWIN_BUTTONREBINDS) << "Invalid mouse button" << entry;
return;
}
bool ok = false;
const MouseButton mb{entry.last().toUInt(&ok)};
MouseButton mb{entry[1].toUInt(&ok), {}};
// Last bit is the keyboard mods
if (entry.size() == 3) {
const auto keyboardModsRaw = entry.last().toInt(&ok);
mb.modifiers = Qt::KeyboardModifiers{keyboardModsRaw};
}
if (ok) {
m_actions.at(type).insert(trigger, mb);
} else {
@ -294,6 +301,7 @@ bool ButtonRebindsFilter::send(TriggerType type, const Trigger &trigger, bool pr
if (pressed && type != Pointer) {
sendMousePosition(m_tabletCursorPos, timestamp);
}
sendKeyModifiers(mb->modifiers, pressed, timestamp);
return sendMouseButton(mb->button, pressed, timestamp);
}
if (const auto tb = std::get_if<TabletToolButton>(&action)) {
@ -384,6 +392,33 @@ bool ButtonRebindsFilter::sendKeySequence(const QKeySequence &keys, bool pressed
return true;
}
bool ButtonRebindsFilter::sendKeyModifiers(const Qt::KeyboardModifiers &modifiers, bool pressed, std::chrono::microseconds time)
{
if (modifiers == Qt::NoModifier) {
return false;
}
auto sendKey = [this, pressed, time](xkb_keycode_t key) {
auto state = pressed ? KWin::InputRedirection::KeyboardKeyPressed : KWin::InputRedirection::KeyboardKeyReleased;
Q_EMIT m_inputDevice.keyChanged(key, state, time, &m_inputDevice);
};
if (modifiers.testFlag(Qt::ShiftModifier)) {
sendKey(KEY_LEFTSHIFT);
}
if (modifiers.testFlag(Qt::ControlModifier)) {
sendKey(KEY_LEFTCTRL);
}
if (modifiers.testFlag(Qt::AltModifier)) {
sendKey(KEY_LEFTALT);
}
if (modifiers.testFlag(Qt::MetaModifier)) {
sendKey(KEY_LEFTMETA);
}
return true;
}
bool ButtonRebindsFilter::sendMouseButton(quint32 button, bool pressed, std::chrono::microseconds time)
{
RebindScope scope;

View file

@ -67,6 +67,7 @@ public:
struct MouseButton
{
quint32 button;
Qt::KeyboardModifiers modifiers;
};
struct DisabledButton
{
@ -83,6 +84,7 @@ private:
void insert(TriggerType type, const Trigger &trigger, const QStringList &action);
bool send(TriggerType type, const Trigger &trigger, bool pressed, std::chrono::microseconds timestamp);
bool sendKeySequence(const QKeySequence &sequence, bool pressed, std::chrono::microseconds time);
bool sendKeyModifiers(const Qt::KeyboardModifiers &modifiers, bool pressed, std::chrono::microseconds time);
bool sendMouseButton(quint32 button, bool pressed, std::chrono::microseconds time);
bool sendMousePosition(QPointF position, std::chrono::microseconds time);
bool sendTabletToolButton(quint32 button, bool pressed, std::chrono::microseconds time);