From 892d289f387141cfde4959a4d5062fcea78b71e7 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Fri, 12 Nov 2021 18:22:00 +0100 Subject: [PATCH] [tray] Makes scrolling with a touchpad in activiy list more natural This basically use the same method that is used in Kirigami and Plasma Components3. Signed-off-by: Carl Schwan --- src/gui/CMakeLists.txt | 1 + src/gui/systray.cpp | 3 + src/gui/tray/.wheelhandler.h.swo | Bin 0 -> 16384 bytes src/gui/tray/ActivityList.qml | 5 + src/gui/tray/wheelhandler.cpp | 289 +++++++++++++++++++++++++++++++ src/gui/tray/wheelhandler.h | 213 +++++++++++++++++++++++ 6 files changed, 511 insertions(+) create mode 100644 src/gui/tray/.wheelhandler.h.swo create mode 100644 src/gui/tray/wheelhandler.cpp create mode 100644 src/gui/tray/wheelhandler.h diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index b41810cd3..cc65cd6ff 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -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 diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp index ed9ed0a71..4158809de 100644 --- a/src/gui/systray.cpp +++ b/src/gui/systray.cpp @@ -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("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 index 0000000000000000000000000000000000000000..ab3ea46e26d47ebdb21f4aefe743e69578e63ae6 GIT binary patch literal 16384 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 literal 0 HcmV?d00001 diff --git a/src/gui/tray/ActivityList.qml b/src/gui/tray/ActivityList.qml index dc3528af9..795b3dc77 100644 --- a/src/gui/tray/ActivityList.qml +++ b/src/gui/tray/ActivityList.qml @@ -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 index 000000000..1e60e5f20 --- /dev/null +++ b/src/gui/tray/wheelhandler.cpp @@ -0,0 +1,289 @@ +/* + * SPDX-FileCopyrightText: 2019 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "wheelhandler.h" +#include +#include +#include + +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(obj); + m_handlersForItem.remove(item); + }); + + connect(handler, &QObject::destroyed, this, [this](QObject *obj) { + WheelHandler *handler = static_cast(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(watched); + if (!item || !item->isEnabled()) { + return QObject::eventFilter(watched, event); + } + QWheelEvent *we = static_cast(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 index 000000000..8e81f51e4 --- /dev/null +++ b/src/gui/tray/wheelhandler.h @@ -0,0 +1,213 @@ +/* + * SPDX-FileCopyrightText: 2019 Marco Martin + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include +#include +#include +#include + +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 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 m_target; + bool m_blockTargetWheel = true; + bool m_scrollFlickableTarget = true; + KirigamiWheelEvent m_wheelEvent; + + friend class GlobalWheelFilter; +}; + + -- 2.30.2