import QtQuick import QtQuick.Controls import QtQuick.Templates as T import qs.Commons Item { id: root property color handleColor: Qt.alpha(Color.mHover, 0.8) property color handleHoverColor: handleColor property color handlePressedColor: handleColor property color trackColor: "transparent" property real handleWidth: Math.round(6 * Style.uiScaleRatio) property real handleRadius: Style.iRadiusM property int verticalPolicy: ScrollBar.AsNeeded property int horizontalPolicy: ScrollBar.AlwaysOff readonly property bool verticalScrollBarActive: { if (listView.ScrollBar.vertical.policy === ScrollBar.AlwaysOff) return false; return listView.contentHeight > listView.height; } readonly property bool contentOverflows: listView.contentHeight > listView.height property bool showGradientMasks: true property color gradientColor: Color.mSurfaceVariant property int gradientHeight: 16 property bool reserveScrollbarSpace: true // Keep scrollbars visible whenever overflow exists (without forcing visibility when not scrollable) property bool showScrollbarWhenScrollable: Settings.data.ui.scrollbarAlwaysVisible // Available width for content (excludes scrollbar space when reserveScrollbarSpace is true) readonly property real availableWidth: width - (reserveScrollbarSpace ? handleWidth + Style.marginXS : 0) // Forward ListView properties property alias model: listView.model property alias delegate: listView.delegate property alias spacing: listView.spacing property alias orientation: listView.orientation property alias currentIndex: listView.currentIndex property alias count: listView.count property alias contentHeight: listView.contentHeight property alias contentWidth: listView.contentWidth property alias contentY: listView.contentY property alias contentX: listView.contentX property alias currentItem: listView.currentItem property alias highlightItem: listView.highlightItem property alias headerItem: listView.headerItem property alias footerItem: listView.footerItem property alias section: listView.section property alias highlightFollowsCurrentItem: listView.highlightFollowsCurrentItem property alias highlightMoveDuration: listView.highlightMoveDuration property alias highlightMoveVelocity: listView.highlightMoveVelocity property alias preferredHighlightBegin: listView.preferredHighlightBegin property alias preferredHighlightEnd: listView.preferredHighlightEnd property alias highlightRangeMode: listView.highlightRangeMode property alias snapMode: listView.snapMode property alias keyNavigationWraps: listView.keyNavigationWraps property alias cacheBuffer: listView.cacheBuffer property alias displayMarginBeginning: listView.displayMarginBeginning property alias displayMarginEnd: listView.displayMarginEnd property alias layoutDirection: listView.layoutDirection property alias effectiveLayoutDirection: listView.effectiveLayoutDirection property alias verticalLayoutDirection: listView.verticalLayoutDirection property alias boundsBehavior: listView.boundsBehavior property alias flickableDirection: listView.flickableDirection property alias interactive: listView.interactive property alias moving: listView.moving property alias flicking: listView.flicking property alias dragging: listView.dragging property alias horizontalVelocity: listView.horizontalVelocity property alias verticalVelocity: listView.verticalVelocity // Scroll speed multiplier for mouse wheel (1.0 = default, higher = faster) property real wheelScrollMultiplier: 2.0 property int smoothWheelAnimationDuration: Style.animationNormal property real _wheelTargetY: 0 function clampScrollY(value) { return Math.max(0, Math.min(value, listView.contentHeight - listView.height)); } function applyWheelScroll(delta) { if (!root.contentOverflows) return; const step = delta * root.wheelScrollMultiplier; if (!Settings.data.general.smoothScrollEnabled || Settings.data.general.animationDisabled) { listView.contentY = root.clampScrollY(listView.contentY - step); root._wheelTargetY = listView.contentY; return; } if (!wheelScrollAnimation.running) root._wheelTargetY = listView.contentY; root._wheelTargetY = root.clampScrollY(root._wheelTargetY - step); wheelScrollAnimation.to = root._wheelTargetY; wheelScrollAnimation.restart(); } function animateToContentY(targetY) { const clampedY = root.clampScrollY(targetY); if (!Settings.data.general.smoothScrollEnabled || Settings.data.general.animationDisabled || listView.dragging || listView.flicking) { listView.contentY = clampedY; root._wheelTargetY = clampedY; return; } root._wheelTargetY = clampedY; wheelScrollAnimation.to = clampedY; wheelScrollAnimation.restart(); } // Forward ListView methods function positionViewAtIndex(index, mode) { const shouldAnimate = mode === ListView.Contain; if (!shouldAnimate) { listView.positionViewAtIndex(index, mode); root._wheelTargetY = listView.contentY; return; } const previousY = listView.contentY; listView.positionViewAtIndex(index, mode); const targetY = root.clampScrollY(listView.contentY); if (Math.abs(targetY - previousY) < 0.5) { root._wheelTargetY = targetY; return; } listView.contentY = previousY; root._wheelTargetY = previousY; root.animateToContentY(targetY); } function positionViewAtBeginning() { listView.positionViewAtBeginning(); } function positionViewAtEnd() { listView.positionViewAtEnd(); } function forceLayout() { listView.forceLayout(); } function cancelFlick() { listView.cancelFlick(); } function flick(xVelocity, yVelocity) { listView.flick(xVelocity, yVelocity); } function incrementCurrentIndex() { listView.incrementCurrentIndex(); } function decrementCurrentIndex() { listView.decrementCurrentIndex(); } function indexAt(x, y) { return listView.indexAt(x, y); } function itemAt(x, y) { return listView.itemAt(x, y); } function itemAtIndex(index) { return listView.itemAtIndex(index); } // Set reasonable implicit sizes for Layout usage implicitWidth: 200 implicitHeight: 200 Component.onCompleted: { _wheelTargetY = listView.contentY; createGradients(); } // Dynamically create gradient overlays function createGradients() { if (!showGradientMasks) return; Qt.createQmlObject(` import QtQuick import qs.Commons Rectangle { x: 0 y: 0 width: root.availableWidth height: root.gradientHeight z: 1 visible: root.showGradientMasks && root.contentOverflows opacity: { if (listView.contentY <= 1) return 0; if (listView.currentItem && listView.currentItem.y - listView.contentY < root.gradientHeight) return 0; return 1; } Behavior on opacity { NumberAnimation { duration: Style.animationFast; easing.type: Easing.InOutQuad } } gradient: Gradient { GradientStop { position: 0.0; color: root.gradientColor } GradientStop { position: 1.0; color: "transparent" } } } `, root, "topGradient"); Qt.createQmlObject(` import QtQuick import qs.Commons Rectangle { x: 0 anchors.bottom: parent.bottom anchors.bottomMargin: -1 width: root.availableWidth height: root.gradientHeight + 1 z: 1 visible: root.showGradientMasks && root.contentOverflows opacity: { if (listView.contentY + listView.height >= listView.contentHeight - 1) return 0; if (listView.currentItem && listView.currentItem.y + listView.currentItem.height > listView.contentY + listView.height - root.gradientHeight) return 0; return 1; } Behavior on opacity { NumberAnimation { duration: Style.animationFast; easing.type: Easing.InOutQuad } } gradient: Gradient { GradientStop { position: 0.0; color: "transparent" } GradientStop { position: 1.0; color: root.gradientColor } } } `, root, "bottomGradient"); } ListView { id: listView anchors.fill: parent anchors.rightMargin: root.reserveScrollbarSpace ? root.handleWidth + Style.marginXS : 0 clip: true boundsBehavior: Flickable.StopAtBounds NumberAnimation { id: wheelScrollAnimation target: listView property: "contentY" duration: root.smoothWheelAnimationDuration easing.type: Easing.OutCubic } onDraggingChanged: { if (dragging) { wheelScrollAnimation.stop(); root._wheelTargetY = contentY; } } onFlickingChanged: { if (flicking) { wheelScrollAnimation.stop(); root._wheelTargetY = contentY; } } onContentHeightChanged: root._wheelTargetY = root.clampScrollY(root._wheelTargetY) onHeightChanged: root._wheelTargetY = root.clampScrollY(root._wheelTargetY) WheelHandler { enabled: !root.contentOverflows acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad onWheel: event => { event.accepted = true; } } WheelHandler { enabled: root.wheelScrollMultiplier !== 1.0 && root.contentOverflows acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad onWheel: event => { const delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y : event.angleDelta.y / 2; root.applyWheelScroll(delta); event.accepted = true; } } ScrollBar.vertical: ScrollBar { parent: root x: root.mirrored ? 0 : root.width - width y: 0 height: root.height policy: root.verticalPolicy visible: policy === ScrollBar.AlwaysOn || root.verticalScrollBarActive contentItem: Rectangle { implicitWidth: root.handleWidth implicitHeight: 100 radius: root.handleRadius color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor opacity: parent.policy === ScrollBar.AlwaysOn ? 1.0 : root.verticalScrollBarActive ? ((root.showScrollbarWhenScrollable || parent.active) ? 1.0 : 0.0) : 0.0 Behavior on opacity { NumberAnimation { duration: Style.animationFast } } Behavior on color { ColorAnimation { duration: Style.animationFast } } } background: Rectangle { implicitWidth: root.handleWidth implicitHeight: 100 color: root.trackColor opacity: parent.policy === ScrollBar.AlwaysOn ? 0.3 : root.verticalScrollBarActive ? ((root.showScrollbarWhenScrollable || parent.active) ? 0.3 : 0.0) : 0.0 radius: root.handleRadius / 2 Behavior on opacity { NumberAnimation { duration: Style.animationFast } } } } } }