diff --git a/quickshell/Modals/Settings/SettingsContent.qml b/quickshell/Modals/Settings/SettingsContent.qml index b0a53270f..c9fd9c8fb 100644 --- a/quickshell/Modals/Settings/SettingsContent.qml +++ b/quickshell/Modals/Settings/SettingsContent.qml @@ -17,15 +17,12 @@ FocusScope { readonly property bool sessionVisible: parentModal?.shouldBeVisible ?? false readonly property list pagePath: (parentModal?.pageHistory ?? []).concat([currentPage]) readonly property Item currentPageItem: pageStack.currentItem?.item ?? null - readonly property bool currentPageSettled: pageStack.currentItem?.settled ?? false - readonly property var pageInfo: SettingsTabs.page(currentPage) + 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 isHubPage: pageInfo?.kind === "hub" 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 string pageTitle: (pageInfo?.titleFrom ? SettingsUiState[pageInfo.titleFrom] : "") || (pageInfo?.text ?? "") - readonly property real pageContentMaxWidth: currentPageItem?.contentMaxWidth ?? SettingsMetrics.contentMaxWidth readonly property bool animationsEnabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None // Mouse navigation lands parked; only keyboard navigation highlights a control on the page property bool keyboardNavigation: true @@ -50,7 +47,7 @@ FocusScope { return; if (parentModal?.modalFocusScope?.transientOwnsFocus()) return; - if (!sessionVisible || !currentPageItem || !pageStack.currentItem.presented) + if (!sessionVisible || !currentPageItem || pageStack.currentItem.page !== currentPage || !pageStack.currentItem.presented) return; if (parentModal?.focusPane === "sidebar") return; @@ -208,51 +205,88 @@ FocusScope { return properties; } + function _discardPendingPage() { + if (!pendingPage) + return; + pendingPage.destroy(); + pendingPage = null; + } + + function _createPage(page, index) { + return pageComponent.createObject(pageStack, { + page, + pathIndex: index + }); + } + function _syncPages() { if (!sessionVisible) { + _discardPendingPage(); pageStack.clear(StackView.Immediate); return; } - if (!currentPage) + if (!currentPage || pageStack.busy) 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); return; } - const drillDown = animationsEnabled && shared > 0 && shared === pageStack.depth && shared === pagePath.length - 1; - const deferred = parentModal?.visible ?? false; - let index = shared; - if (index < pageStack.depth) - pageStack.replace(pageStack.get(index), pageComponent, { - page: pagePath[index++] - }, StackView.Immediate).load(deferred); - for (; index < pagePath.length; index++) - pageStack.push(pageComponent, { - page: pagePath[index] - }, drillDown ? StackView.PushTransition : StackView.Immediate).load(deferred); + 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 pages = []; + for (let index = shared; index < pagePath.length - 1; index++) { + const page = _createPage(pagePath[index], index); + page.load(false); + pages.push(page); + } + pages.push(pendingPage); + pendingPage = null; + if (shared < pageStack.depth) { + pageStack.replace(pageStack.get(shared), pages, animate ? StackView.ReplaceTransition : StackView.Immediate); + return; + } + pageStack.push(pages, animate ? StackView.PushTransition : StackView.Immediate); } onPagePathChanged: Qt.callLater(_syncPages) onSessionVisibleChanged: Qt.callLater(_syncPages) - component Slide: Transition { - id: slide + component PageTransition: Transition { + id: transition + property bool entering: true property real fromX: 0 property real toX: 0 NumberAnimation { property: "x" - from: slide.fromX - to: slide.toX + from: transition.fromX + to: transition.toX duration: SettingsMetrics.transitionDuration 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 + easing.type: Easing.BezierSpline + easing.bezierCurve: Theme.expressiveCurves.expressiveEffects + } } function _scrollerOf(item) { @@ -282,21 +316,35 @@ FocusScope { Component { id: pageComponent - // StackView writes visible and opacity on the element it manages, so the presentation gate lives on the inner Loader Item { id: host required property string page - readonly property bool pageActive: root.sessionVisible && pageStack.currentItem === host + required property int pathIndex + readonly property bool pageActive: root.sessionVisible && page === root.currentPage && pageStack.currentItem === host readonly property alias item: loader.item readonly property int status: loader.status readonly property bool settled: !pending && loader.status !== Loader.Loading && (presented || loader.status !== Loader.Ready) + readonly property var pageInfo: SettingsTabs.page(page) + readonly property string pageTitle: (pageInfo?.titleFrom ? SettingsUiState[pageInfo.titleFrom] : "") || (pageInfo?.text ?? "") + readonly property bool showBack: canGoBack || SettingsTabs.isPluginPage(page) || root.menuHidden + readonly property string parentId: SettingsTabs.parentOf(page) + readonly property bool canGoBack: pathIndex > 0 || (parentId !== "" && (SettingsTabs.isPluginPage(page) || SettingsTabs.visibleLeaves(parentId).length > 1 || !!SettingsTabs.page(parentId)?.hubHeader)) property Item rememberedFocus: null property bool pending: true property bool presented: false - enabled: pageActive + width: pageStack.width + height: pageStack.height + opacity: 0 + enabled: pageActive && presented + + StackView.onRemoved: destroy() + onSettledChanged: { + if (settled && root.pendingPage === host) + Qt.callLater(root._syncPages); + } function load(deferred) { const file = root._fileFor(page); @@ -308,11 +356,56 @@ FocusScope { pending = false; } + Item { + id: pageHeader + width: Math.min(host.item?.contentMaxWidth ?? SettingsMetrics.contentMaxWidth, parent.width - SettingsMetrics.panePadding * 2) + anchors.horizontalCenter: parent.horizontalCenter + height: Math.max(SettingsMetrics.pageHeaderHeight, pageHeading.implicitHeight + Theme.spacingM * 2) + + Item { + id: backSlot + + readonly property real glyphInset: (Theme.iconButtonSize - Theme.iconSize) / 2 + + anchors.left: parent.left + anchors.leftMargin: host.showBack ? -glyphInset : 0 + anchors.verticalCenter: parent.verticalCenter + width: host.showBack ? Theme.iconButtonSize + Theme.spacingM - glyphInset : 0 + height: Theme.iconButtonSize + + DActionButton { + buttonSize: Theme.iconButtonSize + iconName: I18n.isRtl ? "arrow_forward" : "arrow_back" + Accessible.name: I18n.tr("Back") + iconSize: Theme.iconSize + iconColor: Theme.surfaceText + visible: host.showBack + onClicked: root.goBack() + } + } + + StyledText { + id: pageHeading + anchors.left: backSlot.right + anchors.right: parent.right + wrapMode: Text.WordWrap + anchors.verticalCenter: parent.verticalCenter + text: host.pageTitle + font.pixelSize: Theme.fontSizeXXLarge + color: Theme.surfaceText + visible: host.pageTitle !== "" + } + } + Loader { id: loader - anchors.fill: parent - opacity: host.presented ? 1 : 0 + anchors.top: pageHeader.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.leftMargin: SettingsMetrics.panePadding + anchors.rightMargin: SettingsMetrics.panePadding enabled: host.presented onLoaded: { @@ -428,95 +521,59 @@ FocusScope { readonly property var pagesWithParentModal: ["dankbar_widgets", "window_rules", "notification_rules", "display_config", "users", "time_weather", "weather", "lock_screen", "greeter", "dank_dash", "wallpaper_cycling", "theme_schedule", "surface_shadows", "keybinds", "dankbar_settings", "dankbar_appearance", "bar_widget", "dock_general", "dock_widgets", "dock_appearance", "dock_advanced", "launcher", "theme", "theme_apps", "media_player", "desktop_widgets", "desktop_widget", "autostart", "compositor_layout", "updater", "display_gamma"] - Column { + StackView { + id: pageStack + anchors.fill: parent - anchors.leftMargin: SettingsMetrics.panePadding - anchors.rightMargin: SettingsMetrics.panePadding - spacing: 0 + clip: true - Item { - id: pageHeader - width: Math.min(root.pageContentMaxWidth, parent.width) - anchors.horizontalCenter: parent.horizontalCenter - height: Math.max(SettingsMetrics.pageHeaderHeight, pageHeading.implicitHeight + Theme.spacingM * 2) + readonly property real pageOffset: SettingsData.reduceMotion ? 0 : (I18n.isRtl ? -Theme.spacingXL : Theme.spacingXL) - Item { - id: backSlot - - readonly property real glyphInset: (Theme.iconButtonSize - Theme.iconSize) / 2 - - anchors.left: parent.left - anchors.leftMargin: root.showBack ? -glyphInset : 0 - anchors.verticalCenter: parent.verticalCenter - width: root.showBack ? Theme.iconButtonSize + Theme.spacingM - glyphInset : 0 - height: Theme.iconButtonSize - - DActionButton { - buttonSize: Theme.iconButtonSize - iconName: I18n.isRtl ? "arrow_forward" : "arrow_back" - Accessible.name: I18n.tr("Back") - iconSize: Theme.iconSize - iconColor: Theme.surfaceText - visible: root.showBack - onClicked: root.goBack() - } - } - - StyledText { - id: pageHeading - anchors.left: backSlot.right - anchors.right: parent.right - wrapMode: Text.WordWrap - anchors.verticalCenter: parent.verticalCenter - text: root.pageTitle - font.pixelSize: Theme.fontSizeXXLarge - color: Theme.surfaceText - visible: root.pageTitle !== "" - } + onBusyChanged: { + if (!busy) + Qt.callLater(root._syncPages); } - StackView { - id: pageStack - width: parent.width - height: parent.height - pageHeader.height - clip: true + pushEnter: PageTransition { + fromX: pageStack.pageOffset + } + pushExit: PageTransition { + entering: false + toX: -pageStack.pageOffset + } + popEnter: PageTransition { + fromX: -pageStack.pageOffset + } + popExit: PageTransition { + entering: false + toX: pageStack.pageOffset + } + replaceEnter: PageTransition {} + replaceExit: PageTransition { + entering: false + } - readonly property real slideOffscreen: I18n.isRtl ? -width : width + DSpinner { + id: pageSpinner - pushEnter: Slide { - fromX: pageStack.slideOffscreen - } - pushExit: Slide { - toX: -pageStack.slideOffscreen - } - popEnter: Slide { - fromX: -pageStack.slideOffscreen - } - popExit: Slide { - toX: pageStack.slideOffscreen - } + readonly property bool loading: root.pendingPage !== null - DSpinner { - id: pageSpinner - - readonly property bool loading: pageStack.currentItem?.presented === false - - anchors.centerIn: parent - visible: false - onLoadingChanged: { - if (!loading) { - spinnerDelay.stop(); - visible = false; - return; - } - spinnerDelay.restart(); + anchors.centerIn: parent + z: pageStack.depth + visible: false + onLoadingChanged: { + if (!loading) { + spinnerDelay.stop(); + visible = false; + return; } + spinnerDelay.restart(); + } - Timer { - id: spinnerDelay - interval: SettingsMetrics.pageSettleDeadline + SettingsMetrics.fadeDuration - onTriggered: pageSpinner.visible = pageSpinner.loading - } + Timer { + id: spinnerDelay + interval: SettingsMetrics.pageSettleDeadline + SettingsMetrics.fadeDuration + onTriggered: pageSpinner.visible = pageSpinner.loading } } } diff --git a/quickshell/Modals/Settings/SettingsModal.qml b/quickshell/Modals/Settings/SettingsModal.qml index a41c9dc8c..2367f796f 100644 --- a/quickshell/Modals/Settings/SettingsModal.qml +++ b/quickshell/Modals/Settings/SettingsModal.qml @@ -442,7 +442,7 @@ DFloatingWindow { clip: true Behavior on x { - enabled: settingsModal.isCompactMode && Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None + enabled: settingsModal.isCompactMode && !SettingsData.reduceMotion && Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None NumberAnimation { duration: SettingsMetrics.transitionDuration easing.type: Easing.BezierSpline