mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
Merge pull request #2303 from tibssy/feat/smooth-scroll-nviews
feat/smooth scroll animations on NListView, NGridView, and NScrollView
This commit is contained in:
@@ -1217,6 +1217,8 @@
|
||||
"profile-tooltip": "Profile picture",
|
||||
"reverse-scrolling-description": "Reverse the interpreted scroll direction",
|
||||
"reverse-scrolling-label": "Reverse scrolling",
|
||||
"smooth-scrolling-description": "Animate list scrolling for a smoother wheel experience.",
|
||||
"smooth-scrolling-label": "Smooth scrolling",
|
||||
"screen-corners-desc": "Customize screen corner rounding and visual effects.",
|
||||
"screen-corners-radius-description": "Adjust the rounded corners of the screen.",
|
||||
"screen-corners-radius-label": "Screen corners radius",
|
||||
|
||||
@@ -141,7 +141,8 @@
|
||||
"Del"
|
||||
]
|
||||
},
|
||||
"reverseScroll": false
|
||||
"reverseScroll": false,
|
||||
"smoothScrollEnabled": true
|
||||
},
|
||||
"ui": {
|
||||
"fontDefault": "",
|
||||
|
||||
@@ -1279,6 +1279,14 @@
|
||||
"tabLabel": "common.general",
|
||||
"subTab": null
|
||||
},
|
||||
{
|
||||
"labelKey": "panels.general.smooth-scrolling-label",
|
||||
"descriptionKey": "panels.general.smooth-scrolling-description",
|
||||
"widget": "NToggle",
|
||||
"tab": 0,
|
||||
"tabLabel": "common.general",
|
||||
"subTab": null
|
||||
},
|
||||
{
|
||||
"labelKey": "panels.general.keybinds-title",
|
||||
"descriptionKey": "panels.general.keybinds-description",
|
||||
|
||||
@@ -319,6 +319,7 @@ Singleton {
|
||||
property list<string> keyRemove: ["Del"]
|
||||
}
|
||||
property bool reverseScroll: false
|
||||
property bool smoothScrollEnabled: true
|
||||
}
|
||||
|
||||
// ui
|
||||
|
||||
@@ -150,6 +150,15 @@ ColumnLayout {
|
||||
onToggled: checked => Settings.data.general.reverseScroll = checked
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: I18n.tr("panels.general.smooth-scrolling-label")
|
||||
description: I18n.tr("panels.general.smooth-scrolling-description")
|
||||
checked: Settings.data.general.smoothScrollEnabled
|
||||
defaultValue: Settings.getDefaultValue("general.smoothScrollEnabled")
|
||||
onToggled: checked => Settings.data.general.smoothScrollEnabled = checked
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginM
|
||||
|
||||
@@ -1051,19 +1051,8 @@ SmartPanel {
|
||||
bottomMargin: Style.marginS
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
// Synchronize scroll with current item position
|
||||
if (currentIndex >= 0) {
|
||||
let row = Math.floor(currentIndex / columns);
|
||||
let itemY = row * cellHeight;
|
||||
let viewportTop = contentY;
|
||||
let viewportBottom = viewportTop + height;
|
||||
|
||||
// If item is out of view, scroll
|
||||
if (itemY < viewportTop) {
|
||||
contentY = Math.max(0, itemY - cellHeight);
|
||||
} else if (itemY + cellHeight > viewportBottom) {
|
||||
contentY = Math.min(contentHeight - height, itemY + cellHeight - height);
|
||||
}
|
||||
positionViewAtIndex(currentIndex, GridView.Contain);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1524,16 +1513,7 @@ SmartPanel {
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
if (currentIndex >= 0) {
|
||||
let row = Math.floor(currentIndex / columns);
|
||||
let itemY = row * cellHeight;
|
||||
let viewportTop = contentY;
|
||||
let viewportBottom = viewportTop + height;
|
||||
|
||||
if (itemY < viewportTop) {
|
||||
contentY = Math.max(0, itemY - cellHeight);
|
||||
} else if (itemY + cellHeight > viewportBottom) {
|
||||
contentY = Math.min(contentHeight - height, itemY + cellHeight - height);
|
||||
}
|
||||
positionViewAtIndex(currentIndex, GridView.Contain);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+85
-2
@@ -85,6 +85,46 @@ Item {
|
||||
|
||||
// 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, gridView.contentHeight - gridView.height));
|
||||
}
|
||||
|
||||
function applyWheelScroll(delta) {
|
||||
if (!root.contentOverflows)
|
||||
return;
|
||||
|
||||
const step = delta * root.wheelScrollMultiplier;
|
||||
|
||||
if (!Settings.data.general.smoothScrollEnabled || Settings.data.general.animationDisabled) {
|
||||
gridView.contentY = root.clampScrollY(gridView.contentY - step);
|
||||
root._wheelTargetY = gridView.contentY;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!wheelScrollAnimation.running)
|
||||
root._wheelTargetY = gridView.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 || gridView.dragging || gridView.flicking) {
|
||||
gridView.contentY = clampedY;
|
||||
root._wheelTargetY = clampedY;
|
||||
return;
|
||||
}
|
||||
|
||||
root._wheelTargetY = clampedY;
|
||||
wheelScrollAnimation.to = clampedY;
|
||||
wheelScrollAnimation.restart();
|
||||
}
|
||||
|
||||
// Track selection index for gradient visibility (set externally)
|
||||
property int trackedSelectionIndex: -1
|
||||
@@ -129,7 +169,25 @@ Item {
|
||||
|
||||
// Forward GridView methods
|
||||
function positionViewAtIndex(index, mode) {
|
||||
const shouldAnimate = mode === GridView.Contain;
|
||||
if (!shouldAnimate) {
|
||||
gridView.positionViewAtIndex(index, mode);
|
||||
root._wheelTargetY = gridView.contentY;
|
||||
return;
|
||||
}
|
||||
|
||||
const previousY = gridView.contentY;
|
||||
gridView.positionViewAtIndex(index, mode);
|
||||
const targetY = root.clampScrollY(gridView.contentY);
|
||||
|
||||
if (Math.abs(targetY - previousY) < 0.5) {
|
||||
root._wheelTargetY = targetY;
|
||||
return;
|
||||
}
|
||||
|
||||
gridView.contentY = previousY;
|
||||
root._wheelTargetY = previousY;
|
||||
root.animateToContentY(targetY);
|
||||
}
|
||||
|
||||
function positionViewAtBeginning() {
|
||||
@@ -197,6 +255,7 @@ Item {
|
||||
implicitHeight: 200
|
||||
|
||||
Component.onCompleted: {
|
||||
_wheelTargetY = gridView.contentY;
|
||||
createGradients();
|
||||
}
|
||||
|
||||
@@ -280,6 +339,31 @@ Item {
|
||||
// Enable flickable for smooth scrolling
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
NumberAnimation {
|
||||
id: wheelScrollAnimation
|
||||
target: gridView
|
||||
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)
|
||||
|
||||
// Focus handling depends on keyNavigationEnabled
|
||||
focus: keyNavigationEnabled
|
||||
activeFocusOnTab: keyNavigationEnabled
|
||||
@@ -296,8 +380,7 @@ Item {
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
onWheel: event => {
|
||||
const delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y : event.angleDelta.y / 2;
|
||||
const newY = gridView.contentY - (delta * root.wheelScrollMultiplier);
|
||||
gridView.contentY = Math.max(0, Math.min(newY, gridView.contentHeight - gridView.height));
|
||||
root.applyWheelScroll(delta);
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
+85
-2
@@ -73,10 +73,68 @@ Item {
|
||||
|
||||
// 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() {
|
||||
@@ -124,6 +182,7 @@ Item {
|
||||
implicitHeight: 200
|
||||
|
||||
Component.onCompleted: {
|
||||
_wheelTargetY = listView.contentY;
|
||||
createGradients();
|
||||
}
|
||||
|
||||
@@ -192,6 +251,31 @@ Item {
|
||||
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
|
||||
@@ -205,8 +289,7 @@ Item {
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
onWheel: event => {
|
||||
const delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y : event.angleDelta.y / 2;
|
||||
const newY = listView.contentY - (delta * root.wheelScrollMultiplier);
|
||||
listView.contentY = Math.max(0, Math.min(newY, listView.contentHeight - listView.height));
|
||||
root.applyWheelScroll(delta);
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
+67
-5
@@ -28,6 +28,36 @@ ScrollView {
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -91,6 +121,40 @@ ScrollView {
|
||||
// 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)
|
||||
@@ -105,6 +169,8 @@ ScrollView {
|
||||
child.flickableDirection = Flickable.VerticalFlick;
|
||||
child.contentWidth = Qt.binding(() => child.width);
|
||||
}
|
||||
|
||||
root._wheelTargetY = child.contentY;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -114,12 +180,8 @@ ScrollView {
|
||||
enabled: root.wheelScrollMultiplier !== 1.0 && root._internalFlickable !== null
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
onWheel: event => {
|
||||
if (!root._internalFlickable)
|
||||
return;
|
||||
const flickable = root._internalFlickable;
|
||||
const delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y : event.angleDelta.y / 2;
|
||||
const newY = flickable.contentY - (delta * root.wheelScrollMultiplier);
|
||||
flickable.contentY = Math.max(0, Math.min(newY, flickable.contentHeight - flickable.height));
|
||||
root.applyWheelScroll(delta);
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user