From d35f399f535e31c53fd60409cae6a95214255dd9 Mon Sep 17 00:00:00 2001 From: Lemmy Date: Wed, 11 Mar 2026 10:07:30 -0400 Subject: [PATCH] feat(settings): improved search index to support visibility conditions, prevent showing results that would be invisible to the user. Fix #2113 --- Assets/settings-search-index.json | 275 ++++++++++++++++---- Modules/Panels/Settings/SettingsContent.qml | 143 +++++++++- Scripts/dev/build-settings-search-index.py | 94 ++++++- 3 files changed, 451 insertions(+), 61 deletions(-) diff --git a/Assets/settings-search-index.json b/Assets/settings-search-index.json index 057cfcda9..bf07197e0 100644 --- a/Assets/settings-search-index.json +++ b/Assets/settings-search-index.json @@ -195,7 +195,10 @@ "tab": 4, "tabLabel": "panels.bar.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.bar.useSeparateOpacity" + ] }, { "labelKey": "panels.bar.appearance-font-scale-label", @@ -249,7 +252,10 @@ "tab": 4, "tabLabel": "panels.bar.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.bar.showCapsule" + ] }, { "labelKey": "panels.bar.appearance-capsule-opacity-label", @@ -258,7 +264,10 @@ "tab": 4, "tabLabel": "panels.bar.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.bar.showCapsule" + ] }, { "labelKey": "panels.bar.appearance-enable-exclusion-zone-inset-label", @@ -276,7 +285,10 @@ "tab": 4, "tabLabel": "panels.bar.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "CompositorService.isNiri" + ] }, { "labelKey": "panels.bar.appearance-outer-corners-label", @@ -285,7 +297,10 @@ "tab": 4, "tabLabel": "panels.bar.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.bar.barType === \"simple\"" + ] }, { "labelKey": "panels.bar.appearance-frame-settings-label", @@ -294,7 +309,10 @@ "tab": 4, "tabLabel": "panels.bar.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.bar.barType === \"framed\"" + ] }, { "labelKey": "panels.bar.appearance-frame-thickness", @@ -303,7 +321,10 @@ "tab": 4, "tabLabel": "panels.bar.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.bar.barType === \"framed\"" + ] }, { "labelKey": "panels.bar.appearance-frame-radius", @@ -312,7 +333,10 @@ "tab": 4, "tabLabel": "panels.bar.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.bar.barType === \"framed\"" + ] }, { "labelKey": "panels.bar.appearance-margins-vertical", @@ -321,7 +345,10 @@ "tab": 4, "tabLabel": "panels.bar.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.bar.barType === \"floating\"" + ] }, { "labelKey": "panels.bar.appearance-margins-horizontal", @@ -330,7 +357,10 @@ "tab": 4, "tabLabel": "panels.bar.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.bar.barType === \"floating\"" + ] }, { "labelKey": "panels.bar.appearance-auto-hide-delay-label", @@ -339,7 +369,10 @@ "tab": 4, "tabLabel": "panels.bar.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.bar.displayMode === \"auto_hide\"" + ] }, { "labelKey": "panels.bar.appearance-auto-show-delay-label", @@ -348,7 +381,10 @@ "tab": 4, "tabLabel": "panels.bar.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.bar.displayMode === \"auto_hide\"" + ] }, { "labelKey": "panels.bar.appearance-show-on-workspace-switch-label", @@ -357,7 +393,10 @@ "tab": 4, "tabLabel": "panels.bar.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.bar.displayMode === \"auto_hide\"" + ] }, { "labelKey": "panels.bar.behavior-workspace-scroll-label", @@ -375,7 +414,10 @@ "tab": 4, "tabLabel": "panels.bar.title", "subTab": 2, - "subTabLabel": "common.behavior" + "subTabLabel": "common.behavior", + "visibleWhen": [ + "Settings.data.bar.mouseWheelAction !== \"none\"" + ] }, { "labelKey": "panels.bar.behavior-wheel-wrap-label", @@ -384,7 +426,10 @@ "tab": 4, "tabLabel": "panels.bar.title", "subTab": 2, - "subTabLabel": "common.behavior" + "subTabLabel": "common.behavior", + "visibleWhen": [ + "Settings.data.bar.mouseWheelAction === \"workspace\"" + ] }, { "labelKey": "panels.bar.behavior-middle-click-label", @@ -402,7 +447,10 @@ "tab": 4, "tabLabel": "panels.bar.title", "subTab": 2, - "subTabLabel": "common.behavior" + "subTabLabel": "common.behavior", + "visibleWhen": [ + "Settings.data.bar.middleClickAction === \"command\"" + ] }, { "labelKey": "panels.bar.behavior-middle-click-follow-mouse-label", @@ -411,7 +459,10 @@ "tab": 4, "tabLabel": "panels.bar.title", "subTab": 2, - "subTabLabel": "common.behavior" + "subTabLabel": "common.behavior", + "visibleWhen": [ + "Settings.data.bar.middleClickAction !== \"none\" && Settings.data.bar.middleClickAction !== \"command\" && !(Settings.data.bar.middleClickAction === \"settings\" && Settings.data.ui.settingsPanelMode === \"window\")" + ] }, { "labelKey": "panels.bar.behavior-right-click-label", @@ -429,7 +480,10 @@ "tab": 4, "tabLabel": "panels.bar.title", "subTab": 2, - "subTabLabel": "common.behavior" + "subTabLabel": "common.behavior", + "visibleWhen": [ + "Settings.data.bar.rightClickAction === \"command\"" + ] }, { "labelKey": "panels.bar.behavior-right-click-follow-mouse-label", @@ -438,7 +492,10 @@ "tab": 4, "tabLabel": "panels.bar.title", "subTab": 2, - "subTabLabel": "common.behavior" + "subTabLabel": "common.behavior", + "visibleWhen": [ + "Settings.data.bar.rightClickAction !== \"none\" && Settings.data.bar.rightClickAction !== \"command\" && !(Settings.data.bar.rightClickAction === \"settings\" && Settings.data.ui.settingsPanelMode === \"window\")" + ] }, { "labelKey": "panels.bar.monitor-override-settings", @@ -474,7 +531,10 @@ "tab": 2, "tabLabel": "panels.color-scheme.title", "subTab": 0, - "subTabLabel": "common.colors" + "subTabLabel": "common.colors", + "visibleWhen": [ + "Settings.data.colorSchemes.schedulingMode === \"manual\"" + ] }, { "labelKey": "panels.color-scheme.color-source-use-wallpaper-colors-label", @@ -591,7 +651,10 @@ "tab": 16, "tabLabel": "panels.connections.title", "subTab": 1, - "subTabLabel": "common.bluetooth" + "subTabLabel": "common.bluetooth", + "visibleWhen": [ + "Settings.data.network.bluetoothRssiPollingEnabled" + ] }, { "labelKey": "toast.airplane-mode.title", @@ -760,7 +823,10 @@ "tab": 5, "tabLabel": "panels.dock.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.dock.dockType === \"floating\"" + ] }, { "labelKey": "panels.dock.appearance-sit-on-frame-label", @@ -769,7 +835,10 @@ "tab": 5, "tabLabel": "panels.dock.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.dock.dockType === \"static\" && Settings.data.bar.barType === \"framed\"" + ] }, { "labelKey": "panels.dock.appearance-dock-indicator-label", @@ -787,7 +856,10 @@ "tab": 5, "tabLabel": "panels.dock.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.dock.showDockIndicator" + ] }, { "labelKey": "panels.dock.appearance-indicator-color-label", @@ -796,7 +868,10 @@ "tab": 5, "tabLabel": "panels.dock.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.dock.showDockIndicator" + ] }, { "labelKey": "panels.dock.appearance-indicator-opacity-label", @@ -805,7 +880,10 @@ "tab": 5, "tabLabel": "panels.dock.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.dock.showDockIndicator" + ] }, { "labelKey": "panels.osd.background-opacity-label", @@ -832,7 +910,10 @@ "tab": 5, "tabLabel": "panels.dock.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.dock.dockType === \"floating\"" + ] }, { "labelKey": "panels.dock.appearance-icon-size-label", @@ -850,7 +931,10 @@ "tab": 5, "tabLabel": "panels.dock.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.dock.dockType === \"floating\" && Settings.data.dock.displayMode === \"auto_hide\"" + ] }, { "labelKey": "panels.dock.appearance-inactive-indicators-label", @@ -886,7 +970,10 @@ "tab": 5, "tabLabel": "panels.dock.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.dock.groupApps" + ] }, { "labelKey": "panels.dock.appearance-group-context-menu-mode-label", @@ -895,7 +982,10 @@ "tab": 5, "tabLabel": "panels.dock.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.dock.groupApps" + ] }, { "labelKey": "panels.dock.appearance-group-indicator-style-label", @@ -904,7 +994,10 @@ "tab": 5, "tabLabel": "panels.dock.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.dock.groupApps" + ] }, { "labelKey": "panels.dock.monitors-only-same-monitor-label", @@ -940,7 +1033,10 @@ "tab": 5, "tabLabel": "panels.dock.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.dock.showLauncherIcon" + ] }, { "labelKey": "common.select-icon-color", @@ -949,7 +1045,10 @@ "tab": 5, "tabLabel": "panels.dock.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.dock.showLauncherIcon" + ] }, { "labelKey": "panels.general.profile-picture-label", @@ -1105,7 +1204,10 @@ "tab": 13, "tabLabel": "panels.idle.title", "subTab": 0, - "subTabLabel": "panels.idle.tab-behavior" + "subTabLabel": "panels.idle.tab-behavior", + "visibleWhen": [ + "IdleService.nativeIdleMonitorAvailable" + ] }, { "labelKey": "panels.idle.timeouts-label", @@ -1177,7 +1279,10 @@ "tab": 8, "tabLabel": "panels.launcher.title", "subTab": 1, - "subTabLabel": "common.clipboard" + "subTabLabel": "common.clipboard", + "visibleWhen": [ + "Settings.data.appLauncher.enableClipboardHistory" + ] }, { "labelKey": "panels.launcher.settings-clipboard-watch-image-label", @@ -1186,7 +1291,10 @@ "tab": 8, "tabLabel": "panels.launcher.title", "subTab": 1, - "subTabLabel": "common.clipboard" + "subTabLabel": "common.clipboard", + "visibleWhen": [ + "Settings.data.appLauncher.enableClipboardHistory" + ] }, { "labelKey": "panels.launcher.settings-use-app2unit-label", @@ -1222,7 +1330,10 @@ "tab": 8, "tabLabel": "panels.launcher.title", "subTab": 2, - "subTabLabel": "common.execute" + "subTabLabel": "common.execute", + "visibleWhen": [ + "Settings.data.appLauncher.customLaunchPrefixEnabled" + ] }, { "labelKey": "panels.launcher.settings-annotation-tool-label", @@ -1348,7 +1459,10 @@ "tab": 11, "tabLabel": "panels.lock-screen.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.general.clockStyle === \"custom\"" + ] }, { "labelKey": "panels.lock-screen.password-chars-label", @@ -1375,7 +1489,10 @@ "tab": 11, "tabLabel": "panels.lock-screen.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "!Settings.data.general.compactLockScreen" + ] }, { "labelKey": "panels.lock-screen.lock-screen-animations-label", @@ -1447,7 +1564,10 @@ "tab": 11, "tabLabel": "panels.lock-screen.title", "subTab": 1, - "subTabLabel": "common.behavior" + "subTabLabel": "common.behavior", + "visibleWhen": [ + "Settings.data.general.showSessionButtonsOnLockScreen" + ] }, { "labelKey": "panels.session-menu.enable-countdown-label", @@ -1456,7 +1576,10 @@ "tab": 11, "tabLabel": "panels.lock-screen.title", "subTab": 1, - "subTabLabel": "common.behavior" + "subTabLabel": "common.behavior", + "visibleWhen": [ + "Settings.data.general.showSessionButtonsOnLockScreen" + ] }, { "labelKey": "panels.session-menu.countdown-duration-label", @@ -1465,7 +1588,10 @@ "tab": 11, "tabLabel": "panels.lock-screen.title", "subTab": 1, - "subTabLabel": "common.behavior" + "subTabLabel": "common.behavior", + "visibleWhen": [ + "Settings.data.general.showSessionButtonsOnLockScreen && Settings.data.general.enableLockScreenCountdown" + ] }, { "labelKey": "panels.notifications.duration-respect-expire-label", @@ -1591,7 +1717,10 @@ "tab": 9, "tabLabel": "common.notifications", "subTab": 3, - "subTabLabel": "common.sound" + "subTabLabel": "common.sound", + "visibleWhen": [ + "!SoundService.multimediaAvailable" + ] }, { "labelKey": "panels.notifications.sounds-enabled-label", @@ -1627,7 +1756,10 @@ "tab": 9, "tabLabel": "common.notifications", "subTab": 3, - "subTabLabel": "common.sound" + "subTabLabel": "common.sound", + "visibleWhen": [ + "!(Settings.data.notifications?.sounds?.separateSounds ?? false)" + ] }, { "labelKey": "panels.notifications.sounds-files-low-label", @@ -1636,7 +1768,10 @@ "tab": 9, "tabLabel": "common.notifications", "subTab": 3, - "subTabLabel": "common.sound" + "subTabLabel": "common.sound", + "visibleWhen": [ + "SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false) && (Settings.data.notifications?.sounds?.separateSounds ?? false)" + ] }, { "labelKey": "panels.notifications.sounds-files-normal-label", @@ -1645,7 +1780,10 @@ "tab": 9, "tabLabel": "common.notifications", "subTab": 3, - "subTabLabel": "common.sound" + "subTabLabel": "common.sound", + "visibleWhen": [ + "SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false) && (Settings.data.notifications?.sounds?.separateSounds ?? false)" + ] }, { "labelKey": "panels.notifications.sounds-files-critical-label", @@ -1654,7 +1792,10 @@ "tab": 9, "tabLabel": "common.notifications", "subTab": 3, - "subTabLabel": "common.sound" + "subTabLabel": "common.sound", + "visibleWhen": [ + "SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false) && (Settings.data.notifications?.sounds?.separateSounds ?? false)" + ] }, { "labelKey": "panels.notifications.sounds-excluded-apps-label", @@ -1879,7 +2020,10 @@ "tab": 12, "tabLabel": "session-menu.title", "subTab": 0, - "subTabLabel": "common.general" + "subTabLabel": "common.general", + "visibleWhen": [ + "Settings.data.sessionMenu.largeButtonsStyle" + ] }, { "labelKey": "panels.session-menu.show-header-label", @@ -1888,7 +2032,10 @@ "tab": 12, "tabLabel": "session-menu.title", "subTab": 0, - "subTabLabel": "common.general" + "subTabLabel": "common.general", + "visibleWhen": [ + "!Settings.data.sessionMenu.largeButtonsStyle" + ] }, { "labelKey": "panels.session-menu.show-keybinds-label", @@ -2005,7 +2152,10 @@ "tab": 1, "tabLabel": "panels.user-interface.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.general.enableShadows" + ] }, { "labelKey": "panels.user-interface.scaling-label", @@ -2050,7 +2200,10 @@ "tab": 1, "tabLabel": "panels.user-interface.title", "subTab": 0, - "subTabLabel": "common.appearance" + "subTabLabel": "common.appearance", + "visibleWhen": [ + "!Settings.data.general.animationDisabled" + ] }, { "labelKey": "panels.user-interface.panels-attached-to-bar-label", @@ -2068,7 +2221,10 @@ "tab": 1, "tabLabel": "panels.user-interface.title", "subTab": 1, - "subTabLabel": "common.panels" + "subTabLabel": "common.panels", + "visibleWhen": [ + "(Quickshell.screens.length > 1)" + ] }, { "labelKey": "panels.user-interface.panel-background-opacity-label", @@ -2221,7 +2377,10 @@ "tab": 3, "tabLabel": "common.wallpaper", "subTab": 0, - "subTabLabel": "common.general" + "subTabLabel": "common.general", + "visibleWhen": [ + "CompositorService.isNiri" + ] }, { "labelKey": "panels.wallpaper.settings-overview-blur-strength-label", @@ -2230,7 +2389,10 @@ "tab": 3, "tabLabel": "common.wallpaper", "subTab": 0, - "subTabLabel": "common.general" + "subTabLabel": "common.general", + "visibleWhen": [ + "CompositorService.isNiri" + ] }, { "labelKey": "panels.wallpaper.settings-overview-tint-label", @@ -2239,7 +2401,10 @@ "tab": 3, "tabLabel": "common.wallpaper", "subTab": 0, - "subTabLabel": "common.general" + "subTabLabel": "common.general", + "visibleWhen": [ + "CompositorService.isNiri" + ] }, { "labelKey": "panels.wallpaper.look-feel-fill-mode-label", diff --git a/Modules/Panels/Settings/SettingsContent.qml b/Modules/Panels/Settings/SettingsContent.qml index 957d42883..d4c6cf1cd 100644 --- a/Modules/Panels/Settings/SettingsContent.qml +++ b/Modules/Panels/Settings/SettingsContent.qml @@ -25,6 +25,8 @@ import qs.Modules.Panels.Settings.Tabs.SessionMenu import qs.Modules.Panels.Settings.Tabs.SystemMonitor import qs.Modules.Panels.Settings.Tabs.UserInterface import qs.Modules.Panels.Settings.Tabs.Wallpaper +import qs.Services.Compositor +import qs.Services.Power import qs.Services.System import qs.Services.UI import qs.Widgets @@ -90,6 +92,143 @@ Item { } } + // Visibility condition evaluation for search filtering. + // Resolves a dotted property path on a known root object. + function _resolveValue(path) { + const roots = { + "CompositorService": CompositorService, + "Settings": Settings, + "Quickshell": Quickshell, + "IdleService": IdleService, + "SystemStatService": SystemStatService, + "SoundService": SoundService + }; + + const parts = path.split("."); + const rootObj = roots[parts[0]]; + if (rootObj === undefined) + return undefined; + + let obj = rootObj; + for (let i = 1; i < parts.length; i++) { + if (obj === undefined || obj === null) + return undefined; + // Strip optional chaining marker (e.g. "sounds?" -> "sounds") + let key = parts[i]; + if (key.endsWith("?")) + key = key.slice(0, -1); + obj = obj[key]; + } + return obj; + } + + // Split an expression on top-level && (respecting parentheses). + function _splitAnd(expr) { + const parts = []; + let depth = 0; + let current = ""; + for (let i = 0; i < expr.length; i++) { + const ch = expr[i]; + if (ch === "(") + depth++; + else if (ch === ")") + depth--; + else if (depth === 0 && ch === "&" && i + 1 < expr.length && expr[i + 1] === "&") { + parts.push(current); + current = ""; + i++; // skip second & + continue; + } + current += ch; + } + parts.push(current); + return parts; + } + + // Evaluate a single visibility condition expression. + // Returns true if the condition is met (item should be visible). + // Falls back to true for unrecognized expressions. + function _evalCondition(expr) { + expr = expr.trim(); + + // Strip outer parentheses + if (expr.startsWith("(") && expr.endsWith(")")) { + let depth = 0; + let allWrapped = true; + for (let i = 0; i < expr.length - 1; i++) { + if (expr[i] === "(") + depth++; + else if (expr[i] === ")") + depth--; + if (depth === 0) { + allWrapped = false; + break; + } + } + if (allWrapped) + return _evalCondition(expr.slice(1, -1)); + } + + // AND: all parts must be true + if (expr.includes("&&")) { + const parts = _splitAnd(expr); + if (parts.length > 1) { + for (let i = 0; i < parts.length; i++) { + if (!_evalCondition(parts[i])) + return false; + } + return true; + } + } + + // Negation + if (expr.startsWith("!")) + return !_evalCondition(expr.slice(1).trim()); + + // Literal false + if (expr === "false") + return false; + + // Strip nullish coalescing fallback (e.g. "expr ?? false") + const nullishMatch = expr.match(/^(.+?)\s*\?\?\s*(?:false|true)\s*$/); + if (nullishMatch) + return _evalCondition(nullishMatch[1]); + + // === comparison + let m = expr.match(/^(.+?)\s*===\s*"([^"]*)"\s*$/); + if (m) + return _resolveValue(m[1].trim()) === m[2]; + + // !== comparison + m = expr.match(/^(.+?)\s*!==\s*"([^"]*)"\s*$/); + if (m) + return _resolveValue(m[1].trim()) !== m[2]; + + // > comparison + m = expr.match(/^(.+?)\s*>\s*(\d+)\s*$/); + if (m) + return _resolveValue(m[1].trim()) > parseInt(m[2]); + + // Simple property path — resolve and return truthiness + const val = _resolveValue(expr); + if (val !== undefined) + return !!val; + + // Unrecognized expression — assume visible + return true; + } + + // Check if a search index entry is currently visible. + function _isEntryVisible(entry) { + if (!entry.visibleWhen || entry.visibleWhen.length === 0) + return true; + for (let i = 0; i < entry.visibleWhen.length; i++) { + if (!_evalCondition(entry.visibleWhen[i])) + return false; + } + return true; + } + // Search function onSearchTextChanged: { if (searchText.trim() === "") { @@ -113,10 +252,12 @@ Item { if (searchIndex.length === 0) return; - // Build searchable items with resolved translations + // Build searchable items with resolved translations, filtering out invisible entries let items = []; for (let j = 0; j < searchIndex.length; j++) { const entry = searchIndex[j]; + if (!_isEntryVisible(entry)) + continue; items.push({ "labelKey": entry.labelKey, "descriptionKey": entry.descriptionKey, diff --git a/Scripts/dev/build-settings-search-index.py b/Scripts/dev/build-settings-search-index.py index 50f82a420..aa5e760e5 100755 --- a/Scripts/dev/build-settings-search-index.py +++ b/Scripts/dev/build-settings-search-index.py @@ -3,7 +3,7 @@ Build settings search index from QML source files. Parses settings tab QML files to extract searchable metadata -(i18n keys, widget types, tab/sub-tab locations). +(i18n keys, widget types, tab/sub-tab locations, visibility conditions). Output: Assets/settings-search-index.json @@ -42,6 +42,21 @@ RE_WIDGET_OPEN = re.compile( ) RE_LABEL = re.compile(r'label:\s*I18n\.tr\("([^"]+)"') RE_DESCRIPTION = re.compile(r'description:\s*I18n\.tr\("([^"]+)"') +RE_VISIBLE = re.compile(r'^\s*visible:\s*(.+?)(?:\s*;)?\s*$') + +# Prefixes that indicate externally-resolvable conditions (singleton services or globals). +# Conditions referencing local variables (root., parent., model, index, etc.) are skipped. +RESOLVABLE_PREFIXES = ( + "CompositorService.", + "Settings.data.", + "Quickshell.", + "IdleService.", + "SystemStatService.", + "SoundService.", + "BluetoothService.", + "LocationService.", + "false", +) def parse_component_declarations(content: str) -> dict[str, str]: @@ -234,9 +249,61 @@ def resolve_tab_info( return tab_index, tab_label, None, None -def extract_widget_blocks(content: str) -> list[tuple[str, str]]: +def is_resolvable_condition(cond: str) -> bool: + """Check if a visibility condition can be resolved at runtime by the shell.""" + # Strip negation for prefix checking + check = cond.lstrip("!").lstrip(" ").lstrip("(").lstrip(" ") + return any(check.startswith(p) for p in RESOLVABLE_PREFIXES) + + +def build_scope_visibility(content: str) -> dict[int, list[str]]: """ - Extract (widget_type, block_text) pairs from QML content. + For each line number, compute the list of inherited visibility conditions + from all enclosing QML scopes. + + Tracks brace-depth to maintain a scope stack. When a ``visible:`` property + is found, it is associated with the innermost scope. The conditions are + inherited by all lines within that scope. + + Returns: line_number -> list of condition strings + """ + lines = content.splitlines() + # Each entry: visibility condition string or None + scope_stack: list[str | None] = [] + result: dict[int, list[str]] = {} + + for line_num, raw_line in enumerate(lines): + stripped = raw_line.strip() + + # Record inherited conditions BEFORE processing this line's braces. + # This means a widget opening on this line inherits from parent scopes, + # not from its own scope (which hasn't been populated yet). + result[line_num] = [c for c in scope_stack if c is not None] + + # Process opening braces — push new scopes + n_opens = stripped.count("{") + for _ in range(n_opens): + scope_stack.append(None) + + # Check for visible: property — assign to the innermost scope + vis_match = RE_VISIBLE.match(stripped) + if vis_match and scope_stack: + cond = vis_match.group(1).strip() + if cond != "true": + scope_stack[-1] = cond + + # Process closing braces — pop scopes + n_closes = stripped.count("}") + for _ in range(n_closes): + if scope_stack: + scope_stack.pop() + + return result + + +def extract_widget_blocks(content: str) -> list[tuple[str, str, int]]: + """ + Extract (widget_type, block_text, start_line) tuples from QML content. Uses brace-depth tracking to capture the full widget block. """ @@ -250,6 +317,7 @@ def extract_widget_blocks(content: str) -> list[tuple[str, str]]: widget_type = m.group(1) depth = 0 block_lines = [] + start_line = i j = i while j < len(lines): @@ -261,7 +329,7 @@ def extract_widget_blocks(content: str) -> list[tuple[str, str]]: j += 1 block_text = "\n".join(block_lines) - results.append((widget_type, block_text)) + results.append((widget_type, block_text, start_line)) i = j + 1 else: i += 1 @@ -282,9 +350,10 @@ def extract_entries( return [] content = qml_file.read_text() + scope_vis = build_scope_visibility(content) entries = [] - for widget_type, block in extract_widget_blocks(content): + for widget_type, block, start_line in extract_widget_blocks(content): label_match = RE_LABEL.search(block) if not label_match: continue @@ -293,6 +362,19 @@ def extract_entries( desc_match = RE_DESCRIPTION.search(block) desc_key = desc_match.group(1) if desc_match else None + # Collect visibility conditions: ancestor scopes + widget's own visible: + conditions = list(scope_vis.get(start_line, [])) + widget_vis = re.search( + r'^\s*visible:\s*(.+?)(?:\s*;)?\s*$', block, re.MULTILINE + ) + if widget_vis: + cond = widget_vis.group(1).strip() + if cond != "true" and cond not in conditions: + conditions.append(cond) + + # Keep only externally-resolvable conditions + conditions = [c for c in conditions if is_resolvable_condition(c)] + entry = { "labelKey": label_key, "descriptionKey": desc_key, @@ -303,6 +385,8 @@ def extract_entries( } if sub_tab_label is not None: entry["subTabLabel"] = sub_tab_label + if conditions: + entry["visibleWhen"] = conditions entries.append(entry)