[tray] Makes scrolling with a touchpad in activiy list more natural
authorCarl Schwan <carl@carlschwan.eu>
Fri, 12 Nov 2021 17:22:00 +0000 (18:22 +0100)
committerMatthieu Gallien (Rebase PR Action) <matthieu_gallien@yahoo.fr>
Mon, 22 Nov 2021 11:18:18 +0000 (11:18 +0000)
This basically use the same method that is used in Kirigami and Plasma
Components3.

Signed-off-by: Carl Schwan <carl@carlschwan.eu>
src/gui/CMakeLists.txt
src/gui/systray.cpp
src/gui/tray/.wheelhandler.h.swo [new file with mode: 0644]
src/gui/tray/ActivityList.qml
src/gui/tray/wheelhandler.cpp [new file with mode: 0644]
src/gui/tray/wheelhandler.h [new file with mode: 0644]

index b41810cd334cacc8cea19a5854f1cc33192cbe23..cc65cd6ffe2ba39fa51010849bece04ff212bdd1 100644 (file)
@@ -134,6 +134,7 @@ set(client_SRCS
     tray/usermodel.cpp
     tray/notificationhandler.cpp
     tray/notificationcache.cpp
+    tray/wheelhandler.cpp
     creds/credentialsfactory.cpp
     creds/httpcredentialsgui.cpp
     creds/oauth.cpp
index ed9ed0a71d596bdd967ba8a606e431c79de2bcac..4158809de9cd26a3278b562c08e05b2d2c33b669 100644 (file)
@@ -19,6 +19,7 @@
 #include "common/utility.h"
 #include "tray/svgimageprovider.h"
 #include "tray/usermodel.h"
+#include "tray/wheelhandler.h"
 #include "tray/unifiedsearchresultimageprovider.h"
 #include "configfile.h"
 
@@ -91,6 +92,8 @@ Systray::Systray()
         }
     );
 
+    qmlRegisterType<WheelHandler>("com.nextcloud.desktopclient", 1, 0, "WheelHandler");
+
 #ifndef Q_OS_MAC
     auto contextMenu = new QMenu();
     if (AccountManager::instance()->accounts().isEmpty()) {
diff --git a/src/gui/tray/.wheelhandler.h.swo b/src/gui/tray/.wheelhandler.h.swo
new file mode 100644 (file)
index 0000000..ab3ea46
Binary files /dev/null and b/src/gui/tray/.wheelhandler.h.swo differ
index dc3528af954e323d9d850acdcf3bd1f16f9ca879..795b3dc7786736d19f9f4c7c0451fb04dd236990 100644 (file)
@@ -6,6 +6,7 @@ import Style 1.0
 import com.nextcloud.desktopclient 1.0 as NC
 
 ScrollView {
+    id: controlRoot
     property alias model: activityList.model
 
     signal showFileActivity(string displayPath, string absolutePath)
@@ -15,6 +16,10 @@ ScrollView {
 
     ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
 
+    data: NC.WheelHandler {
+        target: controlRoot.contentItem
+    }
+
     ListView {
         id: activityList
 
diff --git a/src/gui/tray/wheelhandler.cpp b/src/gui/tray/wheelhandler.cpp
new file mode 100644 (file)
index 0000000..1e60e5f
--- /dev/null
@@ -0,0 +1,289 @@
+/*
+ *  SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
+ *
+ *  SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#include "wheelhandler.h"
+#include <QWheelEvent>
+#include <QQuickItem>
+#include <QDebug>
+
+class GlobalWheelFilterSingleton
+{
+public:
+    GlobalWheelFilter self;
+};
+
+Q_GLOBAL_STATIC(GlobalWheelFilterSingleton, privateGlobalWheelFilterSelf)
+
+GlobalWheelFilter::GlobalWheelFilter(QObject *parent)
+    : QObject(parent)
+{
+}
+
+GlobalWheelFilter::~GlobalWheelFilter()
+{}
+
+GlobalWheelFilter *GlobalWheelFilter::self()
+{
+    return &privateGlobalWheelFilterSelf()->self;
+}
+
+void GlobalWheelFilter::setItemHandlerAssociation(QQuickItem *item, WheelHandler *handler)
+{
+    if (!m_handlersForItem.contains(handler->target())) {
+        handler->target()->installEventFilter(this);
+    }
+    m_handlersForItem.insert(item, handler);
+
+    connect(item, &QObject::destroyed, this, [this](QObject *obj) {
+        QQuickItem *item = static_cast<QQuickItem *>(obj);
+        m_handlersForItem.remove(item);
+    });
+
+    connect(handler, &QObject::destroyed, this, [this](QObject *obj) {
+        WheelHandler *handler = static_cast<WheelHandler *>(obj);
+        removeItemHandlerAssociation(handler->target(), handler);
+    });
+}
+
+void GlobalWheelFilter::removeItemHandlerAssociation(QQuickItem *item, WheelHandler *handler)
+{
+    if (!item || !handler) {
+        return;
+    }
+    m_handlersForItem.remove(item, handler);
+    if (!m_handlersForItem.contains(item)) {
+        item->removeEventFilter(this);
+    }
+}
+
+bool GlobalWheelFilter::eventFilter(QObject *watched, QEvent *event)
+{
+    if (event->type() == QEvent::Wheel) {
+        QQuickItem *item = qobject_cast<QQuickItem *>(watched);
+        if (!item || !item->isEnabled()) {
+            return QObject::eventFilter(watched, event);
+        }
+        QWheelEvent *we = static_cast<QWheelEvent *>(event);
+        m_wheelEvent.initializeFromEvent(we);
+
+        bool shouldBlock = false;
+        bool shouldScrollFlickable = false;
+
+        for (auto *handler : m_handlersForItem.values(item)) {
+            if (handler->m_blockTargetWheel) {
+                shouldBlock = true;
+            }
+            if (handler->m_scrollFlickableTarget) {
+                shouldScrollFlickable = true;
+            }
+            emit handler->wheel(&m_wheelEvent);
+        }
+
+        if (shouldScrollFlickable && !m_wheelEvent.isAccepted()) {
+            manageWheel(item, we);
+        }
+
+        if (shouldBlock) {
+            return true;
+        }
+    }
+    return QObject::eventFilter(watched, event);
+}
+
+void GlobalWheelFilter::manageWheel(QQuickItem *target, QWheelEvent *event)
+{
+    // Duck typing: accept everyhint that has all the properties we need
+    if (target->metaObject()->indexOfProperty("contentX") == -1
+        || target->metaObject()->indexOfProperty("contentY") == -1
+        || target->metaObject()->indexOfProperty("contentWidth") == -1
+        || target->metaObject()->indexOfProperty("contentHeight") == -1
+        || target->metaObject()->indexOfProperty("topMargin") == -1
+        || target->metaObject()->indexOfProperty("bottomMargin") == -1
+        || target->metaObject()->indexOfProperty("leftMargin") == -1
+        || target->metaObject()->indexOfProperty("rightMargin") == -1
+        || target->metaObject()->indexOfProperty("originX") == -1
+        || target->metaObject()->indexOfProperty("originY") == -1) {
+        return;
+    }
+
+    qreal contentWidth = target->property("contentWidth").toReal();
+    qreal contentHeight = target->property("contentHeight").toReal();
+    qreal contentX = target->property("contentX").toReal();
+    qreal contentY = target->property("contentY").toReal();
+    qreal topMargin = target->property("topMargin").toReal();
+    qreal bottomMargin = target->property("bottomMargin").toReal();
+    qreal leftMargin = target->property("leftMaring").toReal();
+    qreal rightMargin = target->property("rightMargin").toReal();
+    qreal originX = target->property("originX").toReal();
+    qreal originY = target->property("originY").toReal();
+
+    // Scroll Y
+    if (contentHeight > target->height()) {
+
+        int y = event->pixelDelta().y() != 0 ? event->pixelDelta().y() : event->angleDelta().y() / 8;
+
+        //if we don't have a pixeldelta, apply the configured mouse wheel lines
+        if (!event->pixelDelta().y()) {
+            y *= 3; // Magic copied value from Kirigami::Settings
+        }
+
+        // Scroll one page regardless of delta:
+        if ((event->modifiers() & Qt::ControlModifier) || (event->modifiers() & Qt::ShiftModifier)) {
+            if (y > 0) {
+                y = target->height();
+            } else if (y < 0) {
+                y = -target->height();
+            }
+        }
+
+        qreal minYExtent = topMargin - originY;
+        qreal maxYExtent = target->height() - (contentHeight + bottomMargin + originY);
+
+        target->setProperty("contentY", qMin(-maxYExtent, qMax(-minYExtent, contentY - y)));
+    }
+
+    //Scroll X
+    if (contentWidth > target->width()) {
+
+        int x = event->pixelDelta().x() != 0 ? event->pixelDelta().x() : event->angleDelta().x() / 8;
+
+        // Special case: when can't scroll vertically, scroll horizontally with vertical wheel as well
+        if (x == 0 && contentHeight <= target->height()) {
+            x = event->pixelDelta().y() != 0 ? event->pixelDelta().y() : event->angleDelta().y() / 8;
+        }
+
+        //if we don't have a pixeldelta, apply the configured mouse wheel lines
+        if (!event->pixelDelta().x()) {
+            x *= 3; // Magic copied value from Kirigami::Settings
+        }
+
+        // Scroll one page regardless of delta:
+        if ((event->modifiers() & Qt::ControlModifier) || (event->modifiers() & Qt::ShiftModifier)) {
+            if (x > 0) {
+                x = target->width();
+            } else if (x < 0) {
+                x = -target->width();
+            }
+        }
+
+        qreal minXExtent = leftMargin - originX;
+        qreal maxXExtent = target->width() - (contentWidth + rightMargin + originX);
+
+        target->setProperty("contentX", qMin(-maxXExtent, qMax(-minXExtent, contentX - x)));
+    }
+
+    //this is just for making the scrollbar
+    target->metaObject()->invokeMethod(target, "flick", Q_ARG(double, 0), Q_ARG(double, 1));
+    target->metaObject()->invokeMethod(target, "cancelFlick");
+}
+
+
+////////////////////////////
+KirigamiWheelEvent::KirigamiWheelEvent(QObject *parent)
+    : QObject(parent)
+{}
+
+KirigamiWheelEvent::~KirigamiWheelEvent()
+{}
+
+void KirigamiWheelEvent::initializeFromEvent(QWheelEvent *event)
+{
+#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
+    m_x = event->position().x();
+    m_y = event->position().y();
+#else
+    m_x = event->x();
+    m_y = event->y();
+#endif
+    m_angleDelta = event->angleDelta();
+    m_pixelDelta = event->pixelDelta();
+    m_buttons = event->buttons();
+    m_modifiers = event->modifiers();
+    m_accepted = false;
+    m_inverted = event->inverted();
+}
+
+qreal KirigamiWheelEvent::x() const
+{
+    return m_x;
+}
+
+qreal KirigamiWheelEvent::y() const
+{
+    return m_y;
+}
+
+QPointF KirigamiWheelEvent::angleDelta() const
+{
+    return m_angleDelta;
+}
+
+QPointF KirigamiWheelEvent::pixelDelta() const
+{
+    return m_pixelDelta;
+}
+
+int KirigamiWheelEvent::buttons() const
+{
+    return m_buttons;
+}
+
+int KirigamiWheelEvent::modifiers() const
+{
+    return m_modifiers;
+}
+
+bool KirigamiWheelEvent::inverted() const
+{
+    return m_inverted;
+}
+
+bool KirigamiWheelEvent::isAccepted()
+{
+    return m_accepted;
+}
+
+void KirigamiWheelEvent::setAccepted(bool accepted)
+{
+    m_accepted = accepted;
+}
+
+
+///////////////////////////////
+
+WheelHandler::WheelHandler(QObject *parent)
+    : QObject(parent)
+{
+}
+
+WheelHandler::~WheelHandler()
+{
+}
+
+QQuickItem *WheelHandler::target() const
+{
+    return m_target;
+}
+
+void WheelHandler::setTarget(QQuickItem *target)
+{
+    if (m_target == target) {
+        return;
+    }
+
+    if (m_target) {
+        GlobalWheelFilter::self()->removeItemHandlerAssociation(m_target, this);
+    }
+
+    m_target = target;
+
+    GlobalWheelFilter::self()->setItemHandlerAssociation(target, this);
+
+    emit targetChanged();
+}
+
+
+#include "moc_wheelhandler.cpp"
diff --git a/src/gui/tray/wheelhandler.h b/src/gui/tray/wheelhandler.h
new file mode 100644 (file)
index 0000000..8e81f51
--- /dev/null
@@ -0,0 +1,213 @@
+/*
+ *  SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
+ *
+ *  SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#pragma once
+
+#include <QtQml>
+#include <QPoint>
+#include <QQuickItem>
+#include <QObject>
+
+class QWheelEvent;
+
+class WheelHandler;
+
+/**
+ * Describes the mouse wheel event
+ */
+class KirigamiWheelEvent : public QObject
+{
+    Q_OBJECT
+
+    /**
+     * x: real
+     *
+     * X coordinate of the mouse pointer
+     */
+    Q_PROPERTY(qreal x READ x CONSTANT)
+
+    /**
+     * y: real
+     *
+     * Y coordinate of the mouse pointer
+     */
+    Q_PROPERTY(qreal y READ y CONSTANT)
+
+    /**
+     * angleDelta: point
+     *
+     * The distance the wheel is rotated in degrees.
+     * The x and y coordinates indicate the horizontal and vertical wheels respectively.
+     * A positive value indicates it was rotated up/right, negative, bottom/left
+     * This value is more likely to be set in traditional mice.
+     */
+    Q_PROPERTY(QPointF angleDelta READ angleDelta CONSTANT)
+
+    /**
+     * pixelDelta: point
+     *
+     * provides the delta in screen pixels available on high resolution trackpads
+     */
+    Q_PROPERTY(QPointF pixelDelta READ pixelDelta CONSTANT)
+
+    /**
+     * buttons: int
+     *
+     * it contains an OR combination of the buttons that were pressed during the wheel, they can be:
+     * Qt.LeftButton, Qt.MiddleButton, Qt.RightButton
+     */
+    Q_PROPERTY(int buttons READ buttons CONSTANT)
+
+    /**
+     * modifiers: int
+     *
+     * Keyboard mobifiers that were pressed during the wheel event, such as:
+     * Qt.NoModifier (default, no modifiers)
+     * Qt.ControlModifier
+     * Qt.ShiftModifier
+     * ...
+     */
+    Q_PROPERTY(int modifiers READ modifiers CONSTANT)
+
+    /**
+     * inverted: bool
+     *
+     * Whether the delta values are inverted
+     * On some platformsthe returned delta are inverted, so positive values would mean bottom/left
+     */
+    Q_PROPERTY(bool inverted READ inverted CONSTANT)
+
+    /**
+     * accepted: bool
+     *
+     * If set, the event shouldn't be managed anymore,
+     * for instance it can be used to block the handler to manage the scroll of a view on some scenarions
+     * @code
+     * // This handler handles automatically the scroll of
+     * // flickableItem, unless Ctrl is pressed, in this case the
+     * // app has custom code to handle Ctrl+wheel zooming
+     * Kirigami.WheelHandler {
+     *   target: flickableItem
+     *   blockTargetWheel: true
+     *   scrollFlickableTarget: true
+     *   onWheel: {
+     *        if (wheel.modifiers & Qt.ControlModifier) {
+     *            wheel.accepted = true;
+     *            // Handle scaling of the view
+     *       }
+     *   }
+     * }
+     * @endcode
+     *
+     */
+    Q_PROPERTY(bool accepted READ isAccepted WRITE setAccepted)
+
+public:
+    KirigamiWheelEvent(QObject *parent = nullptr);
+    ~KirigamiWheelEvent();
+
+    void initializeFromEvent(QWheelEvent *event);
+
+    qreal x() const;
+    qreal y() const;
+    QPointF angleDelta() const;
+    QPointF pixelDelta() const;
+    int buttons() const;
+    int modifiers() const;
+    bool inverted() const;
+    bool isAccepted();
+    void setAccepted(bool accepted);
+
+private:
+    qreal m_x = 0;
+    qreal m_y = 0;
+    QPointF m_angleDelta;
+    QPointF m_pixelDelta;
+    Qt::MouseButtons m_buttons = Qt::NoButton;
+    Qt::KeyboardModifiers m_modifiers = Qt::NoModifier;
+    bool m_inverted = false;
+    bool m_accepted = false;
+};
+
+class GlobalWheelFilter : public QObject
+{
+    Q_OBJECT
+
+public:
+    GlobalWheelFilter(QObject *parent = nullptr);
+    ~GlobalWheelFilter();
+
+    static GlobalWheelFilter *self();
+
+    void setItemHandlerAssociation(QQuickItem *item, WheelHandler *handler);
+    void removeItemHandlerAssociation(QQuickItem *item, WheelHandler *handler);
+
+protected:
+    bool eventFilter(QObject *watched, QEvent *event) override;
+
+private:
+    void manageWheel(QQuickItem *target, QWheelEvent *wheel);
+
+    QMultiHash<QQuickItem *, WheelHandler *> m_handlersForItem;
+    KirigamiWheelEvent m_wheelEvent;
+};
+
+
+
+/**
+ * This class intercepts the mouse wheel events of its target, and gives them to the user code as a signal, which can be used for custom mouse wheel management code.
+ * The handler can block completely the wheel events from its target, and if it's a Flickable, it can automatically handle scrolling on it
+ */
+class WheelHandler : public QObject
+{
+    Q_OBJECT
+
+    /**
+     * target: Item
+     *
+     * The target we want to manage wheel events.
+     * We will receive wheel() signals every time the user moves
+     * the mouse wheel (or scrolls with the touchpad) on top
+     * of that item.
+     */
+    Q_PROPERTY(QQuickItem *target READ target WRITE setTarget NOTIFY targetChanged)
+
+    /**
+     * blockTargetWheel: bool
+     *
+     * If true, the target won't receive any wheel event at all (default true)
+     */
+    Q_PROPERTY(bool blockTargetWheel MEMBER m_blockTargetWheel NOTIFY blockTargetWheelChanged)
+
+    /**
+     * scrollFlickableTarget: bool
+     * If this property is true and the target is a Flickable, wheel events will cause the Flickable to scroll (default true)
+     */
+    Q_PROPERTY(bool scrollFlickableTarget MEMBER m_scrollFlickableTarget NOTIFY scrollFlickableTargetChanged)
+
+public:
+    explicit WheelHandler(QObject *parent = nullptr);
+    ~WheelHandler() override;
+
+    QQuickItem *target() const;
+    void setTarget(QQuickItem *target);
+
+Q_SIGNALS:
+    void targetChanged();
+    void blockTargetWheelChanged();
+    void scrollFlickableTargetChanged();
+    void wheel(KirigamiWheelEvent *wheel);
+
+private:
+    QPointer<QQuickItem> m_target;
+    bool m_blockTargetWheel = true;
+    bool m_scrollFlickableTarget = true;
+    KirigamiWheelEvent m_wheelEvent;
+
+    friend class GlobalWheelFilter;
+};
+
+