Files
noctalia-shell/Widgets/NListView.qml
T

341 lines
11 KiB
QML

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
}
}
}
}
}
}