[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