From c4a83d7e0b5687ffe17a6726fad5918788600f4f Mon Sep 17 00:00:00 2001 From: Lemmy Date: Sun, 8 Feb 2026 18:40:57 -0500 Subject: [PATCH] settings: improved auto-nav to subtabs and highlight focus --- Modules/Panels/Settings/SettingsContent.qml | 55 +++++++++++-------- Modules/Panels/Settings/SettingsPanel.qml | 5 +- .../Panels/Settings/SettingsPanelWindow.qml | 6 +- .../Tabs/Display/BrightnessSubTab.qml | 2 +- Scripts/dev/build-settings-search-index.py | 5 ++ Widgets/NTabView.qml | 24 ++++++++ 6 files changed, 67 insertions(+), 30 deletions(-) diff --git a/Modules/Panels/Settings/SettingsContent.qml b/Modules/Panels/Settings/SettingsContent.qml index 04b66cbdb..91e0de1fc 100644 --- a/Modules/Panels/Settings/SettingsContent.qml +++ b/Modules/Panels/Settings/SettingsContent.qml @@ -231,11 +231,12 @@ Item { } } - // Set sub-tab on the currently loaded tab content + // Set sub-tab on the currently loaded tab content. Returns true if an NTabBar was found. function setSubTabIndex(subTabIndex) { if (activeTabContent) { - setSubTabRecursive(activeTabContent, subTabIndex); + return setSubTabRecursive(activeTabContent, subTabIndex); } + return false; } function setSubTabRecursive(item, subTabIndex) { @@ -243,6 +244,16 @@ Item { return false; if (item.objectName === "NTabBar") { + // Prepare the sibling NTabView so the index change doesn't animate + if (item.parent) { + for (let j = 0; j < item.parent.children.length; j++) { + const sibling = item.parent.children[j]; + if (sibling.objectName === "NTabView" && sibling.setIndexWithoutAnimation) { + sibling.setIndexWithoutAnimation(subTabIndex); + break; + } + } + } item.currentIndex = subTabIndex; return true; } @@ -346,21 +357,21 @@ Item { if (root.activeTabContent && targetKey) { const widget = root.findAndHighlightWidget(root.activeTabContent, targetKey); if (widget && root.activeScrollView) { - // Scroll widget into view - const mapped = widget.mapToItem(root.activeScrollView.contentItem, 0, 0); - const scrollBar = root.activeScrollView.ScrollBar.vertical; - if (scrollBar) { - const targetPos = (mapped.y - root.activeScrollView.height / 3) / root.activeScrollView.contentHeight; - scrollBar.position = Math.max(0, Math.min(targetPos, 1.0 - scrollBar.size)); - } + // Scroll widget into view using the Flickable directly + const flickable = root.activeScrollView.contentItem; + const mapped = widget.mapToItem(flickable.contentItem, 0, 0); + const targetY = mapped.y - flickable.height / 3; + flickable.contentY = Math.max(0, Math.min(targetY, flickable.contentHeight - flickable.height)); - // Position highlight overlay - const overlayPos = widget.mapToItem(tabContentArea, 0, 0); - highlightOverlay.x = overlayPos.x - Style.marginM; - highlightOverlay.y = overlayPos.y - Style.marginM; - highlightOverlay.width = widget.width + Style.marginM * 2; - highlightOverlay.height = widget.height + Style.marginM * 2; - highlightAnimation.restart(); + // Position highlight overlay after scroll layout has settled + Qt.callLater(function () { + const overlayPos = widget.mapToItem(tabContentArea, 0, 0); + highlightOverlay.x = overlayPos.x - Style.marginM; + highlightOverlay.y = overlayPos.y - Style.marginM; + highlightOverlay.width = widget.width + Style.marginM * 2; + highlightOverlay.height = widget.height + Style.marginM * 2; + highlightAnimation.restart(); + }); } } targetKey = ""; @@ -1226,13 +1237,13 @@ Item { item.screen = root.screen; } root.activeTabContent = item; - if (root.highlightLabelKey) { - if (root._pendingSubTab >= 0) { - root.navigatingFromSearch = true; - root.setSubTabIndex(root._pendingSubTab); - root.navigatingFromSearch = false; + if (root._pendingSubTab >= 0) { + root.navigatingFromSearch = true; + if (root.setSubTabIndex(root._pendingSubTab)) root._pendingSubTab = -1; - } + root.navigatingFromSearch = false; + } + if (root.highlightLabelKey) { highlightScrollTimer.targetKey = root.highlightLabelKey; highlightScrollTimer.restart(); } diff --git a/Modules/Panels/Settings/SettingsPanel.qml b/Modules/Panels/Settings/SettingsPanel.qml index 0ae83bef6..331d6eaa9 100644 --- a/Modules/Panels/Settings/SettingsPanel.qml +++ b/Modules/Panels/Settings/SettingsPanel.qml @@ -165,12 +165,11 @@ SmartPanel { Qt.callLater(() => _settingsContent.navigateToResult(entry)); } else { _settingsContent.requestedTab = requestedTab; - _settingsContent.initialize(); if (requestedSubTab >= 0) { - const subTab = requestedSubTab; + _settingsContent._pendingSubTab = requestedSubTab; requestedSubTab = -1; - Qt.callLater(() => _settingsContent.navigateToTab(requestedTab, subTab)); } + _settingsContent.initialize(); } } } diff --git a/Modules/Panels/Settings/SettingsPanelWindow.qml b/Modules/Panels/Settings/SettingsPanelWindow.qml index 49e9f5feb..6c556f3c3 100644 --- a/Modules/Panels/Settings/SettingsPanelWindow.qml +++ b/Modules/Panels/Settings/SettingsPanelWindow.qml @@ -38,13 +38,11 @@ FloatingWindow { Qt.callLater(() => settingsContent.navigateToResult(entry)); } else { settingsContent.requestedTab = SettingsPanelService.requestedTab; - settingsContent.initialize(); if (SettingsPanelService.requestedSubTab >= 0) { - const tab = SettingsPanelService.requestedTab; - const subTab = SettingsPanelService.requestedSubTab; + settingsContent._pendingSubTab = SettingsPanelService.requestedSubTab; SettingsPanelService.requestedSubTab = -1; - Qt.callLater(() => settingsContent.navigateToTab(tab, subTab)); } + settingsContent.initialize(); } isInitialized = true; } diff --git a/Modules/Panels/Settings/Tabs/Display/BrightnessSubTab.qml b/Modules/Panels/Settings/Tabs/Display/BrightnessSubTab.qml index ccdad4936..37fcc350f 100644 --- a/Modules/Panels/Settings/Tabs/Display/BrightnessSubTab.qml +++ b/Modules/Panels/Settings/Tabs/Display/BrightnessSubTab.qml @@ -19,7 +19,7 @@ ColumnLayout { model: Quickshell.screens || [] delegate: NBox { Layout.fillWidth: true - implicitHeight: contentCol.implicitHeight + Style.marginL * 2 + implicitHeight: Math.round(contentCol.implicitHeight + Style.marginL * 2) color: Color.mSurface property var brightnessMonitor: BrightnessService.getMonitorForScreen(modelData) diff --git a/Scripts/dev/build-settings-search-index.py b/Scripts/dev/build-settings-search-index.py index 5b541b4b0..eeee74aed 100755 --- a/Scripts/dev/build-settings-search-index.py +++ b/Scripts/dev/build-settings-search-index.py @@ -225,6 +225,11 @@ def resolve_tab_info( sub_label = subtab_labels[idx] if idx < len(subtab_labels) else None return tab_index, tab_label, idx, sub_label except ValueError: + # File doesn't map to any subtab (e.g. a dialog). If the parent tab + # has subtabs, the focus ring can't reach widgets inside dialogs, so + # exclude them from the index. + if subtab_names: + return None, None, None, None return tab_index, tab_label, None, None diff --git a/Widgets/NTabView.qml b/Widgets/NTabView.qml index e4638e5f0..309dadc89 100644 --- a/Widgets/NTabView.qml +++ b/Widgets/NTabView.qml @@ -4,6 +4,7 @@ import qs.Commons Item { id: root + objectName: "NTabView" property int currentIndex: 0 @@ -29,6 +30,29 @@ Item { anchors.fill: parent } + // Set the visible tab to idx without triggering a slide animation. + // Call this BEFORE the bound currentIndex changes so that + // onCurrentIndexChanged sees previousIndex === currentIndex and skips. + function setIndexWithoutAnimation(idx) { + fromXAnim.stop(); + fromOpacityAnim.stop(); + toXAnim.stop(); + toOpacityAnim.stop(); + animating = false; + previousIndex = idx; + for (let i = 0; i < contentItems.length; i++) { + if (i === idx) { + contentItems[i].x = 0; + contentItems[i].visible = true; + contentItems[i].opacity = 1.0; + } else { + contentItems[i].x = root.width; + contentItems[i].visible = false; + contentItems[i].opacity = 1.0; + } + } + } + Component.onCompleted: { _initializeItems(); }