Commit c6f8abf0 authored by Noah Davis's avatar Noah Davis 🌵
Browse files

Add ScrollHandler

parent 8c2faf5b
Pipeline #91578 passed with stage
in 1 minute and 26 seconds
......@@ -33,6 +33,7 @@ kirigami_add_tests(
tst_avatar.qml
tst_theme.qml
tst_mnemonicdata.qml
tst_scrollhandler.qml
pagepool/tst_pagepool.qml
pagepool/tst_layers.qml
)
......
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import org.kde.kirigami 2.20 as Kirigami
import QtTest 1.15
TestCase {
id: root
readonly property real hstep: scrollHandler.horizontalStepMode == Kirigami.ScrollHandler.PixelStepMode ?
scrollHandler.horizontalStepSize : scrollHandler.horizontalStepSize * flickable.contentWidth
readonly property real vstep: scrollHandler.verticalStepMode == Kirigami.ScrollHandler.PixelStepMode ?
scrollHandler.verticalStepSize : scrollHandler.verticalStepSize * flickable.contentHeight
readonly property real pageWidth: flickable.width - flickable.leftMargin - flickable.rightMargin
readonly property real pageHeight: flickable.height - flickable.topMargin - flickable.bottomMargin
readonly property real contentWidth: flickable.contentWidth
readonly property real contentHeight: flickable.contentHeight
name: "ScrollHandlerTests"
visible: true
when: windowShown
width: 600 + flickable.leftMargin + flickable.rightMargin
height: 600 + flickable.topMargin + flickable.bottomMargin
function test_WheelScroll() {
let x = flickable.contentX
let y = flickable.contentY
mouseWheel(flickable, flickable.leftMargin + 10, 10, -120, -120, Qt.NoButton)
verify(flickable.contentX === x + hstep, "+xTick")
x = flickable.contentX
verify(flickable.contentY === y + vstep, "+yTick")
y = flickable.contentY
mouseWheel(flickable, flickable.leftMargin + 10, 10, 120, 120, Qt.NoButton)
verify(flickable.contentX === x - hstep, "-xTick")
x = flickable.contentX
verify(flickable.contentY === y - vstep, "-yTick")
y = flickable.contentY
mouseWheel(flickable, flickable.leftMargin + 10, 10, 0, -120, Qt.NoButton, scrollHandler.horizontalScrollModifiers)
verify(flickable.contentX === x + hstep, "+h_yTick")
x = flickable.contentX
verify(flickable.contentY === y, "no +yTick")
mouseWheel(flickable, flickable.leftMargin + 10, 10, 0, 120, Qt.NoButton, scrollHandler.horizontalScrollModifiers)
verify(flickable.contentX === x - hstep, "-h_yTick")
x = flickable.contentX
verify(flickable.contentY === y, "no -yTick")
mouseWheel(flickable, flickable.leftMargin + 10, 10, -120, -120, Qt.NoButton, scrollHandler.pageScrollModifiers)
verify(flickable.contentX === x + pageWidth, "+xPage")
x = flickable.contentX
verify(flickable.contentY === y + pageHeight, "+yPage")
y = flickable.contentY
mouseWheel(flickable, flickable.leftMargin + 10, 10, 120, 120, Qt.NoButton, scrollHandler.pageScrollModifiers)
verify(flickable.contentX === x - pageWidth, "-xPage")
x = flickable.contentX
verify(flickable.contentY === y - pageHeight, "-yPage")
y = flickable.contentY
mouseWheel(flickable, flickable.leftMargin + 10, 10, 0, -120, Qt.NoButton,
scrollHandler.horizontalScrollModifiers | scrollHandler.pageScrollModifiers)
verify(flickable.contentX === x + pageWidth, "+h_yPage")
x = flickable.contentX
verify(flickable.contentY === y, "no +yPage")
mouseWheel(flickable, flickable.leftMargin + 10, 10, 0, 120, Qt.NoButton,
scrollHandler.horizontalScrollModifiers | scrollHandler.pageScrollModifiers)
verify(flickable.contentX === x - pageWidth, "-h_yPage")
x = flickable.contentX
verify(flickable.contentY === y, "no -yPage")
}
function test_KeyboardScroll() {
const originalX = flickable.contentX
const originalY = flickable.contentY
let x = originalX
let y = originalY
keyClick(Qt.Key_Right)
verify(flickable.contentX === x + hstep, "Key_Right")
x = flickable.contentX
keyClick(Qt.Key_Left)
verify(flickable.contentX === x - hstep, "Key_Left")
x = flickable.contentX
keyClick(Qt.Key_Down)
verify(flickable.contentY === y + vstep, "Key_Down")
y = flickable.contentY
keyClick(Qt.Key_Up)
verify(flickable.contentY === y - vstep, "Key_Up")
y = flickable.contentY
keyClick(Qt.Key_PageDown)
verify(flickable.contentY === y + pageHeight, "Key_PageDown")
y = flickable.contentY
keyClick(Qt.Key_PageUp)
verify(flickable.contentY === y - pageHeight, "Key_PageUp")
y = flickable.contentY
keyClick(Qt.Key_End)
verify(flickable.contentY === contentHeight - pageHeight, "Key_End")
y = flickable.contentY
keyClick(Qt.Key_Home)
verify(flickable.contentY === originalY, "Key_Home")
y = flickable.contentY
keyClick(Qt.Key_PageDown, scrollHandler.horizontalScrollModifiers)
verify(flickable.contentX === x + pageWidth, "h_Key_PageDown")
x = flickable.contentX
keyClick(Qt.Key_PageUp, scrollHandler.horizontalScrollModifiers)
verify(flickable.contentX === x - pageWidth, "h_Key_PageUp")
x = flickable.contentX
keyClick(Qt.Key_End, scrollHandler.horizontalScrollModifiers)
verify(flickable.contentX === contentWidth - pageWidth, "h_Key_End")
x = flickable.contentX
keyClick(Qt.Key_Home, scrollHandler.horizontalScrollModifiers)
verify(flickable.contentX === originalX, "h_Key_Home")
}
function test_MouseFlick() {
const x = flickable.contentX
const y = flickable.contentY
mousePress(flickable, flickable.leftMargin + 10, 10)
mouseMove(flickable)
mouseRelease(flickable)
verify(flickable.contentX === x && flickable.contentY === y, "not moved")
}
// NOTE: Unfortunately, this test can't work. Flickable does not handle touch events, only mouse events synthesized from touch events
// TODO: Uncomment if Flickable is ever able to use touch events.
/*function test_TouchFlick() {
const x = flickable.contentX, y = flickable.contentY
let touch = touchEvent(flickable)
// Press on center.
touch.press(0, flickable)
touch.commit()
// Move a bit towards top left.
touch.move(0, flickable, flickable.width/2 - 50, flickable.height/2 - 50)
touch.commit()
// Release at the spot we moved to.
touch.release(0, flickable, flickable.width/2 - 50, flickable.height/2 - 50)
touch.commit()
verify(flickable.contentX !== x || flickable.contentY !== y, "moved")
}*/
function test_MouseScrollBars() {
const x = flickable.contentX, y = flickable.contentY
mousePress(flickable, flickable.leftMargin + 10, 10)
mouseMove(flickable)
const interactive = flickable.QQC2.ScrollBar.vertical.interactive || flickable.QQC2.ScrollBar.horizontal.interactive
mouseRelease(flickable)
verify(interactive, "interactive scrollbars")
}
function test_TouchScrollBars() {
const x = flickable.contentX, y = flickable.contentY
let touch = touchEvent(flickable)
touch.press(0, flickable, flickable.leftMargin + 10, 10)
touch.commit()
touch.move(0, flickable)
touch.commit()
const interactive = flickable.QQC2.ScrollBar.vertical.interactive || flickable.QQC2.ScrollBar.horizontal.interactive
touch.release(0, flickable)
touch.commit()
verify(!interactive, "no interactive scrollbars")
}
Flickable {
id: flickable
focus: true
anchors.fill: parent
Kirigami.ScrollHandler {
id: scrollHandler
target: flickable
verticalScrollBar: flickable.QQC2.ScrollBar.vertical
horizontalScrollBar: flickable.QQC2.ScrollBar.horizontal
filterMouseEvents: true
keyNavigationEnabled: true
}
leftMargin: !QQC2.ScrollBar.vertical.mirrored ? 0 : QQC2.ScrollBar.vertical.width
rightMargin: QQC2.ScrollBar.vertical.mirrored ? 0 : QQC2.ScrollBar.vertical.width
bottomMargin: QQC2.ScrollBar.horizontal.height
QQC2.ScrollBar.vertical: QQC2.ScrollBar {
parent: flickable.parent
anchors.right: flickable.right
anchors.top: flickable.top
anchors.bottom: flickable.bottom
anchors.bottomMargin: flickable.QQC2.ScrollBar.horizontal.height
active: flickable.QQC2.ScrollBar.vertical.active
stepSize: scrollHandler.verticalStepSize / flickable.contentHeight
}
QQC2.ScrollBar.horizontal: QQC2.ScrollBar {
parent: flickable.parent
anchors.left: flickable.left
anchors.right: flickable.right
anchors.bottom: flickable.bottom
anchors.rightMargin: flickable.QQC2.ScrollBar.vertical.width
active: flickable.QQC2.ScrollBar.horizontal.active
stepSize: scrollHandler.horizontalStepSize / flickable.contentWidth
}
contentWidth: grid.implicitWidth
contentHeight: grid.implicitHeight
Grid {
id: grid
columns: Math.sqrt(visibleChildren.length)
Repeater {
model: 500
delegate: Rectangle {
implicitHeight: 60
implicitWidth: 60
gradient: Gradient {
orientation: index % 2 ? Gradient.Vertical : Gradient.Horizontal
GradientStop { position: 0; color: Qt.rgba(Math.random(),Math.random(),Math.random(),1) }
GradientStop { position: 1; color: Qt.rgba(Math.random(),Math.random(),Math.random(),1) }
}
}
}
QQC2.Button {
id: enableSliderButton
width: 60
height: 60
contentItem: QQC2.Label {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: "Enable Slider"
wrapMode: Text.Wrap
}
checked: true
}
QQC2.Slider {
wheelEnabled: !scrollHandler.scrolling
enabled: enableSliderButton.checked
width: 60
height: 60
}
Repeater {
model: 500
delegate: Rectangle {
implicitHeight: 60
implicitWidth: 60
gradient: Gradient {
orientation: index % 2 ? Gradient.Vertical : Gradient.Horizontal
GradientStop { position: 0; color: Qt.rgba(Math.random(),Math.random(),Math.random(),1) }
GradientStop { position: 1; color: Qt.rgba(Math.random(),Math.random(),Math.random(),1) }
}
}
}
}
}
}
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import org.kde.kirigami 2.20 as Kirigami
QQC2.ApplicationWindow {
id: root
width: 600 + flickable.leftMargin + flickable.rightMargin
height: 600 + flickable.topMargin + flickable.bottomMargin
color: palette.window
Flickable {
id: flickable
anchors.fill: parent
Kirigami.ScrollHandler {
id: scrollHandler
target: flickable
verticalScrollBar: flickable.QQC2.ScrollBar.vertical
horizontalScrollBar: flickable.QQC2.ScrollBar.horizontal
filterMouseEvents: true
filterKeyEvents: true
}
leftMargin: QQC2.ScrollBar.vertical.mirrored && QQC2.ScrollBar.vertical.visible ?
QQC2.ScrollBar.vertical.implicitWidth : 0
rightMargin: !QQC2.ScrollBar.vertical.mirrored && QQC2.ScrollBar.vertical.visible ?
QQC2.ScrollBar.vertical.implicitWidth : 0
bottomMargin: QQC2.ScrollBar.horizontal.visible ?
QQC2.ScrollBar.horizontal.implicitHeight : 0
QQC2.ScrollBar.vertical: QQC2.ScrollBar {
parent: flickable.parent
anchors.right: flickable.right
anchors.top: flickable.top
anchors.bottom: flickable.bottom
anchors.bottomMargin: flickable.QQC2.ScrollBar.horizontal.height
active: flickable.QQC2.ScrollBar.vertical.active
stepSize: scrollHandler.verticalStepSize / flickable.contentHeight
}
QQC2.ScrollBar.horizontal: QQC2.ScrollBar {
parent: flickable.parent
anchors.left: flickable.left
anchors.right: flickable.right
anchors.bottom: flickable.bottom
anchors.rightMargin: flickable.QQC2.ScrollBar.vertical.width
active: flickable.QQC2.ScrollBar.horizontal.active
stepSize: scrollHandler.horizontalStepSize / flickable.contentWidth
}
contentWidth: grid.implicitWidth
contentHeight: grid.implicitHeight
Grid {
id: grid
columns: Math.sqrt(visibleChildren.length)
Repeater {
model: 1000
delegate: Rectangle {
implicitHeight: 60
implicitWidth: 60
gradient: Gradient {
orientation: index % 2 ? Gradient.Vertical : Gradient.Horizontal
GradientStop { position: 0; color: Qt.rgba(Math.random(),Math.random(),Math.random(),1) }
GradientStop { position: 1; color: Qt.rgba(Math.random(),Math.random(),Math.random(),1) }
}
}
}
}
}
}
import QtQuick 2.15
import QtQuick.Templates 2.15 as T
import QtQuick.Controls 2.15 as QQC2
import org.kde.kirigami 2.20 as Kirigami
T.ScrollView {
id: control
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
contentWidth + leftPadding + rightPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
contentHeight + topPadding + bottomPadding)
leftPadding: mirrored && T.ScrollBar.vertical.visible && !Kirigami.Settings.isMobile ? T.ScrollBar.vertical.width : 0
rightPadding: !mirrored && T.ScrollBar.vertical.visible && !Kirigami.Settings.isMobile ? T.ScrollBar.vertical.width : 0
bottomPadding: T.ScrollBar.horizontal.visible && !Kirigami.Settings.isMobile ? T.ScrollBar.horizontal.height : 0
data: [
Kirigami.ScrollHandler {
id: scrollHandler
target: control.contentItem
verticalScrollBar: control.T.ScrollBar.vertical
horizontalScrollBar: control.T.ScrollBar.horizontal
}
]
T.ScrollBar.vertical: QQC2.ScrollBar {
parent: control
x: control.mirrored ? 0 : control.width - width
y: control.topPadding
height: control.availableHeight
active: control.T.ScrollBar.vertical.active
}
T.ScrollBar.horizontal: QQC2.ScrollBar {
parent: control
x: control.leftPadding
y: control.height - height
width: control.availableWidth
active: control.T.ScrollBar.horizontal.active
}
//contentWidth: grid.implicitWidth
// contentHeight: grid.implicitHeight
contentItem: Flickable {
Grid {
id: grid
columns: Math.sqrt(repeater.count)
Repeater {
id: repeater
model: 1000
delegate: Rectangle {
implicitWidth: 60
implicitHeight: 60
gradient: Gradient {
orientation: index % 2 ? Gradient.Vertical : Gradient.Horizontal
GradientStop { position: 0; color: Qt.rgba(Math.random(),Math.random(),Math.random(),1) }
GradientStop { position: 1; color: Qt.rgba(Math.random(),Math.random(),Math.random(),1) }
}
}
}
}
}
}
......@@ -35,6 +35,7 @@ set(kirigami_SRCS
toolbarlayoutdelegate.cpp
sizegroup.cpp
spellcheckinghint.cpp
scrollhandler.cpp
scenegraph/managedtexturenode.cpp
scenegraph/shadowedrectanglenode.cpp
scenegraph/shadowedrectanglematerial.cpp
......
......@@ -19,6 +19,7 @@
#include "pagepool.h"
#include "pagerouter.h"
#include "scenepositionattached.h"
#include "scrollhandler.h"
#include "settings.h"
#include "shadowedrectangle.h"
#include "shadowedtexture.h"
......@@ -312,6 +313,9 @@ void KirigamiPlugin::registerTypes(const char *uri)
qmlRegisterType(componentUrl(QStringLiteral("NavigationTabBar.qml")), uri, 2, 19, "NavigationTabBar");
qmlRegisterType(componentUrl(QStringLiteral("NavigationTabButton.qml")), uri, 2, 19, "NavigationTabButton");
// 2.20
qmlRegisterType<ScrollHandler>(uri, 2, 20, "ScrollHandler");
qmlProtectModule(uri, 2);
}
......
This diff is collapsed.
/* SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
* SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
* SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#pragma once
#include <QObject>
#include <QPoint>
#include <QQuickItem>
#include <QtQml>
#include <memory>
class KirigamiWheelEvent;
class ScrollHandlerPrivate;
/**
* @brief Handles scrolling for a Flickable and 2 ScrollBars.
*
* ScrollHandler 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, ScrollHandler 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:
*
* - If there is a pixel delta, use that instead of angle delta.
* - When using angle delta, scroll using the step increments defined via `verticalStepMode`, `horizontalStepMode`, `verticalStepSize` and `horizontalStepSize`.
* - When one of the keyboard modifiers in `horizontalScrollModifiers` is used, use the Y axis angle delta to scroll horizontally.
* If horizontal scrolling is not possible and `pageScrollModifiers` uses the same modifier as the one being used for horizontal scrolling,
* the Flickable will scroll vertically by pages instead.
* - When one of the keyboard modifiers in `pageScrollModifiers` is used, scroll by pages.
* When using a device that doesn't use 120 angle delta unit increments such as a touchpad,
* this will increase the speed of scrolling rather than skipping whole pages at once.
* - When vertical scrolling is not possible, automatically use the Y axis angle delta to scroll horizontally.
*
* Common usage with a Flickable:
*
* @include scrollhandler/FlickableUsage.qml
*
* Common usage with a ScrollView:
*
* @include scrollhandler/ScrollViewUsage.qml
*
* @sa filterMouseEvents, keyNavigationEnabled, verticalStepMode, horizontalStepMode, verticalStepSize, horizontalStepSize, horizontalScrollModifiers,
* pageScrollModifiers
*
* @since KDE Frameworks 5.88, org.kde.kirigami 2.20
*/
class ScrollHandler : public QObject
{
Q_OBJECT
/**
* @brief This property holds the Qt Quick Flickable that the ScrollHandler will control.
*
* @sa verticalScrollBar, horizontalScrollBar
*/
Q_PROPERTY(QQuickItem *target READ target WRITE setTarget NOTIFY targetChanged FINAL)
/**
* @brief This property holds the vertical Qt Quick Controls ScrollBar that the ScrollHandler will control.
*
* @sa target, horizontalScrollBar
*/
Q_PROPERTY(QQuickItem *verticalScrollBar READ verticalScrollBar
WRITE setVerticalScrollBar NOTIFY verticalScrollBarChanged FINAL)
/**
* @brief This property holds the horizontal Qt Quick Controls ScrollBar that the ScrollHandler will control.
*
* @sa target, verticalScrollBar
*/
Q_PROPERTY(QQuickItem *horizontalScrollBar READ horizontalScrollBar
WRITE setHorizontalScrollBar NOTIFY horizontalScrollBarChanged FINAL)
/**
* @brief This property holds the vertical step mode.
*
* The default value is `PixelStepMode`.
*
* @sa verticalStepSize, StepMode
*/
Q_PROPERTY(StepMode verticalStepMode READ verticalStepMode
WRITE setVerticalStepMode RESET resetVerticalStepMode
NOTIFY verticalStepModeChanged FINAL)
/**
* @brief This property holds the horizontal step mode.
*
* The default value is `PixelStepMode`.
*
* @sa horizontalStepSize, StepMode
*/
Q_PROPERTY(StepMode horizontalStepMode READ horizontalStepMode
WRITE setHorizontalStepMode RESET resetHorizontalStepMode
NOTIFY horizontalStepModeChanged FINAL)
/**
* @brief This property holds the vertical step size.
*
* When `verticalStepMode` is set to `PixelStepMode`, the default value is equivalent to `20 * Qt.styleHints.wheelScrollLines`. This is consistent with the default increment for QScrollArea.
*
* When `verticalStepMode` is set to `PercentStepMode`, the default value is `0.1` (10%). This is consistent with the default increment for Qt Quick Controls ScrollBar.
*
* @sa verticalStepMode, StepMode
*/
Q_PROPERTY(qreal verticalStepSize READ verticalStepSize
WRITE setVerticalStepSize RESET resetVerticalStepSize
NOTIFY verticalStepSizeChanged FINAL)
/**
* @brief This property holds the horizontal step size.
*
* When `horizontalStepMode` is set to `PixelStepMode`, the default value is equivalent to `20 * Qt.styleHints.wheelScrollLines`. This is consistent with the default increment for QScrollArea.
*
* When `horizontalStepMode` is set to `PercentStepMode`, the default value is `0.1` (10%). This is consistent with the default increment for Qt Quick Controls ScrollBar.
*
* @sa horizontalStepMode, StepMode
*/
Q_PROPERTY(qreal horizontalStepSize READ horizontalStepSize
WRITE setHorizontalStepSize RESET resetHorizontalStepSize
NOTIFY horizontalStepSizeChanged FINAL)
/**
* @brief This property holds whether the ScrollHandler is scrolling.
*
* Scrolling is actually instant, so to make this property useful, `scrollingTimeoutDelay` sets the delay before this is set to false again.
*
* @sa scrollingTimeoutDelay
*/
Q_PROPERTY(bool scrolling READ scrolling NOTIFY scrollingChanged FINAL)
/**
* @brief This property holds the delay before the `scrolling` property is set to false again.
*
* The default value is `400` milliseconds.
*
* @sa scrolling
*/
Q_PROPERTY(int scrollingTimeoutDelay READ scrollingTimeoutDelay
WRITE setScrollingTimeoutDelay RESET resetScrollingTimeoutDelay
NOTIFY scrollingTimeoutDelayChanged FINAL)
/**
* @brief This property holds the keyboard modifiers that will be used to start horizontal scrolling using the Y axis pixelDelta or angleDelta.
*
* When horizontal scrolling isn't possible, vertical page scrolling will be used instead when `pageScrollModifiers` and `horizontalScrollModifiers` share the modifier being used.
*
* The wheel event will always be accepted when horizontal scrolling is used to prevent the Flickable from using the wheel event for vertical scrolling.
*
* The default value is equivalent to `Qt.ShiftModifier`.
*
* @sa pageScrollModifiers
*/
Q_PROPERTY(Qt::KeyboardModifiers horizontalScrollModifiers READ horizontalScrollModifiers
WRITE setHorizontalScrollModifiers RESET resetHorizontalScrollModifiers
NOTIFY horizontalScrollModifiersChanged FINAL)
/**
* @brief This property holds the keyboard modifiers that will be used to start page scrolling.
*
* The default value is equivalent to `Qt.ControlModifier | Qt.ShiftModifier`. This matches QScrollBar, which uses QAbstractSlider behavior.
*
* @sa horizontalScrollModifiers
*/
Q_PROPERTY(Qt::KeyboardModifiers pageScrollModifiers READ pageScrollModifiers
WRITE setPageScrollModifiers RESET resetPageScrollModifiers
NOTIFY pageScrollModifiersChanged FINAL)
/**
* @brief This property holds whether the ScrollHandler filters mouse events like a Qt Quick Controls ScrollView would.
*
* Touch events are allowed to flick the view and make the scrollbars not interactive.
*
* Mouse events are not allowed to flick the view and make the scrollbars interactive.
*
* The default value is `false`.