From: Carl Schwan Date: Fri, 12 Nov 2021 17:22:00 +0000 (+0100) Subject: [tray] Makes scrolling with a touchpad in activiy list more natural X-Git-Tag: archive/raspbian/3.16.7-1_deb13u1+rpi1~1^2~12^2~18^2~96^2~1 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=892d289f387141cfde4959a4d5062fcea78b71e7;p=nextcloud-desktop.git [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 --- 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 000000000..ab3ea46e2 Binary files /dev/null and b/src/gui/tray/.wheelhandler.h.swo differ 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; +}; + +