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

WheelHandler: Improve consistency with scrolling in Qt Widgets, add more properties

Scrolling now behaves like Qt Widgets, even for touchpads.

When scrolling starts, a hidden item used to filter wheel events from children of the flickable is activated. It turns off 400ms after scrolling has stoppped.

Because of issues with the way Flickable handles wheel events created by touchpad gestures on Wayland, all wheel events created by that source are always accepted. Otherwise, the view sometimes snaps back to where it was when scrolling began.

horizontal and vertical scroll speed can now be customized via horizontalStepSize and verticalStepSize

Wheel events on scrollbars attached to the flickable are now handled.

filterMouseEvents can be used to handle mouse and touch input like a scrollview without having to use a scrollview.

keyNavigationEnabled can be used to enable scrolling via keyboard navigation, including arrow key navigaiton, PageUp, PageDown, Home, End and horizontal versions of the last 4 when Alt is held.

Page scrolling is now much smoother with a touchpad.

Alt can be used for horizontal scrolling on both X11 and Wayland.

There are now invokable functions for scrolling in a specific direction by a specific amount that return true if scrolling actually happened.

I've added autotests to help ensure that functionality doesn't regress and some manual tests which will allow you to test the changes locally. Currently, qqc2-desktop-style horizontal scrollbars are bugged, but that's completely unrelated to the changes in this MR.

The global filter has been removed and replaced with instance specific filters. This is necessary because each filter needs information specific to the instance filtering the events. There isn't really a usecase for allowing multiple Kirigami WheelHandlers anymore now that we can use Qt Quick WheelHandler.

Added Qt Widgets comparison tests.

Remove auto horizontal scroll, as requested by ngraham.

Workaround xcb using Alt to transpose deltas and not wayland by not transposing when Alt is held with xcb.
parent 52ce01c7
......@@ -35,6 +35,10 @@ kirigami_add_tests(
tst_mnemonicdata.qml
pagepool/tst_pagepool.qml
pagepool/tst_layers.qml
wheelhandler/tst_filterMouseEvents.qml
wheelhandler/tst_invokables.qml
wheelhandler/tst_onWheel.qml
wheelhandler/tst_scrolling.qml
)
set_tests_properties(tst_theme.qml PROPERTIES
......
/* 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
*/
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
Flickable {
id: flickable
property real cellWidth: 60
property real cellHeight: 60
readonly property QQC2.Button enableSliderButton: enableSliderButton
readonly property QQC2.Slider slider: slider
implicitWidth: cellWidth * 10 + leftMargin + rightMargin
implicitHeight: cellHeight * 10 + topMargin + bottomMargin
contentWidth: contentItem.childrenRect.width
contentHeight: contentItem.childrenRect.height
Grid {
id: grid
columns: Math.sqrt(visibleChildren.length)
Repeater {
model: 500
delegate: Rectangle {
implicitWidth: flickable.cellWidth
implicitHeight: flickable.cellHeight
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: flickable.cellWidth
height: flickable.cellHeight
contentItem: QQC2.Label {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: "Enable Slider"
wrapMode: Text.Wrap
}
checked: true
}
QQC2.Slider {
id: slider
enabled: enableSliderButton.checked
width: flickable.cellWidth
height: flickable.cellHeight
}
Repeater {
model: 500
delegate: Rectangle {
implicitWidth: flickable.cellWidth
implicitHeight: flickable.cellHeight
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) }
}
}
}
}
}
/* 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
*/
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import org.kde.kirigami 2.19 as Kirigami
ContentFlickable {
id: flickable
leftMargin: QQC2.ScrollBar.vertical && QQC2.ScrollBar.vertical.visible && QQC2.ScrollBar.vertical.mirrored ? QQC2.ScrollBar.vertical.width : 0
rightMargin: QQC2.ScrollBar.vertical && QQC2.ScrollBar.vertical.visible && !QQC2.ScrollBar.vertical.mirrored ? QQC2.ScrollBar.vertical.width : 0
bottomMargin: QQC2.ScrollBar.horizontal && QQC2.ScrollBar.horizontal.visible ? QQC2.ScrollBar.horizontal.height : 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 ? flickable.QQC2.ScrollBar.horizontal.height : anchors.margins
active: flickable.QQC2.ScrollBar.vertical.active
}
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 ? flickable.QQC2.ScrollBar.vertical.width : anchors.margins
active: flickable.QQC2.ScrollBar.horizontal.active
}
}
/* 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
*/
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import org.kde.kirigami 2.19 as Kirigami
import QtTest 1.15
TestCase {
id: root
name: "WheelHandler filterMouseEvents"
visible: true
when: windowShown
width: flickable.implicitWidth
height: flickable.implicitHeight
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")
}
ScrollableFlickable {
id: flickable
anchors.fill: parent
Kirigami.WheelHandler {
id: wheelHandler
target: flickable
filterMouseEvents: true
}
}
}
/* 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
*/
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import org.kde.kirigami 2.19 as Kirigami
import QtTest 1.15
TestCase {
id: root
readonly property real hstep: wheelHandler.horizontalStepSize
readonly property real vstep: wheelHandler.verticalStepSize
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
property alias wheelHandler: wheelHandler
property alias flickable: flickable
name: "WheelHandler invokable functions"
visible: true
when: windowShown
width: flickable.implicitWidth
height: flickable.implicitHeight
function test_Invokables() {
const originalX = flickable.contentX
const originalY = flickable.contentY
let x = originalX
let y = originalY
wheelHandler.scrollRight()
verify(flickable.contentX === x + hstep, "scrollRight()")
x = flickable.contentX
wheelHandler.scrollLeft()
verify(flickable.contentX === x - hstep, "scrollLeft()")
x = flickable.contentX
wheelHandler.scrollDown()
verify(flickable.contentY === y + vstep, "scrollDown()")
y = flickable.contentY
wheelHandler.scrollUp()
verify(flickable.contentY === y - vstep, "scrollUp()")
y = flickable.contentY
wheelHandler.scrollRight(101)
verify(flickable.contentX === x + 101, "scrollRight(101)")
x = flickable.contentX
wheelHandler.scrollLeft(101)
verify(flickable.contentX === x - 101, "scrollLeft(101)")
x = flickable.contentX
wheelHandler.scrollDown(101)
verify(flickable.contentY === y + 101, "scrollDown(101)")
y = flickable.contentY
wheelHandler.scrollUp(101)
verify(flickable.contentY === y - 101, "scrollUp(101)")
y = flickable.contentY
}
ScrollableFlickable {
id: flickable
anchors.fill: parent
Kirigami.WheelHandler {
id: wheelHandler
target: flickable
}
}
}
/* 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
*/
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import org.kde.kirigami 2.19 as Kirigami
import QtTest 1.15
TestCase {
id: root
name: "WheelHandler onWheel"
visible: true
when: windowShown
width: 600
height: 600
function test_onWheel() {
let contentX = flickable.contentX
let contentY = flickable.contentY
let contentWidth = flickable.contentWidth
let contentHeight = flickable.contentHeight
// grow
mouseWheel(flickable, flickable.leftMargin, 0, -120, -120, Qt.NoButton, Qt.ControlModifier)
verify(flickable.contentWidth === contentWidth - 120, "-xDelta")
contentWidth = flickable.contentWidth
verify(flickable.contentHeight === contentHeight - 120, "-yDelta")
contentHeight = flickable.contentHeight
// check if accepting the event prevents scrolling
verify(flickable.contentX === contentX && flickable.contentY === contentY, "not moved")
// shrink
mouseWheel(flickable, flickable.leftMargin, 0, 120, 120, Qt.NoButton, Qt.ControlModifier)
verify(flickable.contentWidth === contentWidth + 120, "+xDelta")
verify(flickable.contentHeight === contentHeight + 120, "+yDelta")
// check if accepting the event prevents scrolling
verify(flickable.contentX === contentX && flickable.contentY === contentY, "not moved")
}
Rectangle {
anchors.fill: parent
color: "black"
}
Flickable {
id: flickable
anchors.fill: parent
Kirigami.WheelHandler {
id: wheelHandler
target: flickable
onWheel: {
if (wheel.modifiers & Qt.ControlModifier) {
// Adding delta is the simplest way to change size without running into floating point number issues
// NOTE: Not limiting minimum content size to a size greater than the Flickable size makes it so
// wheel events stop coming to onWheel when the content size is the size of the flickable or smaller.
// Maybe it's a Flickable issue? Koko had the same problem with Flickable.
flickable.contentWidth = Math.max(720, flickable.contentWidth + wheel.angleDelta.x)
flickable.contentHeight = Math.max(720, flickable.contentHeight + wheel.angleDelta.y)
wheel.accepted = true
}
}
}
leftMargin: QQC2.ScrollBar.vertical && QQC2.ScrollBar.vertical.visible && QQC2.ScrollBar.vertical.mirrored ? QQC2.ScrollBar.vertical.width : 0
rightMargin: QQC2.ScrollBar.vertical && QQC2.ScrollBar.vertical.visible && !QQC2.ScrollBar.vertical.mirrored ? QQC2.ScrollBar.vertical.width : 0
bottomMargin: QQC2.ScrollBar.horizontal && QQC2.ScrollBar.horizontal.visible ? QQC2.ScrollBar.horizontal.height : 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 ? flickable.QQC2.ScrollBar.horizontal.height : anchors.margins
active: flickable.QQC2.ScrollBar.vertical.active
}
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 ? flickable.QQC2.ScrollBar.vertical.width : anchors.margins
active: flickable.QQC2.ScrollBar.horizontal.active
}
contentWidth: 1200
contentHeight: 1200
Rectangle {
id: contentRect
anchors.fill: parent
gradient: Gradient.WideMatrix
border.color: Qt.rgba(0,0,0,0.5)
border.width: 2
}
}
QQC2.Label {
anchors.centerIn: parent
leftPadding: 4
rightPadding: 4
wrapMode: Text.Wrap
color: "white"
text: `Rectangle size: ${contentRect.width}x${contentRect.height}`
background: Rectangle {
color: "black"
}
}
}
/* 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
*/
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import org.kde.kirigami 2.19 as Kirigami
import QtTest 1.15
TestCase {
id: root
readonly property real hstep: wheelHandler.horizontalStepSize
readonly property real vstep: wheelHandler.verticalStepSize
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
property alias wheelHandler: wheelHandler
property alias flickable: flickable
name: "WheelHandler scrolling"
visible: true
when: windowShown
width: flickable.implicitWidth
height: flickable.implicitHeight
function wheelScrolling(angleDelta = 120) {
let x = flickable.contentX
let y = flickable.contentY
const angleDeltaFactor = angleDelta / 120
mouseWheel(flickable, flickable.leftMargin, 0, -angleDelta, -angleDelta, Qt.NoButton)
verify(flickable.contentX === x + hstep * angleDeltaFactor, "+xTick")
x = flickable.contentX
verify(flickable.contentY === y + vstep * angleDeltaFactor, "+yTick")
y = flickable.contentY
mouseWheel(flickable, flickable.leftMargin, 0, angleDelta, angleDelta, Qt.NoButton)
verify(flickable.contentX === x - hstep * angleDeltaFactor, "-xTick")
x = flickable.contentX
verify(flickable.contentY === y - vstep * angleDeltaFactor, "-yTick")
y = flickable.contentY
if (Qt.platform.pluginName !== "xcb") {
mouseWheel(flickable, flickable.leftMargin, 0, 0, -angleDelta, Qt.NoButton, Qt.AltModifier)
verify(flickable.contentX === x + hstep * angleDeltaFactor, "+h_yTick")
x = flickable.contentX
verify(flickable.contentY === y, "no +yTick")
mouseWheel(flickable, flickable.leftMargin, 0, 0, angleDelta, Qt.NoButton, Qt.AltModifier)
verify(flickable.contentX === x - hstep * angleDeltaFactor, "-h_yTick")
x = flickable.contentX
verify(flickable.contentY === y, "no -yTick")
}
mouseWheel(flickable, flickable.leftMargin, 0, -angleDelta, -angleDelta, Qt.NoButton, wheelHandler.pageScrollModifiers)
verify(flickable.contentX === x + pageWidth * angleDeltaFactor, "+xPage")
x = flickable.contentX
verify(flickable.contentY === y + pageHeight * angleDeltaFactor, "+yPage")
y = flickable.contentY
mouseWheel(flickable, flickable.leftMargin, 0, angleDelta, angleDelta, Qt.NoButton, wheelHandler.pageScrollModifiers)
verify(flickable.contentX === x - pageWidth * angleDeltaFactor, "-xPage")
x = flickable.contentX
verify(flickable.contentY === y - pageHeight * angleDeltaFactor, "-yPage")
y = flickable.contentY
if (Qt.platform.pluginName !== "xcb") {
mouseWheel(flickable, flickable.leftMargin, 0, 0, -angleDelta, Qt.NoButton,
Qt.AltModifier | wheelHandler.pageScrollModifiers)
verify(flickable.contentX === x + pageWidth * angleDeltaFactor, "+h_yPage")
x = flickable.contentX
verify(flickable.contentY === y, "no +yPage")
mouseWheel(flickable, flickable.leftMargin, 0, 0, angleDelta, Qt.NoButton,
Qt.AltModifier | wheelHandler.pageScrollModifiers)
verify(flickable.contentX === x - pageWidth * angleDeltaFactor, "-h_yPage")
x = flickable.contentX
verify(flickable.contentY === y, "no -yPage")
}
}
function test_WheelScrolling() {
// HID 1bcf:08a0 Mouse
// Angle delta is 120, like most mice.
wheelScrolling()
}
function test_HiResWheelScrolling() {
// Logitech MX Master 3
// Main wheel angle delta is at least 16, plus multiples of 8 when scrolling faster.
wheelScrolling(16)
}
function test_TouchpadScrolling() {
// UNIW0001:00 093A:0255 Touchpad
// 2 finger scroll angle delta is at least 3, but larger increments are used when scrolling faster.
wheelScrolling(3)
}
function keyboardScrolling() {
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, Qt.AltModifier)
verify(flickable.contentX === x + pageWidth, "h_Key_PageDown")
x = flickable.contentX
keyClick(Qt.Key_PageUp, Qt.AltModifier)
verify(flickable.contentX === x - pageWidth, "h_Key_PageUp")
x = flickable.contentX
keyClick(Qt.Key_End, Qt.AltModifier)
verify(flickable.contentX === contentWidth - pageWidth, "h_Key_End")
x = flickable.contentX
keyClick(Qt.Key_Home, Qt.AltModifier)
verify(flickable.contentX === originalX, "h_Key_Home")
}
function test_KeyboardScrolling() {
keyboardScrolling()
}
function test_StepSize() {
// 101 is a value unlikely to be used by any user's combination of settings and hardware
wheelHandler.verticalStepSize = 101
wheelHandler.horizontalStepSize = 101
wheelScrolling()
keyboardScrolling()
// reset to default
wheelHandler.verticalStepSize = undefined
wheelHandler.horizontalStepSize = undefined
verify(wheelHandler.verticalStepSize == 20 * Qt.styleHints.wheelScrollLines, "default verticalStepSize")
verify(wheelHandler.horizontalStepSize == 20 * Qt.styleHints.wheelScrollLines, "default horizontalStepSize")
}
ScrollableFlickable {
id: flickable
focus: true
anchors.fill: parent
Kirigami.WheelHandler {
id: wheelHandler
target: flickable
keyNavigationEnabled: true
}
}
}
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import org.kde.kirigami 2.19 as Kirigami
QQC2.ApplicationWindow {
id: root
width: flickable.implicitWidth
height: flickable.implicitHeight
Flickable {
id: flickable
anchors.fill: parent
implicitWidth: wheelHandler.horizontalStepSize * 10 + leftMargin + rightMargin
implicitHeight: wheelHandler.verticalStepSize * 10 + topMargin + bottomMargin
leftMargin: QQC2.ScrollBar.vertical.visible && QQC2.ScrollBar.vertical.mirrored ? QQC2.ScrollBar.vertical.width : 0
rightMargin: QQC2.ScrollBar.vertical.visible && !QQC2.ScrollBar.vertical.mirrored ? QQC2.ScrollBar.vertical.width : 0
bottomMargin: QQC2.ScrollBar.horizontal.visible ? QQC2.ScrollBar.horizontal.height : 0
contentWidth: contentItem.childrenRect.width
contentHeight: contentItem.childrenRect.height
Kirigami.WheelHandler {
id: wheelHandler
target: flickable
filterMouseEvents: true
keyNavigationEnabled: true
}
QQC2.ScrollBar.vertical: QQC2.ScrollBar {
parent: flickable.parent
height: flickable.height - flickable.topMargin - flickable.bottomMargin
x: mirrored ? 0 : flickable.width - width