[vlc-commits] [Git][videolan/vlc][master] 7 commits: qml: do not use scroll handler if flickable is not interactive in `ListViewExt`

Steve Lhomme (@robUx4) gitlab at videolan.org
Thu Mar 19 11:21:21 UTC 2026



Steve Lhomme pushed to branch master at VideoLAN / VLC


Commits:
4244d3d9 by Fatih Uzunoglu at 2026-03-19T10:20:19+00:00
qml: do not use scroll handler if flickable is not interactive in `ListViewExt`

- - - - -
1367ad64 by Fatih Uzunoglu at 2026-03-19T10:20:19+00:00
qml: do not use scroll handler if flickable is not interactive in `ExpandGridView`

- - - - -
9fa98ce3 by Fatih Uzunoglu at 2026-03-19T10:20:19+00:00
qml: do not use scroll handler if flickable is not interactive in `BrowseHomeDisplay`

- - - - -
4311c5d0 by Fatih Uzunoglu at 2026-03-19T10:20:19+00:00
qml: do not use scroll handler if flickable is not interactive in `HomePage`

- - - - -
89cf535b by Fatih Uzunoglu at 2026-03-19T10:20:19+00:00
qml: do not use scroll handler if flickable is not interactive in `ToolbarEditorButtonList`

- - - - -
0adbd7d0 by Fatih Uzunoglu at 2026-03-19T10:20:19+00:00
qt: introduce kirigami wheel handler

- - - - -
2becd9e1 by Fatih Uzunoglu at 2026-03-19T10:20:19+00:00
qt: introduce option to use kirigami scroll handler

Changing the option requires restarting the application.

- - - - -


15 changed files:

- modules/gui/qt/Makefile.am
- modules/gui/qt/dialogs/toolbar/qml/EditorDNDView.qml
- modules/gui/qt/dialogs/toolbar/qml/ToolbarEditorButtonList.qml
- modules/gui/qt/maininterface/mainui.cpp
- modules/gui/qt/medialibrary/qml/HomePage.qml
- modules/gui/qt/meson.build
- modules/gui/qt/network/qml/BrowseHomeDisplay.qml
- modules/gui/qt/qt.cpp
- modules/gui/qt/util/flickable_scroll_handler.cpp
- modules/gui/qt/util/flickable_scroll_handler.hpp
- + modules/gui/qt/util/kirigamiwheelhandler.cpp
- + modules/gui/qt/util/kirigamiwheelhandler.hpp
- modules/gui/qt/util/qml/DefaultFlickableScrollHandler.qml
- modules/gui/qt/widgets/qml/ExpandGridView.qml
- modules/gui/qt/widgets/qml/ListViewExt.qml


Changes:

=====================================
modules/gui/qt/Makefile.am
=====================================
@@ -345,6 +345,8 @@ libqt_plugin_la_SOURCES = \
 	util/colorizedsvgicon.hpp \
 	util/textureproviderobserver.cpp \
 	util/textureproviderobserver.hpp \
+	util/kirigamiwheelhandler.cpp \
+	util/kirigamiwheelhandler.hpp \
 	widgets/native/animators.cpp \
 	widgets/native/animators.hpp \
 	widgets/native/customwidgets.cpp widgets/native/customwidgets.hpp \
@@ -503,6 +505,7 @@ nodist_libqt_plugin_la_SOURCES = \
 	util/vlchotkeyconverter.moc.cpp \
 	util/qsgtextureview.moc.cpp \
 	util/textureproviderobserver.moc.cpp \
+	util/kirigamiwheelhandler.moc.cpp \
 	widgets/native/animators.moc.cpp \
 	widgets/native/csdthemeimage.moc.cpp \
 	widgets/native/customwidgets.moc.cpp \


=====================================
modules/gui/qt/dialogs/toolbar/qml/EditorDNDView.qml
=====================================
@@ -102,7 +102,7 @@ ListView {
         colorSet: ColorContext.View
     }
 
-    DefaultFlickableScrollHandler {
+    VLCFlickableScrollHandler {
         fallbackScroll: true
         enabled: true
     }


=====================================
modules/gui/qt/dialogs/toolbar/qml/ToolbarEditorButtonList.qml
=====================================
@@ -60,7 +60,17 @@ GridView {
         preventStealing: true
     }
 
-    DefaultFlickableScrollHandler { }
+    property Component implicitFlickableScrollHandler: DefaultFlickableScrollHandler { }
+
+    // NOTE: This property can be set to null to prevent using a scroll handler:
+    property FlickableScrollHandler scrollHandler: {
+        if (interactive) {
+            // JS ownership:
+            return implicitFlickableScrollHandler.createObject(null, { target: root })
+        } else {
+            return null
+        }
+    }
 
     DropArea {
         id: dropArea


=====================================
modules/gui/qt/maininterface/mainui.cpp
=====================================
@@ -41,6 +41,7 @@
 #include "style/systempalette.hpp"
 #include "util/navigation_history.hpp"
 #include "util/flickable_scroll_handler.hpp"
+#include "util/kirigamiwheelhandler.hpp"
 #include "util/color_svg_image_provider.hpp"
 #include "util/effects_image_provider.hpp"
 #include "util/vlcaccess_image_provider.hpp"
@@ -362,7 +363,18 @@ void MainUI::registerQMLTypes()
         qmlRegisterType<ImageLuminanceExtractor>( uri, versionMajor, versionMinor, "ImageLuminanceExtractor");
 
         qmlRegisterType<ItemKeyEventFilter>( uri, versionMajor, versionMinor, "KeyEventFilter" );
-        qmlRegisterType<FlickableScrollHandler>( uri, versionMajor, versionMinor, "FlickableScrollHandler" );
+
+        // WARNING: This type is deprecated, do not use it.
+        qmlRegisterType<FlickableScrollHandler>( uri, versionMajor, versionMinor, "VLCFlickableScrollHandler" );
+
+        {
+            auto funcRegisterScrollHandler = qmlRegisterType<DummyFlickableScrollHandler>;
+            if (var_InheritBool(m_intf, "qt-use-scroll-handler"))
+                funcRegisterScrollHandler = qmlRegisterType<Kirigami::WheelHandler>;
+
+            funcRegisterScrollHandler( uri, versionMajor, versionMinor, "FlickableScrollHandler" );
+        }
+
         qmlRegisterType<ListSelectionModel>( uri, versionMajor, versionMinor, "ListSelectionModel" );
         qmlRegisterType<DoubleClickIgnoringItem>( uri, versionMajor, versionMinor, "DoubleClickIgnoringItem" );
         qmlRegisterType<TextureProviderObserver>( uri, versionMajor, versionMinor, "TextureProviderObserver" );


=====================================
modules/gui/qt/medialibrary/qml/HomePage.qml
=====================================
@@ -114,7 +114,17 @@ T.Page {
             }
         }
 
-        DefaultFlickableScrollHandler {}
+        property Component implicitFlickableScrollHandler: DefaultFlickableScrollHandler { }
+
+        // NOTE: This property can be set to null to prevent using a scroll handler:
+        property FlickableScrollHandler scrollHandler: {
+            if (interactive) {
+                // JS ownership:
+                return implicitFlickableScrollHandler.createObject(null, { target: flickable })
+            } else {
+                return null
+            }
+        }
 
         Component.onCompleted: {
             // Flickable filters child mouse events for flicking (even when


=====================================
modules/gui/qt/meson.build
=====================================
@@ -153,6 +153,7 @@ moc_headers = files(
     'util/list_selection_model.hpp',
     'util/qsgtextureview.hpp',
     'util/textureproviderobserver.hpp',
+    'util/kirigamiwheelhandler.hpp',
     'widgets/native/animators.hpp',
     'widgets/native/csdthemeimage.hpp',
     'widgets/native/customwidgets.hpp',
@@ -495,6 +496,8 @@ qt_plugin_sources = files(
     'util/vlcqtmessagehandler.hpp',
     'util/colorizedsvgicon.cpp',
     'util/colorizedsvgicon.hpp',
+    'util/kirigamiwheelhandler.cpp',
+    'util/kirigamiwheelhandler.hpp',
     'widgets/native/animators.cpp',
     'widgets/native/animators.hpp',
     'widgets/native/customwidgets.cpp',


=====================================
modules/gui/qt/network/qml/BrowseHomeDisplay.qml
=====================================
@@ -154,7 +154,17 @@ FocusScope {
             }
         }
 
-        DefaultFlickableScrollHandler { }
+        property Component implicitFlickableScrollHandler: DefaultFlickableScrollHandler { }
+
+        // NOTE: This property can be set to null to prevent using a scroll handler:
+        property FlickableScrollHandler scrollHandler: {
+            if (interactive) {
+                // JS ownership:
+                return implicitFlickableScrollHandler.createObject(null, { target: flickable })
+            } else {
+                return null
+            }
+        }
 
         Navigation.parentItem: root
 


=====================================
modules/gui/qt/qt.cpp
=====================================
@@ -254,7 +254,11 @@ static void ShowDialog   ( intf_thread_t *, int, int, intf_dialog_args_t * );
 #define QT_COMPOSITOR_LONGTEXT N_("Select Qt video integration backend. Use with care, the interface may not start if an incompatible compositor is selected")
 
 #define SMOOTH_SCROLLING_TEXT N_( "Use smooth scrolling in Flickable based views" )
-#define SMOOTH_SCROLLING_LONGTEXT N_( "Deactivating this option will disable smooth scrolling in Flickable based views (such as the Playqueue)" )
+#define SMOOTH_SCROLLING_LONGTEXT N_( "Deactivating this option will disable smooth scrolling in Flickable based views (such as the Playqueue). " \
+                                      "This option is only respected with a scroll handler, see option `qt-use-scroll-handler`." )
+
+#define SCROLL_HANDLER_TEXT N_( "Use Kirigami scroll handler for Flickable based views" )
+#define SCROLL_HANDLER_LONGTEXT N_( "Using Kirigami scroll handler may improve scrolling behavior." )
 
 #define SAFE_AREA_TEXT N_( "Safe area for the user interface" )
 #define SAFE_AREA_LONGTEXT N_( "Sets the safe area percentage between 0.0 and 100 when you want " \
@@ -442,6 +446,8 @@ vlc_module_begin ()
 
     add_bool( "qt-smooth-scrolling", true, SMOOTH_SCROLLING_TEXT, SMOOTH_SCROLLING_LONGTEXT )
 
+    add_bool( "qt-use-scroll-handler", true, SCROLL_HANDLER_TEXT, SCROLL_HANDLER_LONGTEXT )
+
     add_bool( "qt-verbose", false, VERBOSE_TEXT, VERBOSE_LONGTEXT )
 
     add_bool( "qt-close-to-system-tray", false, HIDE_WINDOW_ON_CLOSE_TEXT, HIDE_WINDOW_ON_CLOSE_LONGTEXT )


=====================================
modules/gui/qt/util/flickable_scroll_handler.cpp
=====================================
@@ -26,7 +26,7 @@
 #define PAGE_SCROLL_SHIFT_OR_CTRL true
 
 FlickableScrollHandler::FlickableScrollHandler(QObject *parent)
-    : QObject(parent)
+    : DummyFlickableScrollHandler(parent)
 {
     connect(this, &FlickableScrollHandler::scaleFactorChanged, this, [this]() {
         m_effectiveScaleFactor = QApplication::styleHints()->wheelScrollLines() * 20 * m_scaleFactor;
@@ -238,19 +238,6 @@ void FlickableScrollHandler::adjustScrollBar(ScrollBar& scrollBar)
     }
 }
 
-qreal FlickableScrollHandler::scaleFactor() const
-{
-    return m_scaleFactor;
-}
-
-void FlickableScrollHandler::setScaleFactor(qreal newScaleFactor)
-{
-    if (qFuzzyCompare(m_scaleFactor, newScaleFactor))
-        return;
-    m_scaleFactor = newScaleFactor;
-    emit scaleFactorChanged();
-}
-
 bool FlickableScrollHandler::enabled() const
 {
     return m_enabled;


=====================================
modules/gui/qt/util/flickable_scroll_handler.hpp
=====================================
@@ -24,13 +24,35 @@
 #include <QQuickItem>
 #include <QQmlParserStatus>
 
-class FlickableScrollHandler : public QObject, public QQmlParserStatus
+class DummyFlickableScrollHandler : public QObject
+{
+    Q_OBJECT
+
+    // These properties may be overridden if necessary, they are not marked as final.
+    Q_PROPERTY(qreal scaleFactor MEMBER m_scaleFactor NOTIFY scaleFactorChanged)
+    Q_PROPERTY(bool smoothScroll MEMBER m_smoothScroll NOTIFY smoothScrollChanged)
+    Q_PROPERTY(int duration MEMBER m_duration NOTIFY durationChanged)
+
+public:
+    explicit DummyFlickableScrollHandler(QObject *parent = nullptr) : QObject(parent) { }
+
+signals:
+    void scaleFactorChanged();
+    void smoothScrollChanged();
+    void durationChanged();
+
+protected:
+    qreal m_scaleFactor = 1.;
+    int m_duration = 200;
+    bool m_smoothScroll = true;
+};
+
+class FlickableScrollHandler : public DummyFlickableScrollHandler, public QQmlParserStatus
 {
     Q_OBJECT
 
     Q_PROPERTY(QObject* parent READ parent NOTIFY initialized FINAL)
 
-    Q_PROPERTY(qreal scaleFactor READ scaleFactor WRITE setScaleFactor NOTIFY scaleFactorChanged FINAL)
     Q_PROPERTY(qreal effectiveScaleFactor READ effectiveScaleFactor NOTIFY effectiveScaleFactorChanged FINAL)
     Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged FINAL)
     Q_PROPERTY(bool fallbackScroll MEMBER m_fallbackScroll NOTIFY fallbackScrollChanged FINAL)
@@ -42,11 +64,9 @@ public:
     explicit FlickableScrollHandler(QObject *parent = nullptr);
     ~FlickableScrollHandler();
 
-    qreal scaleFactor() const;
     qreal effectiveScaleFactor() const;
     bool enabled() const;
 
-    void setScaleFactor(qreal newScaleFactor);
     void setEnabled(bool newEnabled);
 
     void classBegin() override;
@@ -55,7 +75,6 @@ public:
 signals:
     void initialized();
 
-    void scaleFactorChanged();
     void enabledChanged();
     void effectiveScaleFactorChanged();
     void fallbackScrollChanged();
@@ -74,7 +93,6 @@ private:
 
 private:
     QPointer<QQuickItem> m_target = nullptr;
-    qreal m_scaleFactor = 1.;
     qreal m_effectiveScaleFactor;
     bool m_enabled = true;
     bool m_fallbackScroll = false;


=====================================
modules/gui/qt/util/kirigamiwheelhandler.cpp
=====================================
@@ -0,0 +1,899 @@
+/*
+ *  SPDX-FileCopyrightText: 2019 Marco Martin <mart at kde.org>
+ *
+ *  SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+/*
+ *  This file is part of the KDE Kirigami project.
+ *  Upstream commit: 85ac5406
+ *
+ *  It is slightly modified to fit into the codebase here.
+ *  Modifications are subject to the same license.
+ *
+ *  Origin: kirigami/src/wheelhandler.cpp
+ */
+
+#include "kirigamiwheelhandler.hpp"
+
+#include <QQmlEngine>
+#include <QQuickWindow>
+#include <QWheelEvent>
+
+using namespace Kirigami;
+
+KirigamiWheelEvent::KirigamiWheelEvent(QObject *parent)
+    : QObject(parent)
+{
+}
+
+KirigamiWheelEvent::~KirigamiWheelEvent()
+{
+}
+
+void KirigamiWheelEvent::initializeFromEvent(QWheelEvent *event)
+{
+    m_x = event->position().x();
+    m_y = event->position().y();
+    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;
+}
+
+///////////////////////////////
+
+WheelFilterItem::WheelFilterItem(QQuickItem *parent)
+    : QQuickItem(parent)
+{
+    setEnabled(false);
+}
+
+///////////////////////////////
+
+WheelHandler::WheelHandler(QObject *parent)
+    : DummyFlickableScrollHandler(parent)
+    , m_filterItem(new WheelFilterItem(nullptr))
+{
+    m_filterItem->installEventFilter(this);
+
+    m_wheelScrollingTimer.setSingleShot(true);
+    m_wheelScrollingTimer.setInterval(m_wheelScrollingDuration);
+    m_wheelScrollingTimer.callOnTimeout([this]() {
+        setScrolling(false);
+    });
+
+    m_xScrollAnimation.setEasingCurve(QEasingCurve::OutCubic);
+    m_yScrollAnimation.setEasingCurve(QEasingCurve::OutCubic);
+    m_xInertiaScrollAnimation.setEasingCurve(QEasingCurve::OutQuad);
+    m_yInertiaScrollAnimation.setEasingCurve(QEasingCurve::OutQuad);
+
+    const auto adjustStepSizes = [this](int scrollLines) {
+        m_defaultPixelStepSize = 20 * scrollLines * m_scaleFactor;
+        if (!m_explicitVStepSize && m_verticalStepSize != m_defaultPixelStepSize) {
+            m_verticalStepSize = m_defaultPixelStepSize;
+            Q_EMIT verticalStepSizeChanged();
+        }
+        if (!m_explicitHStepSize && m_horizontalStepSize != m_defaultPixelStepSize) {
+            m_horizontalStepSize = m_defaultPixelStepSize;
+            Q_EMIT horizontalStepSizeChanged();
+        }
+    };
+
+    connect(QGuiApplication::styleHints(), &QStyleHints::wheelScrollLinesChanged, this, adjustStepSizes);
+    connect(this, &WheelHandler::scaleFactorChanged, this, [adjustStepSizes]() {
+        assert(QGuiApplication::styleHints());
+        const int scrollLines = QGuiApplication::styleHints()->wheelScrollLines();
+        adjustStepSizes(scrollLines);
+    });
+}
+
+WheelHandler::~WheelHandler()
+{
+    delete m_filterItem;
+}
+
+QQuickItem *WheelHandler::target() const
+{
+    return m_flickable;
+}
+
+void WheelHandler::setTarget(QQuickItem *target)
+{
+    if (m_flickable == target) {
+        return;
+    }
+
+    if (target && !target->inherits("QQuickFlickable")) {
+        qmlWarning(this) << "target must be a QQuickFlickable";
+        return;
+    }
+
+    if (m_flickable) {
+        m_flickable->removeEventFilter(this);
+        disconnect(m_flickable, nullptr, m_filterItem, nullptr);
+        disconnect(m_flickable, &QQuickItem::parentChanged, this, &WheelHandler::_k_rebindScrollBars);
+    }
+
+    m_flickable = target;
+    m_filterItem->setParentItem(target);
+    if (m_xScrollAnimation.targetObject()) {
+        m_xScrollAnimation.stop();
+    }
+    m_xScrollAnimation.setTargetObject(target);
+    if (m_yScrollAnimation.targetObject()) {
+        m_yScrollAnimation.stop();
+    }
+    m_yScrollAnimation.setTargetObject(target);
+    if (m_yInertiaScrollAnimation.targetObject()) {
+        m_yInertiaScrollAnimation.stop();
+    }
+    m_yInertiaScrollAnimation.setTargetObject(target);
+    if (m_xInertiaScrollAnimation.targetObject()) {
+        m_xInertiaScrollAnimation.stop();
+    }
+    m_xInertiaScrollAnimation.setTargetObject(target);
+
+    if (target) {
+        target->installEventFilter(this);
+
+        // Stack WheelFilterItem over the Flickable's scrollable content
+        m_filterItem->stackAfter(target->property("contentItem").value<QQuickItem *>());
+        // Make it fill the Flickable
+        m_filterItem->setWidth(target->width());
+        m_filterItem->setHeight(target->height());
+        connect(target, &QQuickItem::widthChanged, m_filterItem, [this, target]() {
+            m_filterItem->setWidth(target->width());
+        });
+        connect(target, &QQuickItem::heightChanged, m_filterItem, [this, target]() {
+            m_filterItem->setHeight(target->height());
+        });
+    }
+
+    _k_rebindScrollBars();
+
+    Q_EMIT targetChanged();
+}
+
+void WheelHandler::_k_rebindScrollBars()
+{
+    struct ScrollBarAttached {
+        QObject *attached = nullptr;
+        QQuickItem *vertical = nullptr;
+        QQuickItem *horizontal = nullptr;
+    };
+
+    ScrollBarAttached attachedToFlickable;
+    ScrollBarAttached attachedToScrollView;
+
+    if (m_flickable) {
+        // Get ScrollBars so that we can filter them too, even if they're not
+        // in the bounds of the Flickable
+        const auto flickableChildren = m_flickable->children();
+        for (const auto child : flickableChildren) {
+            if (child->inherits("QQuickScrollBarAttached")) {
+                attachedToFlickable.attached = child;
+                attachedToFlickable.vertical = child->property("vertical").value<QQuickItem *>();
+                attachedToFlickable.horizontal = child->property("horizontal").value<QQuickItem *>();
+                break;
+            }
+        }
+
+        // Check ScrollView if there are no scrollbars attached to the Flickable.
+        // We need to check if the parent inherits QQuickScrollView in case the
+        // parent is another Flickable that already has a Kirigami WheelHandler.
+        auto flickableParent = m_flickable->parentItem();
+        if (m_scrollView && m_scrollView != flickableParent) {
+            m_scrollView->removeEventFilter(this);
+        }
+        if (flickableParent && flickableParent->inherits("QQuickScrollView")) {
+            if (m_scrollView != flickableParent) {
+                m_scrollView = flickableParent;
+                m_scrollView->installEventFilter(this);
+            }
+            const auto siblings = m_scrollView->children();
+            for (const auto child : siblings) {
+                if (child->inherits("QQuickScrollBarAttached")) {
+                    attachedToScrollView.attached = child;
+                    attachedToScrollView.vertical = child->property("vertical").value<QQuickItem *>();
+                    attachedToScrollView.horizontal = child->property("horizontal").value<QQuickItem *>();
+                    break;
+                }
+            }
+        }
+    }
+
+    // Dilemma: ScrollBars can be attached to both ScrollView and Flickable,
+    // but only one of them should be shown anyway. Let's prefer Flickable.
+
+    struct ChosenScrollBar {
+        QObject *attached = nullptr;
+        QQuickItem *scrollBar = nullptr;
+    };
+
+    ChosenScrollBar vertical;
+    if (attachedToFlickable.vertical) {
+        vertical.attached = attachedToFlickable.attached;
+        vertical.scrollBar = attachedToFlickable.vertical;
+    } else if (attachedToScrollView.vertical) {
+        vertical.attached = attachedToScrollView.attached;
+        vertical.scrollBar = attachedToScrollView.vertical;
+    }
+
+    ChosenScrollBar horizontal;
+    if (attachedToFlickable.horizontal) {
+        horizontal.attached = attachedToFlickable.attached;
+        horizontal.scrollBar = attachedToFlickable.horizontal;
+    } else if (attachedToScrollView.horizontal) {
+        horizontal.attached = attachedToScrollView.attached;
+        horizontal.scrollBar = attachedToScrollView.horizontal;
+    }
+
+    // Flickable may get re-parented to or out of a ScrollView, so we need to
+    // redo the discovery process. This is especially important for
+    // Kirigami.ScrollablePage component.
+    if (m_flickable) {
+        if (attachedToFlickable.horizontal && attachedToFlickable.vertical) {
+            // But if both scrollbars are already those from the preferred
+            // Flickable, there's no need for rediscovery.
+            disconnect(m_flickable, &QQuickItem::parentChanged, this, &WheelHandler::_k_rebindScrollBars);
+        } else {
+            connect(m_flickable, &QQuickItem::parentChanged, this, &WheelHandler::_k_rebindScrollBars, Qt::UniqueConnection);
+        }
+    }
+
+    if (m_verticalScrollBar != vertical.scrollBar) {
+        if (m_verticalScrollBar) {
+            m_verticalScrollBar->removeEventFilter(this);
+            disconnect(m_verticalChangedConnection);
+        }
+        m_verticalScrollBar = vertical.scrollBar;
+        if (vertical.scrollBar) {
+            vertical.scrollBar->installEventFilter(this);
+            m_verticalChangedConnection = connect(vertical.attached, SIGNAL(verticalChanged()), this, SLOT(_k_rebindScrollBars()));
+        }
+    }
+
+    if (m_horizontalScrollBar != horizontal.scrollBar) {
+        if (m_horizontalScrollBar) {
+            m_horizontalScrollBar->removeEventFilter(this);
+            disconnect(m_horizontalChangedConnection);
+        }
+        m_horizontalScrollBar = horizontal.scrollBar;
+        if (horizontal.scrollBar) {
+            horizontal.scrollBar->installEventFilter(this);
+            m_horizontalChangedConnection = connect(horizontal.attached, SIGNAL(horizontalChanged()), this, SLOT(_k_rebindScrollBars()));
+        }
+    }
+}
+
+qreal WheelHandler::verticalStepSize() const
+{
+    return m_verticalStepSize;
+}
+
+void WheelHandler::setVerticalStepSize(qreal stepSize)
+{
+    m_explicitVStepSize = true;
+    if (qFuzzyCompare(m_verticalStepSize, stepSize)) {
+        return;
+    }
+    // Mimic the behavior of QQuickScrollBar when stepSize is 0
+    if (qFuzzyIsNull(stepSize)) {
+        resetVerticalStepSize();
+        return;
+    }
+    m_verticalStepSize = stepSize;
+    Q_EMIT verticalStepSizeChanged();
+}
+
+void WheelHandler::resetVerticalStepSize()
+{
+    m_explicitVStepSize = false;
+    if (qFuzzyCompare(m_verticalStepSize, m_defaultPixelStepSize)) {
+        return;
+    }
+    m_verticalStepSize = m_defaultPixelStepSize;
+    Q_EMIT verticalStepSizeChanged();
+}
+
+qreal WheelHandler::horizontalStepSize() const
+{
+    return m_horizontalStepSize;
+}
+
+void WheelHandler::setHorizontalStepSize(qreal stepSize)
+{
+    m_explicitHStepSize = true;
+    if (qFuzzyCompare(m_horizontalStepSize, stepSize)) {
+        return;
+    }
+    // Mimic the behavior of QQuickScrollBar when stepSize is 0
+    if (qFuzzyIsNull(stepSize)) {
+        resetHorizontalStepSize();
+        return;
+    }
+    m_horizontalStepSize = stepSize;
+    Q_EMIT horizontalStepSizeChanged();
+}
+
+void WheelHandler::resetHorizontalStepSize()
+{
+    m_explicitHStepSize = false;
+    if (qFuzzyCompare(m_horizontalStepSize, m_defaultPixelStepSize)) {
+        return;
+    }
+    m_horizontalStepSize = m_defaultPixelStepSize;
+    Q_EMIT horizontalStepSizeChanged();
+}
+
+Qt::KeyboardModifiers WheelHandler::pageScrollModifiers() const
+{
+    return m_pageScrollModifiers;
+}
+
+void WheelHandler::setPageScrollModifiers(Qt::KeyboardModifiers modifiers)
+{
+    if (m_pageScrollModifiers == modifiers) {
+        return;
+    }
+    m_pageScrollModifiers = modifiers;
+    Q_EMIT pageScrollModifiersChanged();
+}
+
+void WheelHandler::resetPageScrollModifiers()
+{
+    setPageScrollModifiers(m_defaultPageScrollModifiers);
+}
+
+bool WheelHandler::filterMouseEvents() const
+{
+    return m_filterMouseEvents;
+}
+
+void WheelHandler::setFilterMouseEvents(bool enabled)
+{
+    if (m_filterMouseEvents == enabled) {
+        return;
+    }
+    m_filterMouseEvents = enabled;
+    Q_EMIT filterMouseEventsChanged();
+}
+
+bool WheelHandler::keyNavigationEnabled() const
+{
+    return m_keyNavigationEnabled;
+}
+
+void WheelHandler::setKeyNavigationEnabled(bool enabled)
+{
+    if (m_keyNavigationEnabled == enabled) {
+        return;
+    }
+    m_keyNavigationEnabled = enabled;
+    Q_EMIT keyNavigationEnabledChanged();
+}
+
+void WheelHandler::classBegin()
+{
+    // Initializes smooth scrolling
+    m_engine = qmlEngine(this);
+}
+
+void WheelHandler::componentComplete()
+{
+    const auto parentItem = qobject_cast<QQuickItem*>(parent());
+    if (!target() && parentItem && parentItem->inherits("QQuickFlickable"))
+        setTarget(parentItem);
+}
+
+void WheelHandler::setScrolling(bool scrolling)
+{
+    if (m_wheelScrolling == scrolling) {
+        if (m_wheelScrolling) {
+            m_wheelScrollingTimer.start();
+        }
+        return;
+    }
+    m_wheelScrolling = scrolling;
+    m_filterItem->setEnabled(m_wheelScrolling);
+}
+
+void WheelHandler::startInertiaScrolling()
+{
+    const qreal width = m_flickable->width();
+    const qreal height = m_flickable->height();
+    const qreal contentWidth = m_flickable->property("contentWidth").toReal();
+    const qreal contentHeight = m_flickable->property("contentHeight").toReal();
+    const qreal topMargin = m_flickable->property("topMargin").toReal();
+    const qreal bottomMargin = m_flickable->property("bottomMargin").toReal();
+    const qreal leftMargin = m_flickable->property("leftMargin").toReal();
+    const qreal rightMargin = m_flickable->property("rightMargin").toReal();
+    const qreal originX = m_flickable->property("originX").toReal();
+    const qreal originY = m_flickable->property("originY").toReal();
+    const qreal contentX = m_flickable->property("contentX").toReal();
+    const qreal contentY = m_flickable->property("contentY").toReal();
+
+    QPointF minExtent = QPointF(leftMargin, topMargin) - QPointF(originX, originY);
+    QPointF maxExtent = QPointF(width, height) - (QPointF(contentWidth, contentHeight) + QPointF(rightMargin, bottomMargin) + QPointF(originX, originY));
+
+    QPointF totalDelta(0, 0);
+    for (const QPoint delta : m_wheelEvents) {
+        totalDelta += delta;
+    }
+    const uint64_t elapsed = std::max<uint64_t>(m_timestamps.last() - m_timestamps.first(), 1);
+
+    // The inertia is more natural if we multiply
+    // the actual scrolling speed by some factor,
+    // chosen manually here to be 2.5. Otherwise, the
+    // scrolling will appear to be too slow.
+    const qreal speedFactor = 2.5;
+
+    // We get the velocity in px/s by calculating
+    // displacement / elapsed time; we multiply by
+    // 1000 since the elapsed time is in ms.
+    QPointF vel = -totalDelta * 1000 / elapsed * speedFactor;
+    QPointF startValue = QPointF(contentX, contentY);
+
+    // We decelerate at 4000px/s^2, chosen by manual test
+    // to be natural.
+    const qreal deceleration = 4000 * speedFactor;
+
+    // We use constant deceleration formulas to find:
+    // time = |velocity / deceleration|
+    // distance_traveled = time * velocity / 2
+    QPointF time = QPointF(qAbs(vel.x() / deceleration), qAbs(vel.y() / deceleration));
+    QPointF endValue = QPointF(startValue.x() + time.x() * vel.x() / 2, startValue.y() + time.y() * vel.y() / 2);
+
+    // We bound the end value so that we don't animate
+    // beyond the scrollable amount.
+    QPointF boundedEndValue =
+        QPointF(std::max(std::min(endValue.x(), -maxExtent.x()), -minExtent.x()), std::max(std::min(endValue.y(), -maxExtent.y()), -minExtent.y()));
+
+    // If we did bound the end value, we check how much
+    // (from 0 to 1) of the animation is actually played,
+    // and we adjust the time required for it accordingly.
+    QPointF progressFactor = QPointF((boundedEndValue.x() - startValue.x()) / (endValue.x() - startValue.x()),
+                                     (boundedEndValue.y() - startValue.y()) / (endValue.y() - startValue.y()));
+    // The formula here is:
+    // partial_time = complete_time * (1 - sqrt(1 - partial_progress_factor)),
+    // with partial_progress_factor being between 0 and 1.
+    // It can be obtained by inverting the OutQad easing formula,
+    // which is f(t) = t(2 - t).
+    // We also convert back from seconds to milliseconds.
+    QPointF realTime = QPointF(time.x() * (1 - std::sqrt(1 - progressFactor.x())), time.y() * (1 - std::sqrt(1 - progressFactor.y()))) * 1000;
+    m_wheelEvents.clear();
+    m_timestamps.clear();
+
+    m_xScrollAnimation.stop();
+    m_yScrollAnimation.stop();
+    if (realTime.x() > 0) {
+        m_xInertiaScrollAnimation.setStartValue(startValue.x());
+        m_xInertiaScrollAnimation.setEndValue(boundedEndValue.x());
+        m_xInertiaScrollAnimation.setDuration(realTime.x());
+        m_xInertiaScrollAnimation.start(QAbstractAnimation::KeepWhenStopped);
+    }
+    if (realTime.y() > 0) {
+        m_yInertiaScrollAnimation.setStartValue(startValue.y());
+        m_yInertiaScrollAnimation.setEndValue(boundedEndValue.y());
+        m_yInertiaScrollAnimation.setDuration(realTime.y());
+        m_yInertiaScrollAnimation.start(QAbstractAnimation::KeepWhenStopped);
+    }
+}
+
+bool WheelHandler::scrollFlickable(QPointF pixelDelta, QPointF angleDelta, Qt::KeyboardModifiers modifiers)
+{
+    if (!m_flickable || (pixelDelta.isNull() && angleDelta.isNull())) {
+        return false;
+    }
+
+    const qreal width = m_flickable->width();
+    const qreal height = m_flickable->height();
+    const qreal contentWidth = m_flickable->property("contentWidth").toReal();
+    const qreal contentHeight = m_flickable->property("contentHeight").toReal();
+    const qreal contentX = m_flickable->property("contentX").toReal();
+    const qreal contentY = m_flickable->property("contentY").toReal();
+    const qreal topMargin = m_flickable->property("topMargin").toReal();
+    const qreal bottomMargin = m_flickable->property("bottomMargin").toReal();
+    const qreal leftMargin = m_flickable->property("leftMargin").toReal();
+    const qreal rightMargin = m_flickable->property("rightMargin").toReal();
+    const qreal originX = m_flickable->property("originX").toReal();
+    const qreal originY = m_flickable->property("originY").toReal();
+    const qreal pageWidth = width - leftMargin - rightMargin;
+    const qreal pageHeight = height - topMargin - bottomMargin;
+    const auto window = m_flickable->window();
+    const auto screen = window ? window->screen() : nullptr;
+    const qreal devicePixelRatio = window != nullptr ? window->devicePixelRatio() : qGuiApp->devicePixelRatio();
+    const qreal refreshRate = screen ? screen->refreshRate() : 0;
+    const bool pixelAligned = m_flickable->property("pixelAligned").toBool();
+
+    // HACK: Only transpose deltas when not using xcb in order to not conflict with xcb's own delta transposing
+    if (modifiers & m_defaultHorizontalScrollModifiers && qGuiApp->platformName() != QLatin1String("xcb")) {
+        angleDelta = angleDelta.transposed();
+        pixelDelta = pixelDelta.transposed();
+    }
+
+    const qreal xTicks = angleDelta.x() / 120;
+    const qreal yTicks = angleDelta.y() / 120;
+    bool scrolled = false;
+
+    auto getChange = [pageScrollModifiers = modifiers & m_pageScrollModifiers](qreal ticks, qreal pixelDelta, qreal stepSize, qreal pageSize) {
+        // Use page size with pageScrollModifiers. Matches QScrollBar, which uses QAbstractSlider behavior.
+        if (pageScrollModifiers) {
+            return qBound(-pageSize, ticks * pageSize, pageSize);
+        } else if (pixelDelta != 0) {
+            return pixelDelta;
+        } else {
+            return ticks * stepSize;
+        }
+    };
+
+    auto getPosition = [devicePixelRatio, pixelAligned](qreal size,
+                                                        qreal contentSize,
+                                                        qreal contentPos,
+                                                        qreal originPos,
+                                                        qreal pageSize,
+                                                        qreal leadingMargin,
+                                                        qreal trailingMargin,
+                                                        qreal change,
+                                                        const QPropertyAnimation &animation) {
+        if (contentSize <= pageSize) {
+            return contentPos;
+        }
+
+        // contentX and contentY use reversed signs from what x and y would normally use, so flip the signs
+
+        qreal minExtent = leadingMargin - originPos;
+        qreal maxExtent = size - (contentSize + trailingMargin + originPos);
+        qreal newContentPos = (animation.state() == QPropertyAnimation::Running ? animation.endValue().toReal() : contentPos) - change;
+        // bound the values without asserts
+        newContentPos = std::max(-minExtent, std::min(newContentPos, -maxExtent));
+
+        // Flickable::pixelAligned rounds the position, so round to mimic that behavior.
+        // Rounding prevents fractional positioning from causing text to be
+        // clipped off on the top and bottom.
+        // Multiply by devicePixelRatio before rounding and divide by devicePixelRatio
+        // after to make position match pixels on the screen more closely.
+        if (pixelAligned)
+            return std::round(newContentPos * devicePixelRatio) / devicePixelRatio;
+        else
+            return (newContentPos * devicePixelRatio) / devicePixelRatio;
+    };
+
+    auto setPosition = [this, devicePixelRatio, refreshRate](qreal oldPos, qreal newPos, qreal stepSize, const char *property, QPropertyAnimation &animation) {
+        animation.stop();
+        if (oldPos == newPos) {
+            return false;
+        }
+        if (!m_smoothScroll || !m_engine || refreshRate <= 0) {
+            animation.setDuration(0);
+            m_flickable->setProperty(property, newPos);
+            return true;
+        }
+
+        // Can't use wheelEvent->deviceType() to determine device type since
+        // on Wayland mouse is always regarded as touchpad:
+        // https://invent.kde.org/qt/qt/qtwayland/-/blob/e695a39519a7629c1549275a148cfb9ab99a07a9/src/client/qwaylandinputdevice.cpp#L445
+        // Mouse wheel can generate angle delta like 240, 360 and so on when
+        // scrolling very fast on some mice such as the Logitech M150.
+        // Mice with hi-res mouse wheels such as the Logitech MX Master 3 can
+        // generate angle deltas as small as 16.
+        // On X11, trackpads can also generate very fine angle deltas.
+
+        // Duration is based on the duration and movement for 120 angle delta.
+        // Shorten duration for smaller movements, limit duration for big movements.
+        // We don't want fine deltas to feel extra slow and fast scrolling should still feel fast.
+        // Minimum 3 frames for a 60hz display if delta > 2 physical pixels
+        // (start already rendered -> 1/3 rendered -> 2/3 rendered -> end rendered).
+        // Skip animation if <= 2 real frames for low refresh rate screens.
+        // Otherwise, we don't scale the duration based on refresh rate or
+        // device pixel ratio to avoid making the animation unexpectedly
+        // longer or shorter on different screens.
+
+        qreal absPixelDelta = std::abs(newPos - oldPos);
+        int duration = absPixelDelta * devicePixelRatio > 2 //
+            ? std::max(qCeil(1000.0 / 60.0 * 3), std::min(qRound(absPixelDelta * m_duration / stepSize), m_duration))
+            : 0;
+        animation.setDuration(duration <= qCeil(1000.0 / refreshRate * 2) ? 0 : duration);
+        if (animation.duration() > 0) {
+            animation.setStartValue(oldPos);
+            animation.setEndValue(newPos);
+            animation.start(QAbstractAnimation::KeepWhenStopped);
+        } else {
+            m_flickable->setProperty(property, newPos);
+        }
+        return true;
+    };
+
+    qreal xChange = getChange(xTicks, pixelDelta.x(), m_horizontalStepSize, pageWidth);
+    qreal newContentX = getPosition(width, contentWidth, contentX, originX, pageWidth, leftMargin, rightMargin, xChange, m_xScrollAnimation);
+
+    qreal yChange = getChange(yTicks, pixelDelta.y(), m_verticalStepSize, pageHeight);
+    qreal newContentY = getPosition(height, contentHeight, contentY, originY, pageHeight, topMargin, bottomMargin, yChange, m_yScrollAnimation);
+
+    // Don't use `||` because we need the position to be set for contentX and contentY.
+    scrolled |= setPosition(contentX, newContentX, m_horizontalStepSize, "contentX", m_xScrollAnimation);
+    scrolled |= setPosition(contentY, newContentY, m_verticalStepSize, "contentY", m_yScrollAnimation);
+
+    return scrolled;
+}
+
+bool WheelHandler::scrollUp(qreal stepSize)
+{
+    if (qFuzzyIsNull(stepSize)) {
+        return false;
+    } else if (stepSize < 0) {
+        stepSize = m_verticalStepSize;
+    }
+    // contentY uses reversed sign
+    return scrollFlickable(QPointF(0, stepSize));
+}
+
+bool WheelHandler::scrollDown(qreal stepSize)
+{
+    if (qFuzzyIsNull(stepSize)) {
+        return false;
+    } else if (stepSize < 0) {
+        stepSize = m_verticalStepSize;
+    }
+    // contentY uses reversed sign
+    return scrollFlickable(QPointF(0, -stepSize));
+}
+
+bool WheelHandler::scrollLeft(qreal stepSize)
+{
+    if (qFuzzyIsNull(stepSize)) {
+        return false;
+    } else if (stepSize < 0) {
+        stepSize = m_horizontalStepSize;
+    }
+    // contentX uses reversed sign
+    return scrollFlickable(QPoint(stepSize, 0));
+}
+
+bool WheelHandler::scrollRight(qreal stepSize)
+{
+    if (qFuzzyIsNull(stepSize)) {
+        return false;
+    } else if (stepSize < 0) {
+        stepSize = m_horizontalStepSize;
+    }
+    // contentX uses reversed sign
+    return scrollFlickable(QPoint(-stepSize, 0));
+}
+
+bool WheelHandler::eventFilter(QObject *watched, QEvent *event)
+{
+    auto item = qobject_cast<QQuickItem *>(watched);
+    if (!item || !item->isEnabled()) {
+        return false;
+    }
+
+    // We only process keyboard events for QQuickScrollView.
+    const auto eventType = event->type();
+    if (item == m_scrollView && eventType != QEvent::KeyPress && eventType != QEvent::KeyRelease) {
+        return false;
+    }
+
+    qreal contentWidth = 0;
+    qreal contentHeight = 0;
+    qreal pageWidth = 0;
+    qreal pageHeight = 0;
+    if (m_flickable) {
+        contentWidth = m_flickable->property("contentWidth").toReal();
+        contentHeight = m_flickable->property("contentHeight").toReal();
+        pageWidth = m_flickable->width() - m_flickable->property("leftMargin").toReal() - m_flickable->property("rightMargin").toReal();
+        pageHeight = m_flickable->height() - m_flickable->property("topMargin").toReal() - m_flickable->property("bottomMargin").toReal();
+    }
+
+    // The code handling touch, mouse and hover events is mostly copied/adapted from QQuickScrollView::childMouseEventFilter()
+    switch (eventType) {
+    case QEvent::Wheel: {
+        // QQuickScrollBar::interactive handling Matches behavior in QQuickScrollView::eventFilter()
+        if (m_filterMouseEvents) {
+            if (m_verticalScrollBar) {
+                m_verticalScrollBar->setProperty("interactive", true);
+            }
+            if (m_horizontalScrollBar) {
+                m_horizontalScrollBar->setProperty("interactive", true);
+            }
+        }
+        QWheelEvent *wheelEvent = static_cast<QWheelEvent *>(event);
+
+        // NOTE: On X11 with libinput, pixelDelta is identical to angleDelta when using a mouse that shouldn't use pixelDelta.
+        // If faulty pixelDelta, reset pixelDelta to (0,0).
+        if (wheelEvent->pixelDelta() == wheelEvent->angleDelta()) {
+            // In order to change any of the data, we have to create a whole new QWheelEvent from its constructor.
+            QWheelEvent newWheelEvent(wheelEvent->position(),
+                                      wheelEvent->globalPosition(),
+                                      QPoint(0, 0), // pixelDelta
+                                      wheelEvent->angleDelta(),
+                                      wheelEvent->buttons(),
+                                      wheelEvent->modifiers(),
+                                      wheelEvent->phase(),
+                                      wheelEvent->inverted(),
+                                      wheelEvent->source());
+            m_kirigamiWheelEvent.initializeFromEvent(&newWheelEvent);
+        } else {
+            m_kirigamiWheelEvent.initializeFromEvent(wheelEvent);
+        }
+
+        Q_EMIT wheel(&m_kirigamiWheelEvent);
+
+        if (m_wheelEvents.count() > 6) {
+            m_wheelEvents.dequeue();
+            m_timestamps.dequeue();
+        }
+        if (m_wheelEvents.count() > 2 && wheelEvent->isEndEvent()) {
+            startInertiaScrolling();
+        } else {
+            m_wheelEvents.enqueue(wheelEvent->pixelDelta());
+            m_timestamps.enqueue(wheelEvent->timestamp());
+        }
+
+        if (m_kirigamiWheelEvent.isAccepted()) {
+            return true;
+        }
+
+        bool scrolled = false;
+        if (m_scrollFlickableTarget || (contentHeight <= pageHeight && contentWidth <= pageWidth)) {
+            // Don't use pixelDelta from the event unless angleDelta is not available
+            // because scrolling by pixelDelta is too slow on Wayland with libinput.
+            QPointF pixelDelta = m_kirigamiWheelEvent.angleDelta().isNull() ? m_kirigamiWheelEvent.pixelDelta() : QPoint(0, 0);
+            scrolled = scrollFlickable(pixelDelta, m_kirigamiWheelEvent.angleDelta(), Qt::KeyboardModifiers(m_kirigamiWheelEvent.modifiers()));
+        }
+        setScrolling(scrolled);
+
+        // NOTE: Wheel events created by touchpad gestures with pixel deltas will cause scrolling to jump back
+        // to where scrolling started unless the event is always accepted before it reaches the Flickable.
+        bool flickableWillUseGestureScrolling = !(wheelEvent->source() == Qt::MouseEventNotSynthesized || wheelEvent->pixelDelta().isNull());
+        return scrolled || m_blockTargetWheel || flickableWillUseGestureScrolling;
+    }
+
+    case QEvent::TouchBegin: {
+        m_wasTouched = true;
+        if (!m_filterMouseEvents) {
+            break;
+        }
+        if (m_verticalScrollBar) {
+            m_verticalScrollBar->setProperty("interactive", false);
+        }
+        if (m_horizontalScrollBar) {
+            m_horizontalScrollBar->setProperty("interactive", false);
+        }
+        break;
+    }
+
+    case QEvent::TouchEnd: {
+        m_wasTouched = false;
+        break;
+    }
+
+    case QEvent::MouseButtonPress: {
+        // NOTE: Flickable does not handle touch events, only synthesized mouse events
+        m_wasTouched = static_cast<QMouseEvent *>(event)->source() != Qt::MouseEventNotSynthesized;
+        if (!m_filterMouseEvents) {
+            break;
+        }
+        if (!m_wasTouched) {
+            if (m_verticalScrollBar) {
+                m_verticalScrollBar->setProperty("interactive", true);
+            }
+            if (m_horizontalScrollBar) {
+                m_horizontalScrollBar->setProperty("interactive", true);
+            }
+            break;
+        }
+        return !m_wasTouched && item == m_flickable;
+    }
+
+    case QEvent::MouseMove:
+    case QEvent::MouseButtonRelease: {
+        setScrolling(false);
+        if (!m_filterMouseEvents) {
+            break;
+        }
+        if (static_cast<QMouseEvent *>(event)->source() == Qt::MouseEventNotSynthesized && item == m_flickable) {
+            return true;
+        }
+        break;
+    }
+
+    case QEvent::HoverEnter:
+    case QEvent::HoverMove: {
+        if (!m_filterMouseEvents) {
+            break;
+        }
+        if (m_wasTouched && (item == m_verticalScrollBar || item == m_horizontalScrollBar)) {
+            if (m_verticalScrollBar) {
+                m_verticalScrollBar->setProperty("interactive", true);
+            }
+            if (m_horizontalScrollBar) {
+                m_horizontalScrollBar->setProperty("interactive", true);
+            }
+        }
+        break;
+    }
+
+    case QEvent::KeyPress: {
+        if (!m_keyNavigationEnabled) {
+            break;
+        }
+        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
+        bool horizontalScroll = keyEvent->modifiers() & m_defaultHorizontalScrollModifiers;
+        switch (keyEvent->key()) {
+        case Qt::Key_Up:
+            return scrollUp();
+        case Qt::Key_Down:
+            return scrollDown();
+        case Qt::Key_Left:
+            return scrollLeft();
+        case Qt::Key_Right:
+            return scrollRight();
+        case Qt::Key_PageUp:
+            return horizontalScroll ? scrollLeft(pageWidth) : scrollUp(pageHeight);
+        case Qt::Key_PageDown:
+            return horizontalScroll ? scrollRight(pageWidth) : scrollDown(pageHeight);
+        case Qt::Key_Home:
+            return horizontalScroll ? scrollLeft(contentWidth) : scrollUp(contentHeight);
+        case Qt::Key_End:
+            return horizontalScroll ? scrollRight(contentWidth) : scrollDown(contentHeight);
+        default:
+            break;
+        }
+        break;
+    }
+
+    default:
+        break;
+    }
+
+    return false;
+}


=====================================
modules/gui/qt/util/kirigamiwheelhandler.hpp
=====================================
@@ -0,0 +1,459 @@
+/* SPDX-FileCopyrightText: 2019 Marco Martin <mart at kde.org>
+ * SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs at gmail.com>
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+/*
+ *  This file is part of the KDE Kirigami project.
+ *  Upstream commit: 85ac5406
+ *
+ *  It is slightly modified to fit into the codebase here.
+ *  Modifications are subject to the same license.
+ *
+ *  Origin: kirigami/src/wheelhandler.h
+ */
+
+#pragma once
+
+#include "util/flickable_scroll_handler.hpp"
+
+#include <QGuiApplication>
+#include <QObject>
+#include <QPoint>
+#include <QPropertyAnimation>
+#include <QQmlParserStatus>
+#include <QQueue>
+#include <QQuickItem>
+#include <QStyleHints>
+#include <QTimer>
+
+class QWheelEvent;
+class QQmlEngine;
+
+namespace Kirigami {
+
+class WheelHandler;
+
+/*!
+ * \qmltype WheelEvent
+ * \inqmlmodule org.kde.kirigami
+ *
+ * \brief Describes the mouse wheel event.
+ */
+class KirigamiWheelEvent : public QObject
+{
+    Q_OBJECT
+    QML_NAMED_ELEMENT(WheelEvent)
+    QML_UNCREATABLE("")
+
+    /*!
+     * \qmlproperty double WheelEvent::x
+     *
+     * X coordinate of the mouse pointer.
+     */
+    Q_PROPERTY(qreal x READ x CONSTANT FINAL)
+
+    /*!
+     * \qmlproperty double WheelEvent::y
+     *
+     * Y coordinate of the mouse pointer.
+     */
+    Q_PROPERTY(qreal y READ y CONSTANT FINAL)
+
+    /*!
+     * \qmlproperty point WheelEvent::angleDelta
+     *
+     * 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 FINAL)
+
+    /*!
+     * \qmlproperty point WheelEvent::pixelDelta
+     *
+     * Provides the delta in screen pixels available on high resolution trackpads.
+     */
+    Q_PROPERTY(QPointF pixelDelta READ pixelDelta CONSTANT FINAL)
+
+    /*!
+     * \qmlproperty int WheelEvent::buttons
+     *
+     * It contains an OR combination of the buttons that were pressed during the wheel, they can be:
+     * \list
+     * \li Qt.LeftButton
+     * \li Qt.MiddleButton
+     * \li Qt.RightButton
+     * \endlist
+     */
+    Q_PROPERTY(int buttons READ buttons CONSTANT FINAL)
+
+    /*!
+     * \qmlproperty int WheelEvent::modifiers
+     *
+     * Keyboard modifiers 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 FINAL)
+
+    /*!
+     * \qmlproperty bool WheelEvent::inverted
+     *
+     * 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 FINAL)
+
+    /*!
+     * \qmlproperty bool WheelEvent::accepted
+     *
+     * 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 scenarios.
+     * \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 FINAL)
+
+public:
+    KirigamiWheelEvent(QObject *parent = nullptr);
+    ~KirigamiWheelEvent() override;
+
+    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 WheelFilterItem : public QQuickItem
+{
+    Q_OBJECT
+public:
+    WheelFilterItem(QQuickItem *parent = nullptr);
+};
+
+/*!
+ * \qmltype WheelHandler
+ * \inqmlmodule org.kde.kirigami
+ *
+ * \brief Handles scrolling for a Flickable and 2 attached ScrollBars.
+ *
+ * WheelHandler filters events from a Flickable, a vertical ScrollBar and a horizontal ScrollBar.
+ * Wheel and KeyPress events (when keyNavigationEnabled is true) are used to scroll the Flickable.
+ * When filterMouseEvents is true, WheelHandler blocks mouse button input from reaching the Flickable
+ * and sets the interactive property of the scrollbars to false when touch input is used.
+ *
+ * Wheel event handling behavior:
+ * \list
+ * \li Pixel delta is ignored unless angle delta is not available because pixel delta scrolling is too slow. Qt Widgets doesn't use pixel delta either, so the
+ * default scroll speed should be consistent with Qt Widgets.
+ * \li When using angle delta, scroll using the step increments defined by verticalStepSize and horizontalStepSize.
+ * \li When one of the keyboard modifiers in pageScrollModifiers is used, scroll by pages.
+ * \li When using a device that doesn't use 120 angle delta unit increments such as a touchpad, the verticalStepSize, horizontalStepSize and page increments
+ * (if using page scrolling) will be multiplied by \c{angle delta / 120} to keep scrolling smooth.
+ * \li If scrolling has happened in the last 400ms, use an internal QQuickItem stacked over the Flickable's contentItem to catch wheel events and use those
+ * wheel events to scroll, if possible. This prevents controls inside the Flickable's contentItem that allow scrolling to change the value (e.g., Sliders,
+ * SpinBoxes) from conflicting with scrolling the page. \endlist
+ *
+ * Common usage with a Flickable:
+ *
+ * \quotefile wheelhandler/FlickableUsage.qml
+ *
+ * Common usage inside of a ScrollView template:
+ *
+ * \quotefile wheelhandler/ScrollViewUsage.qml
+ *
+ */
+class WheelHandler : public DummyFlickableScrollHandler, public QQmlParserStatus
+{
+    Q_OBJECT
+    Q_INTERFACES(QQmlParserStatus)
+    QML_ELEMENT
+
+    /*!
+     * \qmlproperty Item WheelHandler::target
+     *
+     * \brief This property holds the Qt Quick Flickable that the WheelHandler will control.
+     */
+    Q_PROPERTY(QQuickItem *target READ target WRITE setTarget NOTIFY targetChanged FINAL)
+
+    /*!
+     * \qmlproperty double WheelHandler::verticalStepSize
+     *
+     * \brief This property holds the vertical step size.
+     *
+     * The default value is equivalent to \c{20 * Qt.styleHints.wheelScrollLines}. This is consistent with the default increment for QScrollArea.
+     *
+     * \sa horizontalStepSize
+     *
+     * \since 5.89
+     */
+    Q_PROPERTY(qreal verticalStepSize READ verticalStepSize WRITE setVerticalStepSize RESET resetVerticalStepSize NOTIFY verticalStepSizeChanged FINAL)
+
+    /*!
+     * \qmlproperty double WheelHandler::horizontalStepSize
+     *
+     * \brief This property holds the horizontal step size.
+     *
+     * The default value is equivalent to \c{20 * Qt.styleHints.wheelScrollLines}. This is consistent with the default increment for QScrollArea.
+     *
+     * \sa verticalStepSize
+     *
+     * \since 5.89
+     */
+    Q_PROPERTY(
+        qreal horizontalStepSize READ horizontalStepSize WRITE setHorizontalStepSize RESET resetHorizontalStepSize NOTIFY horizontalStepSizeChanged FINAL)
+
+    /*!
+     * \qmlproperty int WheelHandler::pageScrollModifiers
+     *
+     * \brief This property holds the keyboard modifiers that will be used to start page scrolling.
+     *
+     * The default value is equivalent to \c{Qt.ControlModifier | Qt.ShiftModifier}. This matches QScrollBar, which uses QAbstractSlider behavior.
+     *
+     * \since 5.89
+     */
+    Q_PROPERTY(Qt::KeyboardModifiers pageScrollModifiers READ pageScrollModifiers WRITE setPageScrollModifiers RESET resetPageScrollModifiers NOTIFY
+                   pageScrollModifiersChanged FINAL)
+
+    /*!
+     * \qmlproperty bool WheelHandler::filterMouseEvents
+     *
+     * \brief This property holds whether the WheelHandler filters mouse events like a Qt Quick Controls ScrollView would.
+     *
+     * Touch events are allowed to flick the view and they make the scrollbars not interactive.
+     *
+     * Mouse events are not allowed to flick the view and they make the scrollbars interactive.
+     *
+     * Hover events on the scrollbars and wheel events on anything also make the scrollbars interactive when this property is set to true.
+     *
+     * The default value is \c false.
+     *
+     * \since 5.89
+     */
+    Q_PROPERTY(bool filterMouseEvents READ filterMouseEvents WRITE setFilterMouseEvents NOTIFY filterMouseEventsChanged FINAL)
+
+    /*!
+     * \qmlproperty bool WheelHandler::keyNavigationEnabled
+     *
+     * \brief This property holds whether the WheelHandler handles keyboard scrolling.
+     *
+     * \list
+     * \li Left arrow scrolls a step to the left.
+     * \li Right arrow scrolls a step to the right.
+     * \li Up arrow scrolls a step upwards.
+     * \li Down arrow scrolls a step downwards.
+     * \li PageUp scrolls to the previous page.
+     * \li PageDown scrolls to the next page.
+     * \li Home scrolls to the beginning.
+     * \li End scrolls to the end.
+     * \li When Alt is held, scroll horizontally when using PageUp, PageDown, Home or End.
+     * \endlist
+     *
+     * The default value is \c false.
+     *
+     * \since 5.89
+     */
+    Q_PROPERTY(bool keyNavigationEnabled READ keyNavigationEnabled WRITE setKeyNavigationEnabled NOTIFY keyNavigationEnabledChanged FINAL)
+
+    /*!
+     * \qmlproperty bool WheelHandler::blockTargetWheel
+     *
+     * \brief This property holds whether the WheelHandler blocks all wheel events from reaching the Flickable.
+     *
+     * When this property is false, scrolling the Flickable with WheelHandler will only block an event from reaching the Flickable if the Flickable is actually
+     * scrolled by WheelHandler.
+     *
+     * \note Wheel events created by touchpad gestures with pixel deltas will always be accepted no matter what. This is because they will cause the Flickable
+     * to jump back to where scrolling started unless the events are always accepted before they reach the Flickable.
+     *
+     * The default value is false.
+     */
+    Q_PROPERTY(bool blockTargetWheel MEMBER m_blockTargetWheel NOTIFY blockTargetWheelChanged FINAL)
+
+    /*!
+     * \qmlproperty bool WheelHandler::scrollFlickableTarget
+     *
+     * \brief This property holds whether the WheelHandler can use wheel events to scroll the Flickable.
+     *
+     * The default value is true.
+     */
+    Q_PROPERTY(bool scrollFlickableTarget MEMBER m_scrollFlickableTarget NOTIFY scrollFlickableTargetChanged FINAL)
+
+public:
+    explicit WheelHandler(QObject *parent = nullptr);
+    ~WheelHandler() override;
+
+    QQuickItem *target() const;
+    void setTarget(QQuickItem *target);
+
+    qreal verticalStepSize() const;
+    void setVerticalStepSize(qreal stepSize);
+    void resetVerticalStepSize();
+
+    qreal horizontalStepSize() const;
+    void setHorizontalStepSize(qreal stepSize);
+    void resetHorizontalStepSize();
+
+    Qt::KeyboardModifiers pageScrollModifiers() const;
+    void setPageScrollModifiers(Qt::KeyboardModifiers modifiers);
+    void resetPageScrollModifiers();
+
+    bool filterMouseEvents() const;
+    void setFilterMouseEvents(bool enabled);
+
+    bool keyNavigationEnabled() const;
+    void setKeyNavigationEnabled(bool enabled);
+
+    /*!
+     * \qmlmethod bool WheelHandler::scrollUp(double stepSize = -1)
+     *
+     * Scroll up one step. If the stepSize parameter is less than 0, the verticalStepSize will be used.
+     *
+     * returns true if the contentItem was moved.
+     *
+     * \since 5.89
+     */
+    Q_INVOKABLE bool scrollUp(qreal stepSize = -1);
+
+    /*!
+     * \qmlmethod bool WheelHandler::scrollDown(double stepSize = -1)
+     *
+     * Scroll down one step. If the stepSize parameter is less than 0, the verticalStepSize will be used.
+     *
+     * returns true if the contentItem was moved.
+     *
+     * \since 5.89
+     */
+    Q_INVOKABLE bool scrollDown(qreal stepSize = -1);
+
+    /*!
+     * \qmlmethod bool WheelHandler::scrollLeft(double stepSize = -1)
+     *
+     * Scroll left one step. If the \a stepSize is less than 0, the horizontalStepSize will be used.
+     *
+     * returns true if the contentItem was moved.
+     *
+     * \since 5.89
+     */
+    Q_INVOKABLE bool scrollLeft(qreal stepSize = -1);
+
+    /*!
+     * \qmlmethod bool WheelHandler::scrollRight(double stepSize = -1)
+     *
+     * Scroll right one step. If the stepSize parameter is less than 0, the horizontalStepSize will be used.
+     *
+     * returns true if the contentItem was moved.
+     *
+     * \since 5.89
+     */
+    Q_INVOKABLE bool scrollRight(qreal stepSize = -1);
+
+Q_SIGNALS:
+    void targetChanged();
+    void verticalStepSizeChanged();
+    void horizontalStepSizeChanged();
+    void pageScrollModifiersChanged();
+    void filterMouseEventsChanged();
+    void keyNavigationEnabledChanged();
+    void blockTargetWheelChanged();
+    void scrollFlickableTargetChanged();
+
+    /*!
+     * \qmlsignal WheelHandler::wheel(WheelEvent wheel)
+     *
+     * \brief This signal is emitted when a wheel event reaches the event filter, just before scrolling is handled.
+     *
+     * Accepting the wheel event in the \c onWheel signal handler prevents scrolling from happening.
+     */
+    void wheel(KirigamiWheelEvent *wheel);
+
+protected:
+    bool eventFilter(QObject *watched, QEvent *event) override;
+
+private Q_SLOTS:
+    void _k_rebindScrollBars();
+
+private:
+    void classBegin() override;
+    void componentComplete() override;
+
+    void setScrolling(bool scrolling);
+    void startInertiaScrolling();
+    bool scrollFlickable(QPointF pixelDelta, QPointF angleDelta = {}, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
+
+    QPointer<QQuickItem> m_flickable;
+    QPointer<QQuickItem> m_verticalScrollBar;
+    QPointer<QQuickItem> m_horizontalScrollBar;
+    QPointer<QQuickItem> m_scrollView;
+    QMetaObject::Connection m_verticalChangedConnection;
+    QMetaObject::Connection m_horizontalChangedConnection;
+    QPointer<QQuickItem> m_filterItem;
+    // Matches QScrollArea and QTextEdit
+    qreal m_defaultPixelStepSize = 20 * QGuiApplication::styleHints()->wheelScrollLines();
+    qreal m_verticalStepSize = m_defaultPixelStepSize;
+    qreal m_horizontalStepSize = m_defaultPixelStepSize;
+    bool m_explicitVStepSize = false;
+    bool m_explicitHStepSize = false;
+    bool m_wheelScrolling = false;
+    constexpr static qreal m_wheelScrollingDuration = 400;
+    bool m_filterMouseEvents = false;
+    bool m_keyNavigationEnabled = false;
+    bool m_blockTargetWheel = false;
+    bool m_scrollFlickableTarget = true;
+    // Same as QXcbWindow.
+    constexpr static Qt::KeyboardModifiers m_defaultHorizontalScrollModifiers = Qt::AltModifier;
+    // Same as QScrollBar/QAbstractSlider.
+    constexpr static Qt::KeyboardModifiers m_defaultPageScrollModifiers = Qt::ControlModifier | Qt::ShiftModifier;
+    Qt::KeyboardModifiers m_pageScrollModifiers = m_defaultPageScrollModifiers;
+    QTimer m_wheelScrollingTimer;
+    KirigamiWheelEvent m_kirigamiWheelEvent;
+    QQueue<QPoint> m_wheelEvents;
+    QQueue<uint64_t> m_timestamps;
+
+
+    // Smooth scrolling
+    QQmlEngine *m_engine = nullptr;
+    QPropertyAnimation m_xScrollAnimation{nullptr, "contentX"};
+    QPropertyAnimation m_yScrollAnimation{nullptr, "contentY"};
+    QPropertyAnimation m_xInertiaScrollAnimation{nullptr, "contentX"};
+    QPropertyAnimation m_yInertiaScrollAnimation{nullptr, "contentY"};
+    bool m_wasTouched = false;
+};
+
+}


=====================================
modules/gui/qt/util/qml/DefaultFlickableScrollHandler.qml
=====================================
@@ -20,11 +20,12 @@ import QtQml
 
 import VLC.MainInterface
 import VLC.Util
+import VLC.Style
 
 FlickableScrollHandler {
     id: handler
 
     scaleFactor: MainCtx.intfScaleFactor
-
-    enabled: !MainCtx.smoothScroll
+    smoothScroll: MainCtx.smoothScroll
+    duration: VLCStyle.duration_long
 }


=====================================
modules/gui/qt/widgets/qml/ExpandGridView.qml
=====================================
@@ -853,7 +853,17 @@ FocusScope {
             }
         }
 
-        DefaultFlickableScrollHandler { }
+        property Component implicitFlickableScrollHandler: DefaultFlickableScrollHandler { }
+
+        // NOTE: This property can be set to null to prevent using a scroll handler:
+        property FlickableScrollHandler scrollHandler: {
+            if (interactive) {
+                // JS ownership:
+                return implicitFlickableScrollHandler.createObject(null, { target: flickable })
+            } else {
+                return null
+            }
+        }
 
         Loader {
             id: headerItemLoader


=====================================
modules/gui/qt/widgets/qml/ListViewExt.qml
=====================================
@@ -657,7 +657,17 @@ ListView {
         }
     }
 
-    DefaultFlickableScrollHandler { }
+    property Component implicitFlickableScrollHandler: DefaultFlickableScrollHandler { }
+
+    // NOTE: This property can be set to null to prevent using a scroll handler:
+    property FlickableScrollHandler scrollHandler: {
+        if (interactive) {
+            // JS ownership:
+            return implicitFlickableScrollHandler.createObject(null, { target: root })
+        } else {
+            return null
+        }
+    }
 
     // FIXME: This is probably not useful anymore.
     Connections {



View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/c01a7eb9559c1804930e1d836997e8a2c8ef3645...2becd9e1ff892f2f2159ab31d237f8450a656a45

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/c01a7eb9559c1804930e1d836997e8a2c8ef3645...2becd9e1ff892f2f2159ab31d237f8450a656a45
You're receiving this email because of your account on code.videolan.org.




More information about the vlc-commits mailing list