diff --git a/quickshell/Modals/DankLauncherV2/Controller.qml b/quickshell/Modals/DankLauncherV2/Controller.qml index b38a8d1e..a9f2a384 100644 --- a/quickshell/Modals/DankLauncherV2/Controller.qml +++ b/quickshell/Modals/DankLauncherV2/Controller.qml @@ -26,6 +26,8 @@ Item { property string activePluginId: "" property var collapsedSections: ({}) property bool keyboardNavigationActive: false + property var _modeSectionsCache: ({}) + property bool _queryDrivenSearch: false property var sectionViewModes: ({}) property var pluginViewPreferences: ({}) property int gridColumns: SettingsData.appLauncherGridColumns @@ -42,6 +44,14 @@ Item { target: SettingsData function onSortAppsAlphabeticallyChanged() { AppSearchService.invalidateLauncherCache(); + _clearModeCache(); + } + } + + Connections { + target: AppSearchService + function onCacheInvalidated() { + _clearModeCache(); } } @@ -266,6 +276,7 @@ Item { function setSearchQuery(query) { _searchVersion++; + _queryDrivenSearch = true; searchQuery = query; searchDebounce.restart(); @@ -324,6 +335,8 @@ Item { activePluginCategory = ""; pluginFilter = ""; collapsedSections = {}; + _clearModeCache(); + _queryDrivenSearch = false; } function loadPluginCategories(pluginId) { @@ -369,7 +382,11 @@ Item { return false; } - function preserveSelectionAfterUpdate() { + function preserveSelectionAfterUpdate(forceFirst) { + if (forceFirst) + return function () { + return getFirstItemIndex(); + }; var previousSelectedId = selectedItem?.id || ""; return function (newFlatModel) { if (!previousSelectedId) @@ -385,7 +402,9 @@ Item { function performSearch() { var currentVersion = _searchVersion; isSearching = true; - var restoreSelection = preserveSelectionAfterUpdate(); + var shouldResetSelection = _queryDrivenSearch; + _queryDrivenSearch = false; + var restoreSelection = preserveSelectionAfterUpdate(shouldResetSelection); var cachedSections = AppSearchService.getCachedDefaultSections(); if (cachedSections && !searchQuery && searchMode === "all" && !pluginFilter) { @@ -394,15 +413,22 @@ Item { activePluginCategories = []; activePluginCategory = ""; clearActivePluginViewPreference(); - sections = cachedSections.map(function (s) { - var copy = Object.assign({}, s, { - items: s.items ? s.items.slice() : [] + var modeCache = _getCachedModeData("all"); + if (modeCache) { + sections = modeCache.sections; + flatModel = modeCache.flatModel; + } else { + sections = cachedSections.map(function (s) { + var copy = Object.assign({}, s, { + items: s.items ? s.items.slice() : [] + }); + if (collapsedSections[s.id] !== undefined) + copy.collapsed = collapsedSections[s.id]; + return copy; }); - if (collapsedSections[s.id] !== undefined) - copy.collapsed = collapsedSections[s.id]; - return copy; - }); - flatModel = Scorer.flattenSections(sections); + flatModel = Scorer.flattenSections(sections); + _setCachedModeData("all", sections, flatModel); + } selectedFlatIndex = restoreSelection(flatModel); updateSelectedItem(); isSearching = false; @@ -475,18 +501,25 @@ Item { if (searchMode === "apps") { var cachedSections = AppSearchService.getCachedDefaultSections(); if (cachedSections && !searchQuery) { - var appSectionIds = ["favorites", "apps"]; - sections = cachedSections.filter(function (s) { - return appSectionIds.indexOf(s.id) !== -1; - }).map(function (s) { - var copy = Object.assign({}, s, { - items: s.items ? s.items.slice() : [] + var modeCache = _getCachedModeData("apps"); + if (modeCache) { + sections = modeCache.sections; + flatModel = modeCache.flatModel; + } else { + var appSectionIds = ["favorites", "apps"]; + sections = cachedSections.filter(function (s) { + return appSectionIds.indexOf(s.id) !== -1; + }).map(function (s) { + var copy = Object.assign({}, s, { + items: s.items ? s.items.slice() : [] + }); + if (collapsedSections[s.id] !== undefined) + copy.collapsed = collapsedSections[s.id]; + return copy; }); - if (collapsedSections[s.id] !== undefined) - copy.collapsed = collapsedSections[s.id]; - return copy; - }); - flatModel = Scorer.flattenSections(sections); + flatModel = Scorer.flattenSections(sections); + _setCachedModeData("apps", sections, flatModel); + } selectedFlatIndex = restoreSelection(flatModel); updateSelectedItem(); isSearching = false; @@ -1055,6 +1088,23 @@ Item { return Nav.getFirstItemIndex(flatModel); } + function _getCachedModeData(mode) { + return _modeSectionsCache[mode] || null; + } + + function _setCachedModeData(mode, sectionsData, flatModelData) { + var cache = Object.assign({}, _modeSectionsCache); + cache[mode] = { + sections: sectionsData, + flatModel: flatModelData + }; + _modeSectionsCache = cache; + } + + function _clearModeCache() { + _modeSectionsCache = {}; + } + function updateSelectedItem() { if (selectedFlatIndex >= 0 && selectedFlatIndex < flatModel.length) { var entry = flatModel[selectedFlatIndex]; @@ -1158,6 +1208,7 @@ Item { } function toggleSection(sectionId) { + _clearModeCache(); var newCollapsed = Object.assign({}, collapsedSections); var currentState = newCollapsed[sectionId]; diff --git a/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml b/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml index 02d4a1e9..c7570312 100644 --- a/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml +++ b/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml @@ -90,6 +90,8 @@ Item { spotlightContent.controller.activePluginName = ""; spotlightContent.controller.pluginFilter = ""; spotlightContent.controller.collapsedSections = {}; + spotlightContent.controller.selectedFlatIndex = 0; + spotlightContent.controller.selectedItem = null; if (query) { spotlightContent.controller.setSearchQuery(query); } else { diff --git a/quickshell/Modals/DankLauncherV2/GridItem.qml b/quickshell/Modals/DankLauncherV2/GridItem.qml index 1da51d72..63bb645b 100644 --- a/quickshell/Modals/DankLauncherV2/GridItem.qml +++ b/quickshell/Modals/DankLauncherV2/GridItem.qml @@ -35,6 +35,21 @@ Rectangle { readonly property int computedIconSize: Math.min(48, Math.max(32, width * 0.45)) + function highlightText(text, query, baseColor) { + if (!text || !query || query.length === 0) + return text; + var lowerText = text.toLowerCase(); + var lowerQuery = query.toLowerCase(); + var idx = lowerText.indexOf(lowerQuery); + if (idx === -1) + return text; + var before = text.substring(0, idx); + var match = text.substring(idx, idx + query.length); + var after = text.substring(idx + query.length); + var highlightColor = Theme.primary; + return '' + before + '' + '' + match + '' + '' + after + ''; + } + radius: Theme.cornerRadius color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryHoverLight : "transparent" @@ -55,11 +70,20 @@ Rectangle { materialIconSizeAdjustment: root.computedIconSize * 0.3 } - StyledText { + Text { width: parent.width - text: root.item?.name ?? "" + text: { + var query = root.controller?.searchQuery ?? ""; + var name = root.item?.name ?? ""; + var baseColor = root.isSelected ? Theme.primary : Theme.surfaceText; + if (!query) + return name; + return root.highlightText(name, query, baseColor); + } + textFormat: root.controller?.searchQuery ? Text.RichText : Text.PlainText font.pixelSize: Theme.fontSizeSmall font.weight: Font.Medium + font.family: Theme.fontFamily color: root.isSelected ? Theme.primary : Theme.surfaceText elide: Text.ElideRight horizontalAlignment: Text.AlignHCenter diff --git a/quickshell/Modals/DankLauncherV2/ResultItem.qml b/quickshell/Modals/DankLauncherV2/ResultItem.qml index c8ebdb7f..fab140cb 100644 --- a/quickshell/Modals/DankLauncherV2/ResultItem.qml +++ b/quickshell/Modals/DankLauncherV2/ResultItem.qml @@ -33,6 +33,21 @@ Rectangle { } } + function highlightText(text, query, baseColor) { + if (!text || !query || query.length === 0) + return text; + var lowerText = text.toLowerCase(); + var lowerQuery = query.toLowerCase(); + var idx = lowerText.indexOf(lowerQuery); + if (idx === -1) + return text; + var before = text.substring(0, idx); + var match = text.substring(idx, idx + query.length); + var after = text.substring(idx + query.length); + var highlightColor = Theme.primary; + return '' + before + '' + '' + match + '' + '' + after + ''; + } + width: parent?.width ?? 200 height: 52 color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryHoverLight : "transparent" @@ -82,23 +97,41 @@ Rectangle { width: parent.width - 36 - Theme.spacingM * 3 - rightContent.width spacing: 2 - StyledText { + Text { width: parent.width - text: root.item?.name ?? "" + text: { + var query = root.controller?.searchQuery ?? ""; + var name = root.item?.name ?? ""; + if (!query) + return name; + return root.highlightText(name, query, Theme.surfaceText); + } + textFormat: root.controller?.searchQuery ? Text.RichText : Text.PlainText font.pixelSize: Theme.fontSizeMedium font.weight: Font.Medium + font.family: Theme.fontFamily color: Theme.surfaceText elide: Text.ElideRight horizontalAlignment: Text.AlignLeft } - StyledText { + Text { width: parent.width - text: root.item?.subtitle ?? "" + text: { + var query = root.controller?.searchQuery ?? ""; + var subtitle = root.item?.subtitle ?? ""; + if (!subtitle) + return ""; + if (!query) + return subtitle; + return root.highlightText(subtitle, query, Theme.surfaceVariantText); + } + textFormat: root.controller?.searchQuery ? Text.RichText : Text.PlainText font.pixelSize: Theme.fontSizeSmall + font.family: Theme.fontFamily color: Theme.surfaceVariantText elide: Text.ElideRight - visible: text.length > 0 + visible: (root.item?.subtitle ?? "").length > 0 horizontalAlignment: Text.AlignLeft } } diff --git a/quickshell/Modals/DankLauncherV2/TileItem.qml b/quickshell/Modals/DankLauncherV2/TileItem.qml index 6f370cfa..e6e6af19 100644 --- a/quickshell/Modals/DankLauncherV2/TileItem.qml +++ b/quickshell/Modals/DankLauncherV2/TileItem.qml @@ -23,6 +23,21 @@ Rectangle { border.width: isSelected ? 2 : 0 border.color: Theme.primary + function highlightText(text, query, baseColor) { + if (!text || !query || query.length === 0) + return text; + var lowerText = text.toLowerCase(); + var lowerQuery = query.toLowerCase(); + var idx = lowerText.indexOf(lowerQuery); + if (idx === -1) + return text; + var before = text.substring(0, idx); + var match = text.substring(idx, idx + query.length); + var after = text.substring(idx + query.length); + var highlightColor = Theme.primary; + return '' + before + '' + '' + match + '' + '' + after + ''; + } + readonly property string toplevelId: item?.data?.toplevelId ?? "" readonly property var waylandToplevel: { if (!toplevelId || !item?.pluginId) @@ -108,12 +123,20 @@ Rectangle { color: Theme.withAlpha(Theme.surfaceContainer, 0.85) visible: root.item?.name?.length > 0 - StyledText { + Text { id: labelText anchors.fill: parent anchors.margins: Theme.spacingXS - text: root.item?.name ?? "" + text: { + var query = root.controller?.searchQuery ?? ""; + var name = root.item?.name ?? ""; + if (!query) + return name; + return root.highlightText(name, query, Theme.surfaceText); + } + textFormat: root.controller?.searchQuery ? Text.RichText : Text.PlainText font.pixelSize: Theme.fontSizeSmall + font.family: Theme.fontFamily color: Theme.surfaceText elide: Text.ElideRight horizontalAlignment: Text.AlignHCenter