From 3798118461ceed1face16c4957c5695a8e5282e9 Mon Sep 17 00:00:00 2001 From: tibssy Date: Thu, 26 Mar 2026 00:23:29 +0000 Subject: [PATCH 1/5] feat(view): implement smooth wheel scrolling for NListView with global setting --- Assets/Translations/en.json | 2 + Assets/settings-default.json | 3 +- Assets/settings-search-index.json | 8 +++ Commons/Settings.qml | 1 + .../Settings/Tabs/General/BasicsSubTab.qml | 9 +++ Widgets/NListView.qml | 55 ++++++++++++++++++- 6 files changed, 75 insertions(+), 3 deletions(-) diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index ad4a9dfe5..0af2ba575 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -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", diff --git a/Assets/settings-default.json b/Assets/settings-default.json index 96e7f1a29..cbc3f5545 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -141,7 +141,8 @@ "Del" ] }, - "reverseScroll": false + "reverseScroll": false, + "smoothScrollEnabled": true }, "ui": { "fontDefault": "", diff --git a/Assets/settings-search-index.json b/Assets/settings-search-index.json index 1b2e7eb07..a2d482cdd 100644 --- a/Assets/settings-search-index.json +++ b/Assets/settings-search-index.json @@ -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", diff --git a/Commons/Settings.qml b/Commons/Settings.qml index d438e0a44..84f3399e6 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -319,6 +319,7 @@ Singleton { property list keyRemove: ["Del"] } property bool reverseScroll: false + property bool smoothScrollEnabled: true } // ui diff --git a/Modules/Panels/Settings/Tabs/General/BasicsSubTab.qml b/Modules/Panels/Settings/Tabs/General/BasicsSubTab.qml index ce24b410f..f459e79a8 100644 --- a/Modules/Panels/Settings/Tabs/General/BasicsSubTab.qml +++ b/Modules/Panels/Settings/Tabs/General/BasicsSubTab.qml @@ -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 diff --git a/Widgets/NListView.qml b/Widgets/NListView.qml index 4f4da6b6a..e6bf0344a 100644 --- a/Widgets/NListView.qml +++ b/Widgets/NListView.qml @@ -73,6 +73,32 @@ 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(); + } // Forward ListView methods function positionViewAtIndex(index, mode) { @@ -124,6 +150,7 @@ Item { implicitHeight: 200 Component.onCompleted: { + _wheelTargetY = listView.contentY; createGradients(); } @@ -191,6 +218,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 @@ -205,8 +257,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; } } From b612c353fda57c4cbdab176da619fe5a45617d0e Mon Sep 17 00:00:00 2001 From: tibssy Date: Thu, 26 Mar 2026 00:47:36 +0000 Subject: [PATCH 2/5] feat(view): implement smooth wheel scrolling for NGridView --- Widgets/NGridView.qml | 55 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/Widgets/NGridView.qml b/Widgets/NGridView.qml index b1d375464..17dc2d276 100644 --- a/Widgets/NGridView.qml +++ b/Widgets/NGridView.qml @@ -85,6 +85,32 @@ 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(); + } // Track selection index for gradient visibility (set externally) property int trackedSelectionIndex: -1 @@ -197,6 +223,7 @@ Item { implicitHeight: 200 Component.onCompleted: { + _wheelTargetY = gridView.contentY; createGradients(); } @@ -280,6 +307,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 +348,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; } } From 01744222c46a99c550c58e124d07277deaae72d5 Mon Sep 17 00:00:00 2001 From: tibssy Date: Thu, 26 Mar 2026 01:23:15 +0000 Subject: [PATCH 3/5] feat(view): implement smooth wheel scrolling for NScrollView --- Widgets/NScrollView.qml | 72 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 5 deletions(-) diff --git a/Widgets/NScrollView.qml b/Widgets/NScrollView.qml index caaa798df..d0d15568a 100644 --- a/Widgets/NScrollView.qml +++ b/Widgets/NScrollView.qml @@ -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; } } From c524c9611f8605a431592892e3a0efbdc4b998af Mon Sep 17 00:00:00 2001 From: tibssy Date: Thu, 26 Mar 2026 02:17:31 +0000 Subject: [PATCH 4/5] feat(view): add smooth scroll animation for keyboard navigation in NListView and NGridView --- Widgets/NGridView.qml | 32 ++++++++++++++++++++++++++++++++ Widgets/NListView.qml | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/Widgets/NGridView.qml b/Widgets/NGridView.qml index 17dc2d276..b43628107 100644 --- a/Widgets/NGridView.qml +++ b/Widgets/NGridView.qml @@ -112,6 +112,20 @@ Item { 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 @@ -155,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() { diff --git a/Widgets/NListView.qml b/Widgets/NListView.qml index e6bf0344a..4ea376cc2 100644 --- a/Widgets/NListView.qml +++ b/Widgets/NListView.qml @@ -100,9 +100,41 @@ Item { 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() { From ea1710c9c96fa0199eacc3ce48f3e1a9cc949e35 Mon Sep 17 00:00:00 2001 From: tibssy Date: Thu, 26 Mar 2026 02:33:06 +0000 Subject: [PATCH 5/5] fix: restore smooth scroll animation for keyboard navigation in wallpaper selector panel --- Modules/Panels/Wallpaper/WallpaperPanel.qml | 24 ++------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/Modules/Panels/Wallpaper/WallpaperPanel.qml b/Modules/Panels/Wallpaper/WallpaperPanel.qml index b9aab6117..4134db987 100644 --- a/Modules/Panels/Wallpaper/WallpaperPanel.qml +++ b/Modules/Panels/Wallpaper/WallpaperPanel.qml @@ -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); } }