settings: improve transition behavior

This commit is contained in:
bbedward
2026-10-07 19:22:24 -04:00
parent f2c06a3cc7
commit b1dde74e4d
14 changed files with 150 additions and 147 deletions
Generated
+3 -3
View File
@@ -3,11 +3,11 @@
"dank-qml-common": {
"flake": false,
"locked": {
"lastModified": 1791404062,
"narHash": "sha256-Q9wxePvAGGVlYBP1o2cI+JJJ61P3jzb27nthusVv4GE=",
"lastModified": 1791415314,
"narHash": "sha256-7tV9UFCKTOk8EN7gdtFJeI8NFXtopCJFC86KJU4qK44=",
"owner": "AvengeMedia",
"repo": "dank-qml-common",
"rev": "f95f3d8c5344613d7bba2ef9a65d489febe9d52a",
"rev": "d5786555ccd248491017b0cf8d1bfbd9e0892e5b",
"type": "github"
},
"original": {
+2 -1
View File
@@ -22,7 +22,7 @@ Singleton {
id: root
readonly property var log: Log.scoped("SettingsData")
readonly property int settingsConfigVersion: 38
readonly property int settingsConfigVersion: 39
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
@@ -853,6 +853,7 @@ Singleton {
return (barConfigs || []).filter(cfg => isIslandBarConfig(cfg));
}
readonly property bool dankIslandEnabled: (barConfigs || []).some(cfg => (cfg.enabled ?? false) && hostsIsland(cfg))
readonly property var enabledIslandBarConfigs: (barConfigs || []).filter(cfg => cfg?.island === true && (cfg.enabled ?? false))
// Session-only: which bar, island or dot last-used shared shortcuts follow on each screen.
property var lastUsedBarByScreen: ({})
// One slot per edge; a dot floats, so it never takes one.
@@ -51,7 +51,6 @@ var ISLAND_DEFAULTS = {
islandSatelliteFollowInterfaceStyle: true,
islandSatelliteTransparency: 1,
islandSatelliteSwoopRadius: 24,
islandReducedMotion: false,
islandSpringStiffness: 560,
islandSpringDamping: 37,
islandSpringMass: 1
@@ -718,9 +718,30 @@ function migrateToVersion(obj, targetVersion) {
settings.configVersion = 38;
}
if (currentVersion < 39 && targetVersion >= 39) {
migrateIslandReducedMotion(settings);
settings.configVersion = 39;
}
return settings;
}
// v39: the per-island toggle folded into the global reduceMotion; any bar that had it on turns the global one on
function migrateIslandReducedMotion(settings) {
for (const bar of Array.isArray(settings.barConfigs) ? settings.barConfigs : []) {
if (!bar || typeof bar !== "object")
continue;
const holders = [bar];
for (const sectionId of ["leftWidgets", "centerWidgets", "rightWidgets"])
holders.push(...(Array.isArray(bar[sectionId]) ? bar[sectionId] : []).filter(entry => entry && typeof entry === "object" && entry.id === "island"));
for (const holder of holders) {
if (holder.islandReducedMotion === true)
settings.reduceMotion = true;
delete holder.islandReducedMotion;
}
}
}
var LOCK_WIDGET_MOVED_KEYS = ["lockScreenShowSystemIcons", "lockScreenShowTime", "lockScreenClockStyle", "lockScreenShowDate", "lockScreenShowPasswordField", "lockScreenShowMediaPlayer", "lockScreenNotificationMode"];
var LOCK_WIDGET_GREETER_KEYS = ["lockScreenShowPowerActions", "lockScreenShowProfileImage", "lockScreenShowWeather"];
+81 -60
View File
@@ -18,12 +18,11 @@ FocusScope {
readonly property list<string> pagePath: (parentModal?.pageHistory ?? []).concat([currentPage])
readonly property Item currentPageItem: pageStack.currentItem?.item ?? null
readonly property bool currentPageSettled: pageStack.currentItem?.page === currentPage && pageStack.currentItem.settled
property Item pendingPage: null
readonly property bool isPluginPage: SettingsTabs.isPluginPage(currentPage)
readonly property bool isCompactMode: parentModal?.isCompactMode ?? false
readonly property bool menuHidden: isCompactMode && !(parentModal?.menuVisible ?? true)
readonly property bool showBack: (parentModal?.canGoBack ?? false) || isPluginPage || menuHidden
readonly property bool animationsEnabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
readonly property bool animationsEnabled: !SettingsData.reduceMotion && Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
// Mouse navigation lands parked; only keyboard navigation highlights a control on the page
property bool keyboardNavigation: true
@@ -205,13 +204,6 @@ FocusScope {
return properties;
}
function _discardPendingPage() {
if (!pendingPage)
return;
pendingPage.destroy();
pendingPage = null;
}
function _createPage(page, index) {
return pageComponent.createObject(pageStack, {
page,
@@ -219,76 +211,104 @@ FocusScope {
});
}
// A navigation that lands mid-transition cuts in rather than queueing behind it
function _operation(transition) {
const animate = animationsEnabled && pageStack.depth > 0 && !pageStack.busy && (parentModal?.visible ?? false);
return animate ? transition : StackView.Immediate;
}
function _syncPages() {
if (!sessionVisible) {
_discardPendingPage();
pageStack.clear(StackView.Immediate);
return;
}
if (!currentPage || pageStack.busy)
if (!currentPage)
return;
if (pendingPage && (pendingPage.page !== currentPage || pendingPage.pathIndex !== pagePath.length - 1))
_discardPendingPage();
let shared = 0;
while (shared < Math.min(pageStack.depth, pagePath.length) && pageStack.get(shared).page === pagePath[shared])
shared++;
if (shared === pagePath.length) {
_discardPendingPage();
if (shared < pageStack.depth)
pageStack.pop(pageStack.get(shared - 1), animationsEnabled ? StackView.PopTransition : StackView.Immediate);
pageStack.pop(pageStack.get(shared - 1), _operation(StackView.PopTransition));
return;
}
if (!pendingPage) {
pendingPage = _createPage(currentPage, pagePath.length - 1);
pendingPage.load(parentModal?.visible ?? false);
}
if (!pendingPage.settled)
return;
const animate = animationsEnabled && pageStack.depth > 0 && (parentModal?.visible ?? false);
const deferred = parentModal?.visible ?? false;
const pages = [];
for (let index = shared; index < pagePath.length - 1; index++) {
for (let index = shared; index < pagePath.length; index++) {
const page = _createPage(pagePath[index], index);
page.load(false);
page.load(deferred && index === pagePath.length - 1);
pages.push(page);
}
pages.push(pendingPage);
pendingPage = null;
if (shared < pageStack.depth) {
pageStack.replace(pageStack.get(shared), pages, animate ? StackView.ReplaceTransition : StackView.Immediate);
pageStack.replace(pageStack.get(shared), pages, _operation(StackView.ReplaceTransition));
return;
}
pageStack.push(pages, animate ? StackView.PushTransition : StackView.Immediate);
pageStack.push(pages, _operation(StackView.PushTransition));
}
onPagePathChanged: Qt.callLater(_syncPages)
onSessionVisibleChanged: Qt.callLater(_syncPages)
component PageTransition: Transition {
id: transition
// Fade through the pane: the leaving page is gone before the next one starts to show, so nothing blends mid-way
component PageExit: Transition {
id: exit
property bool entering: true
property real fromX: 0
property real toX: 0
NumberAnimation {
property: "x"
from: transition.fromX
to: transition.toX
duration: SettingsMetrics.transitionDuration
to: exit.toX
duration: SettingsMetrics.exitDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.standard
}
NumberAnimation {
property: "opacity"
from: transition.entering ? 0 : 1
to: transition.entering ? 1 : 0
duration: SettingsMetrics.fadeDuration
to: 0
duration: SettingsMetrics.exitDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveEffects
}
}
component PageEnter: Transition {
id: enter
property real fromX: 0
SequentialAnimation {
PropertyAction {
property: "opacity"
value: 0
}
PauseAnimation {
duration: SettingsMetrics.exitDuration
}
ParallelAnimation {
NumberAnimation {
property: "x"
from: enter.fromX
to: 0
duration: SettingsMetrics.transitionDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.standard
}
NumberAnimation {
property: "opacity"
from: 0
to: 1
duration: SettingsMetrics.fadeDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveEffects
}
}
}
}
function _scrollerOf(item) {
if (typeof item.flick === "function")
return item;
@@ -338,13 +358,13 @@ FocusScope {
width: pageStack.width
height: pageStack.height
opacity: 0
enabled: pageActive && presented
StackView.onRemoved: destroy()
onSettledChanged: {
if (settled && root.pendingPage === host)
Qt.callLater(root._syncPages);
// An immediate operation skips the nested enter animation, so the final pose is written here
StackView.onActivated: {
opacity = 1;
x = 0;
}
function load(deferred) {
@@ -407,8 +427,18 @@ FocusScope {
anchors.bottom: parent.bottom
anchors.leftMargin: SettingsMetrics.panePadding
anchors.rightMargin: SettingsMetrics.panePadding
opacity: host.presented ? 1 : 0
enabled: host.presented
Behavior on opacity {
enabled: root.animationsEnabled
NumberAnimation {
duration: SettingsMetrics.fadeDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveEffects
}
}
onLoaded: {
if (item.pageActive !== undefined)
item.pageActive = Qt.binding(() => host.pageActive);
@@ -551,36 +581,27 @@ FocusScope {
anchors.fill: parent
clip: true
readonly property real pageOffset: SettingsData.reduceMotion ? 0 : (I18n.isRtl ? -Theme.spacingXL : Theme.spacingXL)
readonly property real pageOffset: I18n.isRtl ? -Theme.spacingXL : Theme.spacingXL
onBusyChanged: {
if (!busy)
Qt.callLater(root._syncPages);
}
pushEnter: PageTransition {
pushEnter: PageEnter {
fromX: pageStack.pageOffset
}
pushExit: PageTransition {
entering: false
pushExit: PageExit {
toX: -pageStack.pageOffset
}
popEnter: PageTransition {
popEnter: PageEnter {
fromX: -pageStack.pageOffset
}
popExit: PageTransition {
entering: false
popExit: PageExit {
toX: pageStack.pageOffset
}
replaceEnter: PageTransition {}
replaceExit: PageTransition {
entering: false
}
replaceEnter: PageEnter {}
replaceExit: PageExit {}
DSpinner {
id: pageSpinner
readonly property bool loading: root.pendingPage !== null
readonly property bool loading: pageStack.currentItem?.presented === false
anchors.centerIn: parent
z: pageStack.depth
@@ -596,7 +617,7 @@ FocusScope {
Timer {
id: spinnerDelay
interval: SettingsMetrics.pageSettleDeadline + SettingsMetrics.fadeDuration
interval: SettingsMetrics.pageSpinnerDelay
onTriggered: pageSpinner.visible = pageSpinner.loading
}
}
@@ -178,6 +178,9 @@ Item {
root.focusAfterNavigation(keyboard);
return;
}
const islandBar = SettingsSearchService.islandBarFor(result.conditionKey, SettingsUiState.selectedBarId);
if (islandBar)
SettingsUiState.selectedBarId = islandBar.id;
if (result.section)
SettingsSearchService.navigateToSection(result.section);
const page = result.page || SettingsTabs.pageForTabIndex(result.tabIndex);
+1 -1
View File
@@ -465,7 +465,7 @@ Item {
compactBackground: root.embedded && root.setting("islandWidgetBackground") === true && !(root.barConfig?.noBackground ?? false)
compactBackgroundColor: BarMetrics.widgetFill(root.barConfig)
opacity: root.freeOpacity
reducedMotion: root.setting("islandReducedMotion") || SettingsData.reduceMotion || SettingsData.animationDuration <= 0
reducedMotion: SettingsData.reduceMotion || SettingsData.animationDuration <= 0
springStiffness: Math.max(root.springStiffnessRange[0], Math.min(root.springStiffnessRange[1], root.setting("islandSpringStiffness")))
springDamping: Math.max(root.springDampingRange[0], Math.min(root.springDampingRange[1], root.setting("islandSpringDamping")))
springMass: Math.max(root.springMassRange[0], Math.min(root.springMassRange[1], root.setting("islandSpringMass")))
@@ -129,7 +129,7 @@ Item {
SettingsCard {
tab: "typography"
tags: ["animation", "motion", "speed", "duration", "spring", "physics", "bounce", "accessibility", "reduce", "ripple", "fluid"]
tags: ["animation", "motion", "speed", "duration", "spring", "physics", "bounce", "accessibility", "ripple", "fluid"]
title: I18n.tr("Animations", "settings card title")
settingKey: "animations"
iconName: "auto_awesome_motion"
@@ -10,17 +10,7 @@ SettingsCard {
iconName: "animation"
title: I18n.tr("Motion", "island settings: spring motion card title")
settingKey: root.keyPrefix + "Motion"
tags: ["island", "motion", "spring", "animation", "reduce"]
SettingsToggleRow {
settingKey: root.keyPrefix + "ReducedMotion"
tags: ["island", "motion", "animation", "reduce", "accessibility", "spring"]
resetStore: root.store
resetKeys: ["islandReducedMotion"]
text: I18n.tr("Reduce motion")
checked: root.store.setting("islandReducedMotion")
onToggled: checked => root.store.apply("islandReducedMotion", checked)
}
tags: ["island", "motion", "spring", "animation"]
SettingsSliderRow {
settingKey: root.keyPrefix + "SpringStiffness"
@@ -33,7 +23,7 @@ SettingsCard {
maximum: 1200
step: 10
value: Math.round(root.store.setting("islandSpringStiffness"))
enabled: !root.store.setting("islandReducedMotion")
enabled: !SettingsData.reduceMotion
onSliderValueChanged: value => root.store.apply("islandSpringStiffness", value)
}
@@ -48,7 +38,7 @@ SettingsCard {
maximum: 100
step: 1
value: Math.round(root.store.setting("islandSpringDamping"))
enabled: !root.store.setting("islandReducedMotion")
enabled: !SettingsData.reduceMotion
onSliderValueChanged: value => root.store.apply("islandSpringDamping", value)
}
@@ -64,7 +54,7 @@ SettingsCard {
unit: ""
decimals: 2
value: Math.round(root.store.setting("islandSpringMass") * 100)
enabled: !root.store.setting("islandReducedMotion")
enabled: !SettingsData.reduceMotion
onSliderValueChanged: value => root.store.apply("islandSpringMass", value / 100)
}
}
@@ -59,6 +59,8 @@ Singleton {
readonly property color selectedRowColor: Theme.selectedContainer
readonly property int transitionDuration: Theme.expressiveDurations.expressiveFastSpatial
readonly property int fadeDuration: Theme.expressiveDurations.expressiveEffects
readonly property int exitDuration: Theme.expressiveDurations.expressiveFastEffects
readonly property int pageSettleFrames: 2
readonly property int pageSettleDeadline: 250
readonly property int pageSpinnerDelay: 500
}
+18 -1
View File
@@ -77,12 +77,29 @@ Singleton {
"matugenAvailable": () => Theme.matugenAvailable,
"greeterAvailable": () => GreeterService.available,
"frameEnabled": () => SettingsData.frameEnabled,
"islandEnabled": () => SettingsData.islandBarConfigs.length > 0,
"islandEnabled": () => root.islandBarFor("islandEnabled") !== null,
"islandFree": () => root.islandBarFor("islandFree") !== null,
"islandDocked": () => root.islandBarFor("islandDocked") !== null,
"dotEnabled": () => SettingsData.dotBarConfig?.enabled ?? false,
"cellularAvailable": () => NetworkService.cellularAvailable,
"dockEnabled": () => (SettingsData.dockConfigs ?? []).some(dock => dock.enabled)
})
readonly property var islandBarFilters: ({
"islandEnabled": cfg => true,
"islandFree": cfg => SettingsData.islandFreePlacement(cfg),
"islandDocked": cfg => !SettingsData.islandFreePlacement(cfg)
})
// The bar pages only show island rows for the selected bar, so a result has to pick one that fits its condition.
function islandBarFor(conditionKey, preferredId) {
const filter = islandBarFilters[conditionKey];
if (!filter)
return null;
const bars = SettingsData.enabledIslandBarConfigs.filter(filter);
return bars.find(cfg => cfg.id === preferredId) ?? bars[0] ?? null;
}
property var pluginSettingLabels: ({})
Component.onCompleted: indexFile.reload()
@@ -507,6 +507,10 @@ def find_settings_components(content, filename, wrappers, tab_meta, hub_meta):
condition_key = "matugenAvailable"
elif "dock.config.enabled" in visible_raw:
condition_key = "dockEnabled"
elif "selectedIslandDocked" in visible_raw or "!root.selectedIslandFree" in visible_raw:
condition_key = "islandDocked"
elif "selectedIslandFree" in visible_raw and "!dankBarTab.selectedIslandFree" not in visible_raw:
condition_key = "islandFree"
if filename in BAR_TAB_FILES and not condition_key:
if setting_key.startswith("frame"):
condition_key = "frameEnabled"
@@ -3468,7 +3468,7 @@
"topbar"
],
"parentLabel": "Bar",
"conditionKey": "islandEnabled"
"conditionKey": "islandFree"
},
{
"section": "barExclusiveZone",
@@ -3894,7 +3894,6 @@
"island",
"motion",
"panel",
"reduce",
"spring",
"statusbar",
"taskbar",
@@ -4028,7 +4027,7 @@
"vertical"
],
"parentLabel": "Bar",
"conditionKey": "islandEnabled"
"conditionKey": "islandFree"
},
{
"section": "islandOuterGap",
@@ -4050,7 +4049,7 @@
"topbar"
],
"parentLabel": "Bar",
"conditionKey": "islandEnabled"
"conditionKey": "islandDocked"
},
{
"section": "islandPlacement",
@@ -4102,28 +4101,6 @@
"parentLabel": "Bar",
"icon": "vertical_align_center"
},
{
"section": "islandReducedMotion",
"label": "Reduce motion",
"tabIndex": 3,
"category": "General",
"keywords": [
"accessibility",
"animation",
"bar",
"general",
"island",
"motion",
"panel",
"reduce",
"spring",
"statusbar",
"taskbar",
"topbar"
],
"parentLabel": "Bar",
"conditionKey": "islandEnabled"
},
{
"section": "islandReserveThickness",
"label": "Reserved width",
@@ -4146,7 +4123,7 @@
"width"
],
"parentLabel": "Bar",
"conditionKey": "islandEnabled"
"conditionKey": "islandDocked"
},
{
"section": "barRightClickAction",
@@ -4504,7 +4481,7 @@
"vertical"
],
"parentLabel": "Bar",
"conditionKey": "islandEnabled"
"conditionKey": "islandDocked"
},
{
"section": "barVisibility",
@@ -5141,7 +5118,7 @@
"width"
],
"parentLabel": "Bar",
"conditionKey": "islandEnabled"
"conditionKey": "islandDocked"
},
{
"section": "islandHomeStatusContent",
@@ -7730,7 +7707,6 @@
"fonts",
"motion",
"physics",
"reduce",
"ripple",
"speed",
"spring",
@@ -10443,14 +10419,6 @@
"runtimeType": "barWidgetOption",
"optionFile": "WorkspaceSwitcherOptions.qml"
},
{
"section": "barWidgetOption:IslandOptions.qml:29",
"label": "Reduce motion",
"tabIndex": 22,
"keywords": [],
"runtimeType": "barWidgetOption",
"optionFile": "IslandOptions.qml"
},
{
"section": "barWidgetOption:WorkspaceSwitcherOptions.qml:19",
"label": "Reverse scroll direction",
@@ -10668,7 +10636,7 @@
"optionFile": "AppsDockOptions.qml"
},
{
"section": "barWidgetOption:IslandOptions.qml:31",
"section": "barWidgetOption:IslandOptions.qml:30",
"label": "Spring damping",
"tabIndex": 22,
"keywords": [],
@@ -10676,7 +10644,7 @@
"optionFile": "IslandOptions.qml"
},
{
"section": "barWidgetOption:IslandOptions.qml:32",
"section": "barWidgetOption:IslandOptions.qml:31",
"label": "Spring mass",
"tabIndex": 22,
"keywords": [],
@@ -10684,7 +10652,7 @@
"optionFile": "IslandOptions.qml"
},
{
"section": "barWidgetOption:IslandOptions.qml:30",
"section": "barWidgetOption:IslandOptions.qml:29",
"label": "Spring stiffness",
"tabIndex": 22,
"keywords": [],
@@ -17484,7 +17452,6 @@
"island",
"motion",
"panel",
"reduce",
"spring",
"statusbar",
"taskbar",
@@ -17617,28 +17584,6 @@
"parentLabel": "Bar",
"conditionKey": "dotEnabled"
},
{
"section": "dotReducedMotion",
"label": "Reduce motion",
"tabIndex": 65,
"category": "Dot",
"keywords": [
"accessibility",
"animation",
"bar",
"dot",
"island",
"motion",
"panel",
"reduce",
"spring",
"statusbar",
"taskbar",
"topbar"
],
"parentLabel": "Bar",
"conditionKey": "dotEnabled"
},
{
"section": "dotSharedRouting",
"label": "Shared shortcuts",