Files

281 lines
9.3 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Templates as T
import qs.Commons
ScrollView {
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.AsNeeded
property bool preventHorizontalScroll: horizontalPolicy === ScrollBar.AlwaysOff
property int boundsBehavior: Flickable.StopAtBounds
readonly property bool verticalScrollable: (contentItem.contentHeight > contentItem.height) || (verticalPolicy == ScrollBar.AlwaysOn)
readonly property bool horizontalScrollable: (contentItem.contentWidth > contentItem.width) || (horizontalPolicy == ScrollBar.AlwaysOn)
property bool showGradientMasks: true
property color gradientColor: Color.mSurfaceVariant
property int gradientHeight: 16
property bool reserveScrollbarSpace: true
property real userRightPadding: 0
// Keep scrollbars visible whenever overflow exists (without forcing visibility when not scrollable)
property bool showScrollbarWhenScrollable: Settings.data.ui.scrollbarAlwaysVisible
// 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) {
if (!root._internalFlickable)
return 0;
const flickable = root._internalFlickable;
return Math.max(0, Math.min(value, flickable.contentHeight - flickable.height));
}
function applyWheelScroll(delta) {
if (!root._internalFlickable)
return;
const flickable = root._internalFlickable;
const step = delta * root.wheelScrollMultiplier;
if (!Settings.data.general.smoothScrollEnabled || Settings.data.general.animationDisabled) {
flickable.contentY = root.clampScrollY(flickable.contentY - step);
root._wheelTargetY = flickable.contentY;
return;
}
if (!wheelScrollAnimation.running)
root._wheelTargetY = flickable.contentY;
root._wheelTargetY = root.clampScrollY(root._wheelTargetY - step);
wheelScrollAnimation.to = root._wheelTargetY;
wheelScrollAnimation.restart();
}
rightPadding: userRightPadding + (reserveScrollbarSpace && verticalScrollable ? handleWidth + Style.marginXS : 0)
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, contentWidth + leftPadding + rightPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, contentHeight + topPadding + bottomPadding)
// Configure the internal flickable when it becomes available
Component.onCompleted: {
configureFlickable();
createGradients();
}
// Dynamically create gradient overlays to avoid interfering with ScrollView content management
function createGradients() {
if (!showGradientMasks)
return;
Qt.createQmlObject(`
import QtQuick
import qs.Commons
Rectangle {
x: root.leftPadding
y: root.topPadding
width: root.availableWidth
height: root.gradientHeight
z: 1
visible: root.showGradientMasks && root.verticalScrollable
opacity: root.contentItem.contentY <= 1 ? 0 : 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: root.leftPadding
y: root.height - root.bottomPadding - height + 1
width: root.availableWidth
height: root.gradientHeight + 1
z: 1
visible: root.showGradientMasks && root.verticalScrollable
opacity: (root.contentItem.contentY + root.contentItem.height >= root.contentItem.contentHeight - 1) ? 0 : 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");
}
// Reference to the internal Flickable for wheel handling
property Flickable _internalFlickable: null
NumberAnimation {
id: wheelScrollAnimation
target: root._internalFlickable
property: "contentY"
duration: root.smoothWheelAnimationDuration
easing.type: Easing.OutCubic
}
Connections {
target: root._internalFlickable
function onDraggingChanged() {
if (!root._internalFlickable || !root._internalFlickable.dragging)
return;
wheelScrollAnimation.stop();
root._wheelTargetY = root._internalFlickable.contentY;
}
function onFlickingChanged() {
if (!root._internalFlickable || !root._internalFlickable.flicking)
return;
wheelScrollAnimation.stop();
root._wheelTargetY = root._internalFlickable.contentY;
}
function onContentHeightChanged() {
root._wheelTargetY = root.clampScrollY(root._wheelTargetY);
}
function onHeightChanged() {
root._wheelTargetY = root.clampScrollY(root._wheelTargetY);
}
}
// Function to configure the underlying Flickable
function configureFlickable() {
// Find the internal Flickable (it's usually the first child)
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (child.toString().indexOf("Flickable") !== -1) {
// Configure the flickable to prevent horizontal scrolling
child.boundsBehavior = root.boundsBehavior;
root._internalFlickable = child;
if (root.preventHorizontalScroll) {
child.flickableDirection = Flickable.VerticalFlick;
child.contentWidth = Qt.binding(() => child.width);
}
root._wheelTargetY = child.contentY;
break;
}
}
}
WheelHandler {
enabled: root.wheelScrollMultiplier !== 1.0 && root._internalFlickable !== null
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;
}
}
// Watch for changes in horizontalPolicy
onHorizontalPolicyChanged: {
preventHorizontalScroll = (horizontalPolicy === ScrollBar.AlwaysOff);
configureFlickable();
}
ScrollBar.vertical: ScrollBar {
parent: root
x: root.mirrored ? 0 : root.width - width
y: root.topPadding
height: root.availableHeight
policy: root.verticalPolicy
interactive: root.verticalScrollable
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.verticalScrollable ? ((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.verticalScrollable ? ((root.showScrollbarWhenScrollable || parent.active) ? 0.3 : 0.0) : 0.0
radius: root.handleRadius / 2
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
}
ScrollBar.horizontal: ScrollBar {
parent: root
x: root.leftPadding
y: root.height - height
width: root.availableWidth
policy: root.horizontalPolicy
interactive: root.horizontalScrollable
contentItem: Rectangle {
implicitWidth: 100
implicitHeight: root.handleWidth
radius: root.handleRadius
color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor
opacity: parent.policy === ScrollBar.AlwaysOn ? 1.0 : root.horizontalScrollable ? ((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: 100
implicitHeight: root.handleWidth
color: root.trackColor
opacity: parent.policy === ScrollBar.AlwaysOn ? 0.3 : root.horizontalScrollable ? ((root.showScrollbarWhenScrollable || parent.active) ? 0.3 : 0.0) : 0.0
radius: root.handleRadius / 2
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
}
}