Index: CMakeLists.txt =================================================================== --- CMakeLists.txt +++ CMakeLists.txt @@ -436,6 +436,7 @@ appmenu.cpp modifier_only_shortcuts.cpp xkb.cpp + gestures.cpp ) if(KWIN_BUILD_TABBOX) Index: autotests/CMakeLists.txt =================================================================== --- autotests/CMakeLists.txt +++ autotests/CMakeLists.txt @@ -333,3 +333,19 @@ add_test(kwin-testOnScreenNotification testOnScreenNotification) ecm_mark_as_test(testOnScreenNotification) + +######################################################## +# Test Gestures +######################################################## +set( testGestures_SRCS + test_gestures.cpp + ../gestures.cpp +) +add_executable( testGestures ${testGestures_SRCS}) + +target_link_libraries(testGestures + Qt5::Test +) + +add_test(kwin-testGestures testGestures) +ecm_mark_as_test(testGestures) Index: autotests/mock_effectshandler.h =================================================================== --- autotests/mock_effectshandler.h +++ autotests/mock_effectshandler.h @@ -177,6 +177,7 @@ void registerAxisShortcut(Qt::KeyboardModifiers, KWin::PointerAxisDirection, QAction *) override {} void registerGlobalShortcut(const QKeySequence &, QAction *) override {} void registerPointerShortcut(Qt::KeyboardModifiers, Qt::MouseButton, QAction *) override {} + void registerTouchpadSwipeShortcut(KWin::SwipeDirection, QAction *) override {} void reloadEffect(KWin::Effect *) override {} void removeSupportProperty(const QByteArray &, KWin::Effect *) override {} void reserveElectricBorder(KWin::ElectricBorder, KWin::Effect *) override {} Index: autotests/test_gestures.cpp =================================================================== --- /dev/null +++ autotests/test_gestures.cpp @@ -0,0 +1,382 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 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 "../gestures.h" + +#include +#include + +using namespace KWin; + +class GestureTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testSwipeMinFinger_data(); + void testSwipeMinFinger(); + void testSwipeMaxFinger_data(); + void testSwipeMaxFinger(); + void testDirection_data(); + void testDirection(); + void testUnregisterSwipeCancels(); + void testDeleteSwipeCancels(); + void testSwipeCancel_data(); + void testSwipeCancel(); + void testSwipeUpdateCancel(); + void testSwipeUpdateTrigger_data(); + void testSwipeUpdateTrigger(); + void testSwipeMinFingerStart_data(); + void testSwipeMinFingerStart(); + void testSwipeMaxFingerStart_data(); + void testSwipeMaxFingerStart(); + void testSwipeDiagonalCancels_data(); + void testSwipeDiagonalCancels(); +}; + +void GestureTest::testSwipeMinFinger_data() +{ + QTest::addColumn("count"); + QTest::addColumn("expectedCount"); + + QTest::newRow("0") << 0u << 0u; + QTest::newRow("1") << 1u << 1u; + QTest::newRow("10") << 10u << 10u; +} + +void GestureTest::testSwipeMinFinger() +{ + SwipeGesture gesture; + QCOMPARE(gesture.minimumFingerCountIsRelevant(), false); + QCOMPARE(gesture.minimumFingerCount(), 0u); + QFETCH(uint, count); + gesture.setMinimumFingerCount(count); + QCOMPARE(gesture.minimumFingerCountIsRelevant(), true); + QTEST(gesture.minimumFingerCount(), "expectedCount"); + gesture.setMinimumFingerCount(0); + QCOMPARE(gesture.minimumFingerCountIsRelevant(), true); + QCOMPARE(gesture.minimumFingerCount(), 0u); +} + +void GestureTest::testSwipeMaxFinger_data() +{ + QTest::addColumn("count"); + QTest::addColumn("expectedCount"); + + QTest::newRow("0") << 0u << 0u; + QTest::newRow("1") << 1u << 1u; + QTest::newRow("10") << 10u << 10u; +} + +void GestureTest::testSwipeMaxFinger() +{ + SwipeGesture gesture; + QCOMPARE(gesture.maximumFingerCountIsRelevant(), false); + QCOMPARE(gesture.maximumFingerCount(), 0u); + QFETCH(uint, count); + gesture.setMaximumFingerCount(count); + QCOMPARE(gesture.maximumFingerCountIsRelevant(), true); + QTEST(gesture.maximumFingerCount(), "expectedCount"); + gesture.setMaximumFingerCount(0); + QCOMPARE(gesture.maximumFingerCountIsRelevant(), true); + QCOMPARE(gesture.maximumFingerCount(), 0u); +} + +void GestureTest::testDirection_data() +{ + QTest::addColumn("direction"); + + QTest::newRow("Up") << KWin::SwipeGesture::Direction::Up; + QTest::newRow("Left") << KWin::SwipeGesture::Direction::Left; + QTest::newRow("Right") << KWin::SwipeGesture::Direction::Right; + QTest::newRow("Down") << KWin::SwipeGesture::Direction::Down; +} + +void GestureTest::testDirection() +{ + SwipeGesture gesture; + QCOMPARE(gesture.direction(), SwipeGesture::Direction::Down); + QFETCH(KWin::SwipeGesture::Direction, direction); + gesture.setDirection(direction); + QCOMPARE(gesture.direction(), direction); + // back to down + gesture.setDirection(SwipeGesture::Direction::Down); + QCOMPARE(gesture.direction(), SwipeGesture::Direction::Down); +} + +void GestureTest::testUnregisterSwipeCancels() +{ + GestureRecognizer recognizer; + QScopedPointer gesture(new SwipeGesture); + QSignalSpy startedSpy(gesture.data(), &SwipeGesture::started); + QVERIFY(startedSpy.isValid()); + QSignalSpy cancelledSpy(gesture.data(), &SwipeGesture::cancelled); + QVERIFY(cancelledSpy.isValid()); + + recognizer.registerGesture(gesture.data()); + recognizer.startSwipeGesture(1); + QCOMPARE(startedSpy.count(), 1); + QCOMPARE(cancelledSpy.count(), 0); + recognizer.unregisterGesture(gesture.data()); + QCOMPARE(cancelledSpy.count(), 1); + + // delete the gesture should not trigger cancel + gesture.reset(); + QCOMPARE(cancelledSpy.count(), 1); +} + +void GestureTest::testDeleteSwipeCancels() +{ + GestureRecognizer recognizer; + QScopedPointer gesture(new SwipeGesture); + QSignalSpy startedSpy(gesture.data(), &SwipeGesture::started); + QVERIFY(startedSpy.isValid()); + QSignalSpy cancelledSpy(gesture.data(), &SwipeGesture::cancelled); + QVERIFY(cancelledSpy.isValid()); + + recognizer.registerGesture(gesture.data()); + recognizer.startSwipeGesture(1); + QCOMPARE(startedSpy.count(), 1); + QCOMPARE(cancelledSpy.count(), 0); + gesture.reset(); + QCOMPARE(cancelledSpy.count(), 1); +} + +void GestureTest::testSwipeCancel_data() +{ + QTest::addColumn("direction"); + + QTest::newRow("Up") << KWin::SwipeGesture::Direction::Up; + QTest::newRow("Left") << KWin::SwipeGesture::Direction::Left; + QTest::newRow("Right") << KWin::SwipeGesture::Direction::Right; + QTest::newRow("Down") << KWin::SwipeGesture::Direction::Down; +} + +void GestureTest::testSwipeCancel() +{ + GestureRecognizer recognizer; + QScopedPointer gesture(new SwipeGesture); + QFETCH(SwipeGesture::Direction, direction); + gesture->setDirection(direction); + QSignalSpy startedSpy(gesture.data(), &SwipeGesture::started); + QVERIFY(startedSpy.isValid()); + QSignalSpy cancelledSpy(gesture.data(), &SwipeGesture::cancelled); + QVERIFY(cancelledSpy.isValid()); + QSignalSpy triggeredSpy(gesture.data(), &SwipeGesture::triggered); + QVERIFY(triggeredSpy.isValid()); + + recognizer.registerGesture(gesture.data()); + recognizer.startSwipeGesture(1); + QCOMPARE(startedSpy.count(), 1); + QCOMPARE(cancelledSpy.count(), 0); + recognizer.cancelSwipeGesture(); + QCOMPARE(cancelledSpy.count(), 1); + QCOMPARE(triggeredSpy.count(), 0); +} + +void GestureTest::testSwipeUpdateCancel() +{ + GestureRecognizer recognizer; + SwipeGesture upGesture; + upGesture.setDirection(SwipeGesture::Direction::Up); + SwipeGesture downGesture; + downGesture.setDirection(SwipeGesture::Direction::Down); + SwipeGesture rightGesture; + rightGesture.setDirection(SwipeGesture::Direction::Right); + SwipeGesture leftGesture; + leftGesture.setDirection(SwipeGesture::Direction::Left); + + QSignalSpy upCancelledSpy(&upGesture, &SwipeGesture::cancelled); + QVERIFY(upCancelledSpy.isValid()); + QSignalSpy downCancelledSpy(&downGesture, &SwipeGesture::cancelled); + QVERIFY(downCancelledSpy.isValid()); + QSignalSpy rightCancelledSpy(&rightGesture, &SwipeGesture::cancelled); + QVERIFY(rightCancelledSpy.isValid()); + QSignalSpy leftCancelledSpy(&leftGesture, &SwipeGesture::cancelled); + QVERIFY(leftCancelledSpy.isValid()); + + QSignalSpy upTriggeredSpy(&upGesture, &SwipeGesture::triggered); + QVERIFY(upTriggeredSpy.isValid()); + QSignalSpy downTriggeredSpy(&downGesture, &SwipeGesture::triggered); + QVERIFY(downTriggeredSpy.isValid()); + QSignalSpy rightTriggeredSpy(&rightGesture, &SwipeGesture::triggered); + QVERIFY(rightTriggeredSpy.isValid()); + QSignalSpy leftTriggeredSpy(&leftGesture, &SwipeGesture::triggered); + QVERIFY(leftTriggeredSpy.isValid()); + + recognizer.registerGesture(&upGesture); + recognizer.registerGesture(&downGesture); + recognizer.registerGesture(&rightGesture); + recognizer.registerGesture(&leftGesture); + + recognizer.startSwipeGesture(4); + + // first a down gesture + recognizer.updateSwipeGesture(QSizeF(1, 20)); + QCOMPARE(upCancelledSpy.count(), 1); + QCOMPARE(downCancelledSpy.count(), 0); + QCOMPARE(leftCancelledSpy.count(), 1); + QCOMPARE(rightCancelledSpy.count(), 1); + // another down gesture + recognizer.updateSwipeGesture(QSizeF(-2, 10)); + QCOMPARE(downCancelledSpy.count(), 0); + // and an up gesture + recognizer.updateSwipeGesture(QSizeF(-2, -10)); + QCOMPARE(upCancelledSpy.count(), 1); + QCOMPARE(downCancelledSpy.count(), 1); + QCOMPARE(leftCancelledSpy.count(), 1); + QCOMPARE(rightCancelledSpy.count(), 1); + + recognizer.endSwipeGesture(); + QCOMPARE(upCancelledSpy.count(), 1); + QCOMPARE(downCancelledSpy.count(), 1); + QCOMPARE(leftCancelledSpy.count(), 1); + QCOMPARE(rightCancelledSpy.count(), 1); + QCOMPARE(upTriggeredSpy.count(), 0); + QCOMPARE(downTriggeredSpy.count(), 0); + QCOMPARE(leftTriggeredSpy.count(), 0); + QCOMPARE(rightTriggeredSpy.count(), 0); +} + +void GestureTest::testSwipeUpdateTrigger_data() +{ + QTest::addColumn("direction"); + QTest::addColumn("delta"); + + QTest::newRow("Up") << KWin::SwipeGesture::Direction::Up << QSizeF(2, -3); + QTest::newRow("Left") << KWin::SwipeGesture::Direction::Left << QSizeF(-3, 1); + QTest::newRow("Right") << KWin::SwipeGesture::Direction::Right << QSizeF(20, -19); + QTest::newRow("Down") << KWin::SwipeGesture::Direction::Down << QSizeF(0, 50); +} + +void GestureTest::testSwipeUpdateTrigger() +{ + GestureRecognizer recognizer; + SwipeGesture gesture; + QFETCH(SwipeGesture::Direction, direction); + gesture.setDirection(direction); + + QSignalSpy triggeredSpy(&gesture, &SwipeGesture::triggered); + QVERIFY(triggeredSpy.isValid()); + QSignalSpy cancelledSpy(&gesture, &SwipeGesture::cancelled); + QVERIFY(cancelledSpy.isValid()); + + recognizer.registerGesture(&gesture); + + recognizer.startSwipeGesture(1); + QFETCH(QSizeF, delta); + recognizer.updateSwipeGesture(delta); + QCOMPARE(cancelledSpy.count(), 0); + QCOMPARE(triggeredSpy.count(), 0); + + recognizer.endSwipeGesture(); + QCOMPARE(cancelledSpy.count(), 0); + QCOMPARE(triggeredSpy.count(), 1); +} + +void GestureTest::testSwipeMinFingerStart_data() +{ + QTest::addColumn("min"); + QTest::addColumn("count"); + QTest::addColumn("started"); + + QTest::newRow("same") << 1u << 1u << true; + QTest::newRow("less") << 2u << 1u << false; + QTest::newRow("more") << 1u << 2u << true; +} + +void GestureTest::testSwipeMinFingerStart() +{ + GestureRecognizer recognizer; + SwipeGesture gesture; + QFETCH(uint, min); + gesture.setMinimumFingerCount(min); + + QSignalSpy startedSpy(&gesture, &SwipeGesture::started); + QVERIFY(startedSpy.isValid()); + + recognizer.registerGesture(&gesture); + QFETCH(uint, count); + recognizer.startSwipeGesture(count); + QTEST(!startedSpy.isEmpty(), "started"); +} + +void GestureTest::testSwipeMaxFingerStart_data() +{ + QTest::addColumn("max"); + QTest::addColumn("count"); + QTest::addColumn("started"); + + QTest::newRow("same") << 1u << 1u << true; + QTest::newRow("less") << 2u << 1u << true; + QTest::newRow("more") << 1u << 2u << false; +} + +void GestureTest::testSwipeMaxFingerStart() +{ + GestureRecognizer recognizer; + SwipeGesture gesture; + QFETCH(uint, max); + gesture.setMaximumFingerCount(max); + + QSignalSpy startedSpy(&gesture, &SwipeGesture::started); + QVERIFY(startedSpy.isValid()); + + recognizer.registerGesture(&gesture); + QFETCH(uint, count); + recognizer.startSwipeGesture(count); + QTEST(!startedSpy.isEmpty(), "started"); +} + +void GestureTest::testSwipeDiagonalCancels_data() +{ + QTest::addColumn("direction"); + + QTest::newRow("Up") << KWin::SwipeGesture::Direction::Up; + QTest::newRow("Left") << KWin::SwipeGesture::Direction::Left; + QTest::newRow("Right") << KWin::SwipeGesture::Direction::Right; + QTest::newRow("Down") << KWin::SwipeGesture::Direction::Down; +} + +void GestureTest::testSwipeDiagonalCancels() +{ + GestureRecognizer recognizer; + SwipeGesture gesture; + QFETCH(SwipeGesture::Direction, direction); + gesture.setDirection(direction); + + QSignalSpy triggeredSpy(&gesture, &SwipeGesture::triggered); + QVERIFY(triggeredSpy.isValid()); + QSignalSpy cancelledSpy(&gesture, &SwipeGesture::cancelled); + QVERIFY(cancelledSpy.isValid()); + + recognizer.registerGesture(&gesture); + + recognizer.startSwipeGesture(1); + recognizer.updateSwipeGesture(QSizeF(1, 1)); + QCOMPARE(cancelledSpy.count(), 1); + QCOMPARE(triggeredSpy.count(), 0); + + recognizer.endSwipeGesture(); + QCOMPARE(cancelledSpy.count(), 1); + QCOMPARE(triggeredSpy.count(), 0); + +} + +QTEST_MAIN(GestureTest) +#include "test_gestures.moc" Index: autotests/test_screen_edges.cpp =================================================================== --- autotests/test_screen_edges.cpp +++ autotests/test_screen_edges.cpp @@ -87,6 +87,10 @@ Q_UNUSED(action) } +void InputRedirection::registerTouchpadSwipeShortcut(SwipeDirection, QAction*) +{ +} + void updateXTime() { } Index: autotests/test_virtual_desktops.cpp =================================================================== --- autotests/test_virtual_desktops.cpp +++ autotests/test_virtual_desktops.cpp @@ -44,6 +44,10 @@ Q_UNUSED(action) } +void InputRedirection::registerTouchpadSwipeShortcut(SwipeDirection, QAction*) +{ +} + } Q_DECLARE_METATYPE(Qt::Orientation) Index: effects.h =================================================================== --- effects.h +++ effects.h @@ -125,6 +125,7 @@ void registerGlobalShortcut(const QKeySequence &shortcut, QAction *action) override; void registerPointerShortcut(Qt::KeyboardModifiers modifiers, Qt::MouseButton pointerButtons, QAction *action) override; void registerAxisShortcut(Qt::KeyboardModifiers modifiers, PointerAxisDirection axis, QAction *action) override; + void registerTouchpadSwipeShortcut(SwipeDirection direction, QAction *action) override; void* getProxy(QString name) override; void startMousePolling() override; void stopMousePolling() override; Index: effects.cpp =================================================================== --- effects.cpp +++ effects.cpp @@ -723,6 +723,11 @@ input()->registerAxisShortcut(modifiers, axis, action); } +void EffectsHandlerImpl::registerTouchpadSwipeShortcut(SwipeDirection direction, QAction *action) +{ + input()->registerTouchpadSwipeShortcut(direction, action); +} + void* EffectsHandlerImpl::getProxy(QString name) { for (QVector< EffectPair >::const_iterator it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) Index: effects/desktopgrid/desktopgrid.cpp =================================================================== --- effects/desktopgrid/desktopgrid.cpp +++ effects/desktopgrid/desktopgrid.cpp @@ -73,6 +73,7 @@ KGlobalAccel::self()->setShortcut(a, QList() << Qt::CTRL + Qt::Key_F8); shortcut = KGlobalAccel::self()->shortcut(a); effects->registerGlobalShortcut(Qt::CTRL + Qt::Key_F8, a); + effects->registerTouchpadSwipeShortcut(SwipeDirection::Up, a); connect(a, SIGNAL(triggered(bool)), this, SLOT(toggle())); connect(KGlobalAccel::self(), &KGlobalAccel::globalShortcutChanged, this, &DesktopGridEffect::globalShortcutChanged); connect(effects, SIGNAL(windowAdded(KWin::EffectWindow*)), this, SLOT(slotWindowAdded(KWin::EffectWindow*))); Index: effects/presentwindows/presentwindows.cpp =================================================================== --- effects/presentwindows/presentwindows.cpp +++ effects/presentwindows/presentwindows.cpp @@ -80,6 +80,7 @@ KGlobalAccel::self()->setShortcut(exposeAllAction, QList() << Qt::CTRL + Qt::Key_F10 << Qt::Key_LaunchC); shortcutAll = KGlobalAccel::self()->shortcut(exposeAllAction); effects->registerGlobalShortcut(Qt::CTRL + Qt::Key_F10, exposeAllAction); + effects->registerTouchpadSwipeShortcut(SwipeDirection::Down, exposeAllAction); connect(exposeAllAction, SIGNAL(triggered(bool)), this, SLOT(toggleActiveAllDesktops())); QAction* exposeClassAction = new QAction(this); exposeClassAction->setObjectName(QStringLiteral("ExposeClass")); Index: gestures.h =================================================================== --- /dev/null +++ gestures.h @@ -0,0 +1,133 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 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 . +*********************************************************************/ +#ifndef KWIN_GESTURES_H +#define KWIN_GESTURES_H + +#include +#include +#include + +namespace KWin +{ + +class Gesture : public QObject +{ + Q_OBJECT +public: + ~Gesture() override; +protected: + explicit Gesture(QObject *parent); + +Q_SIGNALS: + /** + * Matching of a gesture started and this Gesture might match. + * On further evaluation either the signal @link{triggered} or + * @link{cancelled} will get emitted. + **/ + void started(); + /** + * Gesture matching ended and this Gesture matched. + **/ + void triggered(); + /** + * This Gesture no longer matches. + **/ + void cancelled(); +}; + +class SwipeGesture : public Gesture +{ + Q_OBJECT +public: + enum class Direction { + Down, + Left, + Up, + Right + }; + + explicit SwipeGesture(QObject *parent = nullptr); + ~SwipeGesture() override; + + bool minimumFingerCountIsRelevant() const { + return m_minimumFingerCountRelevant; + } + void setMinimumFingerCount(uint count) { + m_minimumFingerCount = count; + m_minimumFingerCountRelevant = true; + } + uint minimumFingerCount() const { + return m_minimumFingerCount; + } + + bool maximumFingerCountIsRelevant() const { + return m_maximumFingerCountRelevant; + } + void setMaximumFingerCount(uint count) { + m_maximumFingerCount = count; + m_maximumFingerCountRelevant = true; + } + uint maximumFingerCount() const { + return m_maximumFingerCount; + } + + Direction direction() const { + return m_direction; + } + void setDirection(Direction direction) { + m_direction = direction; + } + +private: + bool m_minimumFingerCountRelevant = false; + uint m_minimumFingerCount = 0; + bool m_maximumFingerCountRelevant = false; + uint m_maximumFingerCount = 0; + Direction m_direction = Direction::Down; +}; + +class GestureRecognizer : public QObject +{ + Q_OBJECT +public: + GestureRecognizer(QObject *parent = nullptr); + ~GestureRecognizer() override; + + void registerGesture(Gesture *gesture); + void unregisterGesture(Gesture *gesture); + + void startSwipeGesture(uint fingerCount); + void updateSwipeGesture(const QSizeF &delta); + void cancelSwipeGesture(); + void endSwipeGesture(); + +private: + void cancelActiveSwipeGestures(); + QVector m_gestures; + QVector m_activeSwipeGestures; + QMap m_destroyConnections; + QVector m_swipeUpdates; +}; + +} + +Q_DECLARE_METATYPE(KWin::SwipeGesture::Direction) + +#endif Index: gestures.cpp =================================================================== --- /dev/null +++ gestures.cpp @@ -0,0 +1,145 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 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 "gestures.h" + +#include +#include + +namespace KWin +{ + +Gesture::Gesture(QObject *parent) + : QObject(parent) +{ +} + +Gesture::~Gesture() = default; + +SwipeGesture::SwipeGesture(QObject *parent) + : Gesture(parent) +{ +} + +SwipeGesture::~SwipeGesture() = default; + +GestureRecognizer::GestureRecognizer(QObject *parent) + : QObject(parent) +{ +} + +GestureRecognizer::~GestureRecognizer() = default; + +void GestureRecognizer::registerGesture(KWin::Gesture* gesture) +{ + Q_ASSERT(!m_gestures.contains(gesture)); + auto connection = connect(gesture, &QObject::destroyed, this, std::bind(&GestureRecognizer::unregisterGesture, this, gesture)); + m_destroyConnections.insert(gesture, connection); + m_gestures << gesture; +} + +void GestureRecognizer::unregisterGesture(KWin::Gesture* gesture) +{ + auto it = m_destroyConnections.find(gesture); + if (it != m_destroyConnections.end()) { + disconnect(it.value()); + m_destroyConnections.erase(it); + } + m_gestures.removeAll(gesture); + if (m_activeSwipeGestures.removeOne(gesture)) { + emit gesture->cancelled(); + } +} + +void GestureRecognizer::startSwipeGesture(uint fingerCount) +{ + // TODO: verify that no gesture is running + for (Gesture *gesture : qAsConst(m_gestures)) { + SwipeGesture *swipeGesture = qobject_cast(gesture); + if (!gesture) { + continue; + } + if (swipeGesture->minimumFingerCountIsRelevant()) { + if (swipeGesture->minimumFingerCount() > fingerCount) { + continue; + } + } + if (swipeGesture->maximumFingerCountIsRelevant()) { + if (swipeGesture->maximumFingerCount() < fingerCount) { + continue; + } + } + // direction doesn't matter yet + m_activeSwipeGestures << swipeGesture; + emit swipeGesture->started(); + } +} + +void GestureRecognizer::updateSwipeGesture(const QSizeF &delta) +{ + m_swipeUpdates << delta; + // determine the direction of the swipe + if (delta.width() == delta.height()) { + // special case of diagonal, this is not yet supported, thus cancel all gestures + cancelActiveSwipeGestures(); + return; + } + SwipeGesture::Direction direction; + if (std::abs(delta.width()) > std::abs(delta.height())) { + // horizontal + direction = delta.width() < 0 ? SwipeGesture::Direction::Left : SwipeGesture::Direction::Right; + } else { + // vertical + direction = delta.height() < 0 ? SwipeGesture::Direction::Up : SwipeGesture::Direction::Down; + } + for (auto it = m_activeSwipeGestures.begin(); it != m_activeSwipeGestures.end();) { + auto g = qobject_cast(*it); + if (g->direction() == direction) { + it++; + } else { + emit g->cancelled(); + it = m_activeSwipeGestures.erase(it); + } + } +} + +void GestureRecognizer::cancelActiveSwipeGestures() +{ + for (auto g : qAsConst(m_activeSwipeGestures)) { + emit g->cancelled(); + } + m_activeSwipeGestures.clear(); +} + +void GestureRecognizer::cancelSwipeGesture() +{ + cancelActiveSwipeGestures(); + m_swipeUpdates.clear(); +} + +void GestureRecognizer::endSwipeGesture() +{ + for (auto g : qAsConst(m_activeSwipeGestures)) { + emit g->triggered(); + } + m_activeSwipeGestures.clear(); + m_swipeUpdates.clear(); +} + +} Index: globalshortcuts.h =================================================================== --- globalshortcuts.h +++ globalshortcuts.h @@ -32,6 +32,8 @@ { class GlobalShortcut; +class SwipeGesture; +class GestureRecognizer; /** * @brief Manager for the global shortcut system inside KWin. @@ -67,6 +69,8 @@ */ void registerAxisShortcut(QAction *action, Qt::KeyboardModifiers modifiers, PointerAxisDirection axis); + void registerTouchpadSwipe(QAction *action, SwipeDirection direction); + /** * @brief Processes a key event to decide whether a shortcut needs to be triggered. * @@ -94,16 +98,23 @@ */ bool processAxis(Qt::KeyboardModifiers modifiers, PointerAxisDirection axis); + void processSwipeStart(uint fingerCount); + void processSwipeUpdate(const QSizeF &delta); + void processSwipeCancel(); + void processSwipeEnd(); + void setKGlobalAccelInterface(KGlobalAccelInterface *interface) { m_kglobalAccelInterface = interface; } private: void objectDeleted(QObject *object); QHash > m_pointerShortcuts; QHash > m_axisShortcuts; + QHash > m_swipeShortcuts; KGlobalAccelD *m_kglobalAccel = nullptr; KGlobalAccelInterface *m_kglobalAccelInterface = nullptr; + GestureRecognizer *m_gestureRecognizer; }; class GlobalShortcut @@ -114,33 +125,44 @@ const QKeySequence &shortcut() const; Qt::KeyboardModifiers pointerButtonModifiers() const; Qt::MouseButtons pointerButtons() const; + SwipeDirection swipeDirection() const { + return m_swipeDirection; + } virtual void invoke() = 0; protected: GlobalShortcut(const QKeySequence &shortcut); GlobalShortcut(Qt::KeyboardModifiers pointerButtonModifiers, Qt::MouseButtons pointerButtons); GlobalShortcut(Qt::KeyboardModifiers axisModifiers, PointerAxisDirection axis); + GlobalShortcut(SwipeDirection direction); private: QKeySequence m_shortcut; Qt::KeyboardModifiers m_pointerModifiers; Qt::MouseButtons m_pointerButtons; PointerAxisDirection m_axis; + SwipeDirection m_swipeDirection = SwipeDirection::Invalid;; }; class InternalGlobalShortcut : public GlobalShortcut { public: InternalGlobalShortcut(Qt::KeyboardModifiers modifiers, const QKeySequence &shortcut, QAction *action); InternalGlobalShortcut(Qt::KeyboardModifiers pointerButtonModifiers, Qt::MouseButtons pointerButtons, QAction *action); InternalGlobalShortcut(Qt::KeyboardModifiers axisModifiers, PointerAxisDirection axis, QAction *action); + InternalGlobalShortcut(Qt::KeyboardModifiers swipeModifier, SwipeDirection direction, QAction *action); virtual ~InternalGlobalShortcut(); void invoke() override; QAction *action() const; + + SwipeGesture *swipeGesture() const { + return m_swipe.data(); + } private: QAction *m_action; + QScopedPointer m_swipe; }; inline Index: globalshortcuts.cpp =================================================================== --- globalshortcuts.cpp +++ globalshortcuts.cpp @@ -22,6 +22,7 @@ // kwin #include #include "main.h" +#include "gestures.h" #include "utils.h" // KDE #include @@ -32,6 +33,11 @@ namespace KWin { +uint qHash(SwipeDirection direction) +{ + return uint(direction); +} + GlobalShortcut::GlobalShortcut(const QKeySequence &shortcut) : m_shortcut(shortcut) , m_pointerModifiers(Qt::NoModifier) @@ -56,6 +62,15 @@ { } +GlobalShortcut::GlobalShortcut(SwipeDirection direction) + : m_shortcut(QKeySequence()) + , m_pointerModifiers(Qt::NoModifier) + , m_pointerButtons(Qt::NoButton) + , m_axis(PointerAxisUp) + , m_swipeDirection(direction) +{ +} + GlobalShortcut::~GlobalShortcut() { } @@ -79,6 +94,35 @@ { } +static SwipeGesture::Direction toSwipeDirection(SwipeDirection direction) +{ + switch (direction) { + case SwipeDirection::Up: + return SwipeGesture::Direction::Up; + case SwipeDirection::Down: + return SwipeGesture::Direction::Down; + case SwipeDirection::Left: + return SwipeGesture::Direction::Left; + case SwipeDirection::Right: + return SwipeGesture::Direction::Right; + case SwipeDirection::Invalid: + default: + Q_UNREACHABLE(); + } +} + +InternalGlobalShortcut::InternalGlobalShortcut(Qt::KeyboardModifiers swipeModifier, SwipeDirection direction, QAction *action) + : GlobalShortcut(direction) + , m_action(action) + , m_swipe(new SwipeGesture) +{ + Q_UNUSED(swipeModifier) + m_swipe->setDirection(toSwipeDirection(direction)); + m_swipe->setMinimumFingerCount(4); + m_swipe->setMaximumFingerCount(4); + QObject::connect(m_swipe.data(), &SwipeGesture::triggered, m_action, &QAction::trigger, Qt::QueuedConnection); +} + InternalGlobalShortcut::~InternalGlobalShortcut() { } @@ -91,6 +135,7 @@ GlobalShortcutsManager::GlobalShortcutsManager(QObject *parent) : QObject(parent) + , m_gestureRecognizer(new GestureRecognizer(this)) { } @@ -106,6 +151,7 @@ { clearShortcuts(m_pointerShortcuts); clearShortcuts(m_axisShortcuts); + clearShortcuts(m_swipeShortcuts); } void GlobalShortcutsManager::init() @@ -146,10 +192,11 @@ { handleDestroyedAction(object, m_pointerShortcuts); handleDestroyedAction(object, m_axisShortcuts); + handleDestroyedAction(object, m_swipeShortcuts); } template -void addShortcut(T &shortcuts, QAction *action, Qt::KeyboardModifiers modifiers, R value) +GlobalShortcut *addShortcut(T &shortcuts, QAction *action, Qt::KeyboardModifiers modifiers, R value) { GlobalShortcut *cut = new InternalGlobalShortcut(modifiers, value, action); auto it = shortcuts.find(modifiers); @@ -161,6 +208,7 @@ s.insert(value, cut); shortcuts.insert(modifiers, s); } + return cut; } void GlobalShortcutsManager::registerPointerShortcut(QAction *action, Qt::KeyboardModifiers modifiers, Qt::MouseButtons pointerButtons) @@ -175,6 +223,13 @@ connect(action, &QAction::destroyed, this, &GlobalShortcutsManager::objectDeleted); } +void GlobalShortcutsManager::registerTouchpadSwipe(QAction *action, SwipeDirection direction) +{ + auto shortcut = addShortcut(m_swipeShortcuts, action, Qt::NoModifier, direction); + connect(action, &QAction::destroyed, this, &GlobalShortcutsManager::objectDeleted); + m_gestureRecognizer->registerGesture(static_cast(shortcut)->swipeGesture()); +} + template bool processShortcut(Qt::KeyboardModifiers mods, T key, U &shortcuts) { @@ -233,4 +288,25 @@ return processShortcut(mods, axis, m_axisShortcuts); } +void GlobalShortcutsManager::processSwipeStart(uint fingerCount) +{ + m_gestureRecognizer->startSwipeGesture(fingerCount); +} + +void GlobalShortcutsManager::processSwipeUpdate(const QSizeF &delta) +{ + m_gestureRecognizer->updateSwipeGesture(delta); +} + +void GlobalShortcutsManager::processSwipeCancel() +{ + m_gestureRecognizer->cancelSwipeGesture(); +} + +void GlobalShortcutsManager::processSwipeEnd() +{ + m_gestureRecognizer->endSwipeGesture(); + // TODO: cancel on Wayland Seat if one triggered +} + } // namespace Index: input.h =================================================================== --- input.h +++ input.h @@ -108,6 +108,7 @@ void registerShortcut(const QKeySequence &shortcut, QAction *action, T *receiver, void (T::*slot)()); void registerPointerShortcut(Qt::KeyboardModifiers modifiers, Qt::MouseButton pointerButtons, QAction *action); void registerAxisShortcut(Qt::KeyboardModifiers modifiers, PointerAxisDirection axis, QAction *action); + void registerTouchpadSwipeShortcut(SwipeDirection direction, QAction *action); void registerGlobalAccel(KGlobalAccelInterface *interface); /** Index: input.cpp =================================================================== --- input.cpp +++ input.cpp @@ -691,6 +691,26 @@ } return false; } + bool swipeGestureBegin(int fingerCount, quint32 time) override { + Q_UNUSED(time) + input()->shortcuts()->processSwipeStart(fingerCount); + return false; + } + bool swipeGestureUpdate(const QSizeF &delta, quint32 time) override { + Q_UNUSED(time) + input()->shortcuts()->processSwipeUpdate(delta); + return false; + } + bool swipeGestureCancelled(quint32 time) override { + Q_UNUSED(time) + input()->shortcuts()->processSwipeCancel(); + return false; + } + bool swipeGestureEnd(quint32 time) override { + Q_UNUSED(time) + input()->shortcuts()->processSwipeEnd(); + return false; + } }; class InternalWindowEventFilter : public InputEventFilter { @@ -1819,6 +1839,11 @@ m_shortcuts->registerAxisShortcut(action, modifiers, axis); } +void InputRedirection::registerTouchpadSwipeShortcut(SwipeDirection direction, QAction *action) +{ + m_shortcuts->registerTouchpadSwipe(action, direction); +} + void InputRedirection::registerGlobalAccel(KGlobalAccelInterface *interface) { m_shortcuts->setKGlobalAccelInterface(interface); Index: libkwineffects/CMakeLists.txt =================================================================== --- libkwineffects/CMakeLists.txt +++ libkwineffects/CMakeLists.txt @@ -5,7 +5,7 @@ VARIABLE_PREFIX KWINEFFECTS VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/kwineffects_version.h" PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KWinEffectsConfigVersion.cmake" - SOVERSION 10 + SOVERSION 11 ) ### xrenderutils lib ### Index: libkwineffects/kwineffects.h =================================================================== --- libkwineffects/kwineffects.h +++ libkwineffects/kwineffects.h @@ -876,6 +876,15 @@ virtual void registerAxisShortcut(Qt::KeyboardModifiers modifiers, PointerAxisDirection axis, QAction *action) = 0; /** + * @brief Registers a global touchpad swipe gesture shortcut with the provided @p action. + * + * @param direction The direction for the swipe + * @param action The action which gets triggered when the gesture triggers + * @since 5.10 + **/ + virtual void registerTouchpadSwipeShortcut(SwipeDirection direction, QAction *action) = 0; + + /** * Retrieve the proxy class for an effect if it has one. Will return NULL if * the effect isn't loaded or doesn't have a proxy class. */ Index: libkwineffects/kwinglobals.h =================================================================== --- libkwineffects/kwinglobals.h +++ libkwineffects/kwinglobals.h @@ -129,6 +129,18 @@ PointerAxisRight }; +/** + * @brief Directions for swipe gestures + * @since 5.10 + **/ +enum class SwipeDirection { + Invalid, + Down, + Left, + Up, + Right +}; + inline KWIN_EXPORT xcb_connection_t *connection() { Index: virtualdesktops.h =================================================================== --- virtualdesktops.h +++ virtualdesktops.h @@ -32,6 +32,7 @@ class KLocalizedString; class NETRootInfo; +class QAction; namespace KWin { @@ -420,16 +421,16 @@ * @param key The global shortcut for the action * @param slot The slot to invoke when the action is triggered **/ - void addAction(const QString &name, const KLocalizedString &label, uint value, const QKeySequence &key, void (VirtualDesktopManager::*slot)()); + QAction *addAction(const QString &name, const KLocalizedString &label, uint value, const QKeySequence &key, void (VirtualDesktopManager::*slot)()); /** * Creates an action and connects it to the @p slot in this Manager. * Overloaded method for the case that no additional value needs to be passed to the action and * no global shortcut is defined by default. * @param name The name of the action to be created * @param label The localized name for the action to be created * @param slot The slot to invoke when the action is triggered **/ - void addAction(const QString &name, const QString &label, void (VirtualDesktopManager::*slot)()); + QAction *addAction(const QString &name, const QString &label, void (VirtualDesktopManager::*slot)()); QVector m_desktops; QPointer m_current; Index: virtualdesktops.cpp =================================================================== --- virtualdesktops.cpp +++ virtualdesktops.cpp @@ -534,8 +534,10 @@ { initSwitchToShortcuts(); - addAction(QStringLiteral("Switch to Next Desktop"), i18n("Switch to Next Desktop"), &VirtualDesktopManager::slotNext); - addAction(QStringLiteral("Switch to Previous Desktop"), i18n("Switch to Previous Desktop"), &VirtualDesktopManager::slotPrevious); + QAction *nextAction = addAction(QStringLiteral("Switch to Next Desktop"), i18n("Switch to Next Desktop"), &VirtualDesktopManager::slotNext); + input()->registerTouchpadSwipeShortcut(SwipeDirection::Right, nextAction); + QAction *previousAction = addAction(QStringLiteral("Switch to Previous Desktop"), i18n("Switch to Previous Desktop"), &VirtualDesktopManager::slotPrevious); + input()->registerTouchpadSwipeShortcut(SwipeDirection::Left, previousAction); addAction(QStringLiteral("Switch One Desktop to the Right"), i18n("Switch One Desktop to the Right"), &VirtualDesktopManager::slotRight); addAction(QStringLiteral("Switch One Desktop to the Left"), i18n("Switch One Desktop to the Left"), &VirtualDesktopManager::slotLeft); addAction(QStringLiteral("Switch One Desktop Up"), i18n("Switch One Desktop Up"), &VirtualDesktopManager::slotUp); @@ -562,25 +564,27 @@ } } -void VirtualDesktopManager::addAction(const QString &name, const KLocalizedString &label, uint value, const QKeySequence &key, void (VirtualDesktopManager::*slot)()) +QAction *VirtualDesktopManager::addAction(const QString &name, const KLocalizedString &label, uint value, const QKeySequence &key, void (VirtualDesktopManager::*slot)()) { QAction *a = new QAction(this); a->setProperty("componentName", QStringLiteral(KWIN_NAME)); a->setObjectName(name.arg(value)); a->setText(label.subs(value).toString()); a->setData(value); KGlobalAccel::setGlobalShortcut(a, key); input()->registerShortcut(key, a, this, slot); + return a; } -void VirtualDesktopManager::addAction(const QString &name, const QString &label, void (VirtualDesktopManager::*slot)()) +QAction *VirtualDesktopManager::addAction(const QString &name, const QString &label, void (VirtualDesktopManager::*slot)()) { QAction *a = new QAction(this); a->setProperty("componentName", QStringLiteral(KWIN_NAME)); a->setObjectName(name); a->setText(label); KGlobalAccel::setGlobalShortcut(a, QKeySequence()); input()->registerShortcut(QKeySequence(), a, this, slot); + return a; } void VirtualDesktopManager::slotSwitchTo()