diff --git a/quickshell/Common/CacheData.qml b/quickshell/Common/CacheData.qml index 4b8d3428..6de7a8bd 100644 --- a/quickshell/Common/CacheData.qml +++ b/quickshell/Common/CacheData.qml @@ -178,6 +178,33 @@ Singleton { } } + function loadLauncherCache() { + try { + var content = launcherCacheFile.text(); + if (content && content.trim()) + return JSON.parse(content); + } catch (e) { + console.warn("CacheData: Failed to parse launcher cache:", e.message); + } + return null; + } + + function saveLauncherCache(sections) { + if (_loading) + return; + launcherCacheFile.setText(JSON.stringify(sections)); + } + + FileView { + id: launcherCacheFile + + path: isGreeterMode ? "" : _stateDir + "/DankMaterialShell/launcher_cache.json" + blockLoading: true + blockWrites: true + atomicWrites: true + watchChanges: false + } + FileView { id: cacheFile diff --git a/quickshell/Modals/DankLauncherV2/Controller.qml b/quickshell/Modals/DankLauncherV2/Controller.qml index add80034..10de89ca 100644 --- a/quickshell/Modals/DankLauncherV2/Controller.qml +++ b/quickshell/Modals/DankLauncherV2/Controller.qml @@ -28,6 +28,7 @@ Item { property bool keyboardNavigationActive: false property var _modeSectionsCache: ({}) property bool _queryDrivenSearch: false + property bool _diskCacheConsumed: false property var sectionViewModes: ({}) property var pluginViewPreferences: ({}) property int gridColumns: SettingsData.appLauncherGridColumns @@ -52,6 +53,8 @@ Item { target: AppSearchService function onCacheVersionChanged() { _clearModeCache(); + if (!searchQuery && searchMode === "all") + performSearch(); } } @@ -423,6 +426,30 @@ Item { var restoreSelection = preserveSelectionAfterUpdate(shouldResetSelection); var cachedSections = AppSearchService.getCachedDefaultSections(); + if (!cachedSections && !_diskCacheConsumed && !searchQuery && searchMode === "all" && !pluginFilter) { + _diskCacheConsumed = true; + var diskSections = _loadDiskCache(); + if (diskSections) { + activePluginId = ""; + activePluginName = ""; + activePluginCategories = []; + activePluginCategory = ""; + clearActivePluginViewPreference(); + for (var i = 0; i < diskSections.length; i++) { + if (collapsedSections[diskSections[i].id] !== undefined) + diskSections[i].collapsed = collapsedSections[diskSections[i].id]; + } + _applyHighlights(diskSections, ""); + flatModel = Scorer.flattenSections(diskSections); + sections = diskSections; + selectedFlatIndex = restoreSelection(flatModel); + updateSelectedItem(); + isSearching = false; + searchCompleted(); + return; + } + } + if (cachedSections && !searchQuery && searchMode === "all" && !pluginFilter) { activePluginId = ""; activePluginName = ""; @@ -431,6 +458,7 @@ Item { clearActivePluginViewPreference(); var modeCache = _getCachedModeData("all"); if (modeCache) { + _applyHighlights(modeCache.sections, ""); sections = modeCache.sections; flatModel = modeCache.flatModel; } else { @@ -442,6 +470,7 @@ Item { copy.collapsed = collapsedSections[s.id]; return copy; }); + _applyHighlights(newSections, ""); flatModel = Scorer.flattenSections(newSections); sections = newSections; _setCachedModeData("all", sections, flatModel); @@ -489,6 +518,7 @@ Item { } } + _applyHighlights(newSections, triggerMatch.query); flatModel = Scorer.flattenSections(newSections); sections = newSections; selectedFlatIndex = restoreSelection(flatModel); @@ -522,6 +552,7 @@ Item { if (cachedSections && !searchQuery) { var modeCache = _getCachedModeData("apps"); if (modeCache) { + _applyHighlights(modeCache.sections, ""); sections = modeCache.sections; flatModel = modeCache.flatModel; } else { @@ -536,6 +567,7 @@ Item { copy.collapsed = collapsedSections[s.id]; return copy; }); + _applyHighlights(newSections, ""); flatModel = Scorer.flattenSections(newSections); sections = newSections; _setCachedModeData("apps", sections, flatModel); @@ -564,6 +596,7 @@ Item { } } + _applyHighlights(newSections, searchQuery); flatModel = Scorer.flattenSections(newSections); sections = newSections; selectedFlatIndex = restoreSelection(flatModel); @@ -623,6 +656,7 @@ Item { } } + _applyHighlights(newSections, searchQuery); flatModel = Scorer.flattenSections(newSections); sections = newSections; selectedFlatIndex = restoreSelection(flatModel); @@ -699,11 +733,13 @@ Item { } } + _applyHighlights(newSections, searchQuery); flatModel = Scorer.flattenSections(newSections); sections = newSections; if (!AppSearchService.isCacheValid() && !searchQuery && searchMode === "all" && !pluginFilter) { AppSearchService.setCachedDefaultSections(sections, flatModel); + _saveDiskCache(sections); } selectedFlatIndex = restoreSelection(flatModel); @@ -762,6 +798,7 @@ Item { newSections[i].collapsed = collapsedSections[sid]; } + _applyHighlights(newSections, searchQuery); flatModel = Scorer.flattenSections(newSections); sections = newSections; selectedFlatIndex = restoreSelection(flatModel); @@ -835,6 +872,7 @@ Item { newSections.sort(function (a, b) { return a.priority - b.priority; }); + _applyHighlights(newSections, searchQuery); flatModel = Scorer.flattenSections(newSections); sections = newSections; if (selectedFlatIndex >= flatModel.length) { @@ -1184,6 +1222,83 @@ Item { _modeSectionsCache = {}; } + function _saveDiskCache(sectionsData) { + var serializable = []; + for (var i = 0; i < sectionsData.length; i++) { + var s = sectionsData[i]; + var items = []; + var srcItems = s.items || []; + for (var j = 0; j < srcItems.length; j++) { + var it = srcItems[j]; + items.push({ + id: it.id, + type: it.type, + name: it.name || "", + subtitle: it.subtitle || "", + icon: it.icon || "", + iconType: it.iconType || "image", + iconFull: it.iconFull || "", + section: it.section || "", + isCore: it.isCore || false, + isBuiltInLauncher: it.isBuiltInLauncher || false, + pluginId: it.pluginId || "" + }); + } + serializable.push({ + id: s.id, + title: s.title || "", + icon: s.icon || "", + priority: s.priority || 0, + items: items + }); + } + CacheData.saveLauncherCache(serializable); + } + + function _loadDiskCache() { + var cached = CacheData.loadLauncherCache(); + if (!cached || !Array.isArray(cached) || cached.length === 0) + return null; + + var sectionsData = []; + for (var i = 0; i < cached.length; i++) { + var s = cached[i]; + var items = []; + var srcItems = s.items || []; + for (var j = 0; j < srcItems.length; j++) { + var it = srcItems[j]; + items.push({ + id: it.id || "", + type: it.type || "app", + name: it.name || "", + subtitle: it.subtitle || "", + icon: it.icon || "", + iconType: it.iconType || "image", + iconFull: it.iconFull || "", + section: it.section || "", + isCore: it.isCore || false, + isBuiltInLauncher: it.isBuiltInLauncher || false, + pluginId: it.pluginId || "", + data: { + id: it.id + }, + actions: [], + primaryAction: null, + _diskCached: true + }); + } + sectionsData.push({ + id: s.id || "", + title: s.title || "", + icon: s.icon || "", + priority: s.priority || 0, + items: items, + collapsed: false + }); + } + return sectionsData; + } + function updateSelectedItem() { if (selectedFlatIndex >= 0 && selectedFlatIndex < flatModel.length) { var entry = flatModel[selectedFlatIndex]; @@ -1193,6 +1308,48 @@ Item { } } + function _applyHighlights(sectionsData, query) { + if (!query || query.length === 0) { + for (var i = 0; i < sectionsData.length; i++) { + var items = sectionsData[i].items; + for (var j = 0; j < items.length; j++) { + var item = items[j]; + item._hName = item.name || ""; + item._hSub = item.subtitle || ""; + item._hRich = false; + } + } + return; + } + + var highlightColor = Theme.primary; + var nameColor = Theme.surfaceText; + var subColor = Theme.surfaceVariantText; + var lowerQuery = query.toLowerCase(); + + for (var i = 0; i < sectionsData.length; i++) { + var items = sectionsData[i].items; + for (var j = 0; j < items.length; j++) { + var item = items[j]; + item._hName = _highlightField(item.name || "", lowerQuery, query.length, nameColor, highlightColor); + item._hSub = _highlightField(item.subtitle || "", lowerQuery, query.length, subColor, highlightColor); + item._hRich = true; + } + } + } + + function _highlightField(text, lowerQuery, queryLen, baseColor, highlightColor) { + if (!text) + return ""; + var idx = text.toLowerCase().indexOf(lowerQuery); + if (idx === -1) + return text; + var before = text.substring(0, idx); + var match = text.substring(idx, idx + queryLen); + var after = text.substring(idx + queryLen); + return '' + before + '' + match + '' + after + ''; + } + function getCurrentSectionViewMode() { if (selectedFlatIndex < 0 || selectedFlatIndex >= flatModel.length) return "list"; @@ -1334,6 +1491,10 @@ Item { } function executeSelected() { + if (searchDebounce.running) { + searchDebounce.stop(); + performSearch(); + } if (!selectedItem) return; executeItem(selectedItem); diff --git a/quickshell/Modals/DankLauncherV2/GridItem.qml b/quickshell/Modals/DankLauncherV2/GridItem.qml index d6d2fdc0..a7f39c33 100644 --- a/quickshell/Modals/DankLauncherV2/GridItem.qml +++ b/quickshell/Modals/DankLauncherV2/GridItem.qml @@ -35,21 +35,6 @@ 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" @@ -78,15 +63,8 @@ Rectangle { Text { width: parent.width - 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 + text: root.item?._hName ?? root.item?.name ?? "" + textFormat: root.item?._hRich ? Text.RichText : Text.PlainText font.pixelSize: Theme.fontSizeSmall font.weight: Font.Medium font.family: Theme.fontFamily diff --git a/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml b/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml index e9502850..3c555904 100644 --- a/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml +++ b/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml @@ -34,7 +34,7 @@ Popup { return false; } - readonly property bool isCoreApp: item?.type === "app" && item?.isCore + readonly property bool isCoreApp: item?.type === "app" && !!item?.isCore readonly property var coreAppData: isCoreApp ? item?.data ?? null : null readonly property var desktopEntry: !isCoreApp ? (item?.data ?? null) : null readonly property string appId: { diff --git a/quickshell/Modals/DankLauncherV2/NavigationHelpers.js b/quickshell/Modals/DankLauncherV2/NavigationHelpers.js index f13e2bfd..d5300e10 100644 --- a/quickshell/Modals/DankLauncherV2/NavigationHelpers.js +++ b/quickshell/Modals/DankLauncherV2/NavigationHelpers.js @@ -25,6 +25,9 @@ function findPrevNonHeaderIndex(flatModel, startIndex) { } function getSectionBounds(flatModel, sectionId) { + if (flatModel._sectionBounds && flatModel._sectionBounds[sectionId]) + return flatModel._sectionBounds[sectionId]; + var start = -1, end = -1; for (var i = 0; i < flatModel.length; i++) { if (flatModel[i].isHeader && flatModel[i].section?.id === sectionId) { diff --git a/quickshell/Modals/DankLauncherV2/ResultItem.qml b/quickshell/Modals/DankLauncherV2/ResultItem.qml index 4ada194a..e4ad6753 100644 --- a/quickshell/Modals/DankLauncherV2/ResultItem.qml +++ b/quickshell/Modals/DankLauncherV2/ResultItem.qml @@ -33,21 +33,6 @@ 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" @@ -110,14 +95,8 @@ Rectangle { Text { width: parent.width - 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 + text: root.item?._hName ?? root.item?.name ?? "" + textFormat: root.item?._hRich ? Text.RichText : Text.PlainText font.pixelSize: Theme.fontSizeMedium font.weight: Font.Medium font.family: Theme.fontFamily @@ -128,16 +107,8 @@ Rectangle { Text { width: parent.width - 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 + text: root.item?._hSub ?? root.item?.subtitle ?? "" + textFormat: root.item?._hRich ? Text.RichText : Text.PlainText font.pixelSize: Theme.fontSizeSmall font.family: Theme.fontFamily color: Theme.surfaceVariantText @@ -193,7 +164,7 @@ Rectangle { } Rectangle { - visible: root.item?.type && root.item.type !== "app" && root.item.type !== "plugin_browse" + visible: !!root.item?.type && root.item.type !== "app" && root.item.type !== "plugin_browse" width: typeBadge.implicitWidth + Theme.spacingS * 2 height: 20 radius: 10 diff --git a/quickshell/Modals/DankLauncherV2/ResultsList.qml b/quickshell/Modals/DankLauncherV2/ResultsList.qml index afd4c51b..2cab9bf2 100644 --- a/quickshell/Modals/DankLauncherV2/ResultsList.qml +++ b/quickshell/Modals/DankLauncherV2/ResultsList.qml @@ -11,11 +11,111 @@ Item { property var controller: null property int gridColumns: controller?.gridColumns ?? 4 + property var _visualRows: [] + property var _flatIndexToRowMap: ({}) + property var _cumulativeHeights: [] signal itemRightClicked(int index, var item, real mouseX, real mouseY) + function _rebuildVisualModel() { + var sections = root.controller?.sections ?? []; + var rows = []; + var indexMap = {}; + var cumHeights = []; + var cumY = 0; + + for (var s = 0; s < sections.length; s++) { + var section = sections[s]; + var sectionId = section.id; + + cumHeights.push(cumY); + rows.push({ + _rowId: "h_" + sectionId, + type: "header", + section: section, + sectionId: sectionId, + height: 32 + }); + cumY += 32; + + if (section.collapsed) + continue; + + var versionTrigger = root.controller?.viewModeVersion ?? 0; + void (versionTrigger); + var mode = root.controller?.getSectionViewMode(sectionId) ?? "list"; + var items = section.items ?? []; + var flatStartIndex = section.flatStartIndex ?? 0; + + if (mode === "list") { + for (var i = 0; i < items.length; i++) { + var flatIdx = flatStartIndex + i; + indexMap[flatIdx] = rows.length; + cumHeights.push(cumY); + rows.push({ + _rowId: items[i].id, + type: "list_item", + item: items[i], + flatIndex: flatIdx, + sectionId: sectionId, + height: 52 + }); + cumY += 52; + } + } else { + var cols = root.controller?.getGridColumns(sectionId) ?? root.gridColumns; + var cellWidth = mode === "tile" ? Math.floor(root.width / 3) : Math.floor(root.width / root.gridColumns); + var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24; + var numRows = Math.ceil(items.length / cols); + + for (var r = 0; r < numRows; r++) { + var rowItems = []; + for (var c = 0; c < cols; c++) { + var idx = r * cols + c; + if (idx >= items.length) + break; + var fi = flatStartIndex + idx; + indexMap[fi] = rows.length; + rowItems.push({ + item: items[idx], + flatIndex: fi + }); + } + cumHeights.push(cumY); + rows.push({ + _rowId: "gr_" + sectionId + "_" + r, + type: "grid_row", + items: rowItems, + sectionId: sectionId, + viewMode: mode, + cols: cols, + height: cellHeight + }); + cumY += cellHeight; + } + } + } + + root._flatIndexToRowMap = indexMap; + root._cumulativeHeights = cumHeights; + root._visualRows = rows; + } + + onGridColumnsChanged: Qt.callLater(_rebuildVisualModel) + onWidthChanged: Qt.callLater(_rebuildVisualModel) + + Connections { + target: root.controller + function onSectionsChanged() { + Qt.callLater(root._rebuildVisualModel); + } + function onViewModeVersionChanged() { + Qt.callLater(root._rebuildVisualModel); + } + } + function resetScroll() { - mainFlickable.contentY = 0; + mainListView.contentY = mainListView.originY; } function ensureVisible(index) { @@ -24,75 +124,10 @@ Item { var entry = controller.flatModel[index]; if (!entry || entry.isHeader) return; - scrollItemIntoView(index, entry.sectionId); - } - - function scrollItemIntoView(flatIndex, sectionId) { - var sections = controller?.sections ?? []; - var sectionIndex = -1; - for (var i = 0; i < sections.length; i++) { - if (sections[i].id === sectionId) { - sectionIndex = i; - break; - } - } - if (sectionIndex < 0) + var rowIndex = _flatIndexToRowMap[index]; + if (rowIndex === undefined) return; - var itemInSection = 0; - var foundSection = false; - for (var i = 0; i < controller.flatModel.length && i < flatIndex; i++) { - var e = controller.flatModel[i]; - if (e.isHeader && e.section?.id === sectionId) - foundSection = true; - else if (foundSection && !e.isHeader && e.sectionId === sectionId) - itemInSection++; - } - - var mode = controller.getSectionViewMode(sectionId); - var sectionY = 0; - for (var i = 0; i < sectionIndex; i++) { - sectionY += getSectionHeight(sections[i]); - } - - var itemY, itemHeight; - if (mode === "list") { - itemY = itemInSection * 52; - itemHeight = 52; - } else { - var cols = controller.getGridColumns(sectionId); - var cellWidth = mode === "tile" ? Math.floor(mainFlickable.width / 3) : Math.floor(mainFlickable.width / root.gridColumns); - var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24; - var row = Math.floor(itemInSection / cols); - itemY = row * cellHeight; - itemHeight = cellHeight; - } - - var targetY = sectionY + 32 + itemY; - var targetBottom = targetY + itemHeight; - var stickyHeight = mainFlickable.contentY > 0 ? 32 : 0; - - var shadowPadding = 24; - if (targetY < mainFlickable.contentY + stickyHeight) { - mainFlickable.contentY = Math.max(0, targetY - 32); - } else if (targetBottom > mainFlickable.contentY + mainFlickable.height - shadowPadding) { - mainFlickable.contentY = Math.min(mainFlickable.contentHeight - mainFlickable.height, targetBottom - mainFlickable.height + shadowPadding); - } - } - - function getSectionHeight(section) { - var mode = controller?.getSectionViewMode(section.id) ?? "list"; - if (section.collapsed) - return 32; - - if (mode === "list") { - return 32 + (section.items?.length ?? 0) * 52; - } else { - var cols = controller?.getGridColumns(section.id) ?? root.gridColumns; - var rows = Math.ceil((section.items?.length ?? 0) / cols); - var cellWidth = mode === "tile" ? Math.floor(root.width / 3) : Math.floor(root.width / cols); - var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24; - return 32 + rows * cellHeight; - } + mainListView.positionViewAtIndex(rowIndex, ListView.Contain); } function getSelectedItemPosition() { @@ -104,42 +139,30 @@ Item { if (!entry || entry.isHeader) return fallback; - var sections = controller.sections; - var sectionIndex = -1; - for (var i = 0; i < sections.length; i++) { - if (sections[i].id === entry.sectionId) { - sectionIndex = i; - break; - } - } - if (sectionIndex < 0) + var rowIndex = _flatIndexToRowMap[controller.selectedFlatIndex]; + if (rowIndex === undefined) return fallback; - var sectionY = 0; - for (var i = 0; i < sectionIndex; i++) { - sectionY += getSectionHeight(sections[i]); + var rowY = (rowIndex < _cumulativeHeights.length) ? _cumulativeHeights[rowIndex] : 0; + var row = _visualRows[rowIndex]; + if (!row) + return fallback; + + var itemX = width / 2; + var itemH = row.height; + + if (row.type === "grid_row") { + var rowItems = row.items; + for (var i = 0; i < rowItems.length; i++) { + if (rowItems[i].flatIndex === controller.selectedFlatIndex) { + var cellWidth = row.viewMode === "tile" ? Math.floor(width / 3) : Math.floor(width / row.cols); + itemX = i * cellWidth + cellWidth / 2; + break; + } + } } - var mode = controller.getSectionViewMode(entry.sectionId); - var itemInSection = entry.indexInSection || 0; - - var itemY, itemX, itemH; - if (mode === "list") { - itemY = sectionY + 32 + itemInSection * 52; - itemX = width / 2; - itemH = 52; - } else { - var cols = controller.getGridColumns(entry.sectionId); - var cellWidth = mode === "tile" ? Math.floor(width / 3) : Math.floor(width / cols); - var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24; - var row = Math.floor(itemInSection / cols); - var col = itemInSection % cols; - itemY = sectionY + 32 + row * cellHeight; - itemX = col * cellWidth + cellWidth / 2; - itemH = cellHeight; - } - - var visualY = itemY - mainFlickable.contentY + itemH / 2; + var visualY = rowY - mainListView.contentY + mainListView.originY + itemH / 2; var clampedY = Math.max(40, Math.min(height - 40, visualY)); return mapToItem(null, itemX, clampedY); } @@ -153,161 +176,124 @@ Item { } } - DankFlickable { - id: mainFlickable + DankListView { + id: mainListView anchors.fill: parent - contentWidth: width - contentHeight: sectionsColumn.height clip: true + scrollBarTopMargin: (root.controller?.sections?.length > 0) ? 32 : 0 - Component.onCompleted: { - verticalScrollBar.targetFlickable = mainFlickable; - verticalScrollBar.parent = root; - verticalScrollBar.z = 102; - verticalScrollBar.anchors.right = root.right; - verticalScrollBar.anchors.top = root.top; - verticalScrollBar.anchors.bottom = root.bottom; + model: ScriptModel { + values: root._visualRows + objectProp: "_rowId" } - Column { - id: sectionsColumn - width: parent.width + add: null + remove: null + displaced: null + move: null - Repeater { - model: ScriptModel { - values: root.controller?.sections ?? [] - objectProp: "id" + delegate: Item { + id: delegateRoot + required property var modelData + required property int index + + width: mainListView.width + height: modelData?.height ?? 52 + + SectionHeader { + anchors.fill: parent + visible: delegateRoot.modelData?.type === "header" + section: delegateRoot.modelData?.section ?? null + controller: root.controller + viewMode: { + var vt = root.controller?.viewModeVersion ?? 0; + void (vt); + return root.controller?.getSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? "list"; + } + canChangeViewMode: { + var vt = root.controller?.viewModeVersion ?? 0; + void (vt); + return root.controller?.canChangeSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? false; + } + canCollapse: root.controller?.canCollapseSection(delegateRoot.modelData?.sectionId ?? "") ?? false + } + + ResultItem { + anchors.fill: parent + visible: delegateRoot.modelData?.type === "list_item" + item: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.item ?? null) : null + isSelected: delegateRoot.modelData?.type === "list_item" && (delegateRoot.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex + controller: root.controller + flatIndex: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.flatIndex ?? -1) : -1 + + onClicked: { + if (root.controller && delegateRoot.modelData?.item) { + root.controller.executeItem(delegateRoot.modelData.item); + } } - Column { - id: sectionDelegate - required property var modelData - required property int index + onRightClicked: (mouseX, mouseY) => { + root.itemRightClicked(delegateRoot.modelData?.flatIndex ?? -1, delegateRoot.modelData?.item ?? null, mouseX, mouseY); + } + } - readonly property int versionTrigger: root.controller?.viewModeVersion ?? 0 - readonly property string sectionId: modelData?.id ?? "" - readonly property string currentViewMode: { - void (versionTrigger); - return root.controller?.getSectionViewMode(sectionId) ?? "list"; - } - readonly property bool isGridMode: currentViewMode === "grid" || currentViewMode === "tile" - readonly property bool isCollapsed: modelData?.collapsed ?? false + Row { + id: gridRowContent + anchors.fill: parent + visible: delegateRoot.modelData?.type === "grid_row" - width: sectionsColumn.width + Repeater { + model: delegateRoot.modelData?.type === "grid_row" ? (delegateRoot.modelData?.items ?? []) : [] - SectionHeader { - width: parent.width - height: 32 - section: sectionDelegate.modelData - controller: root.controller - viewMode: sectionDelegate.currentViewMode - canChangeViewMode: root.controller?.canChangeSectionViewMode(sectionDelegate.sectionId) ?? false - canCollapse: root.controller?.canCollapseSection(sectionDelegate.sectionId) ?? false - } + Item { + id: gridCellDelegate + required property var modelData + required property int index - Column { - id: listContent - width: parent.width - visible: !sectionDelegate.isGridMode && !sectionDelegate.isCollapsed + readonly property real cellWidth: delegateRoot.modelData?.viewMode === "tile" ? Math.floor(delegateRoot.width / 3) : Math.floor(delegateRoot.width / (delegateRoot.modelData?.cols ?? root.gridColumns)) - Repeater { - model: ScriptModel { - values: sectionDelegate.isGridMode || sectionDelegate.isCollapsed ? [] : (sectionDelegate.modelData?.items ?? []) - objectProp: "id" + width: cellWidth + height: delegateRoot.height + + GridItem { + width: parent.width - 4 + height: parent.height - 4 + anchors.centerIn: parent + visible: delegateRoot.modelData?.viewMode === "grid" + item: gridCellDelegate.modelData?.item ?? null + isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex + controller: root.controller + flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1 + + onClicked: { + if (root.controller && gridCellDelegate.modelData?.item) { + root.controller.executeItem(gridCellDelegate.modelData.item); + } } - ResultItem { - required property var modelData - required property int index - - readonly property int computedFlatIndex: (sectionDelegate.modelData?.flatStartIndex ?? 0) + index - - width: listContent.width - height: 52 - item: modelData - isSelected: computedFlatIndex === root.controller?.selectedFlatIndex - controller: root.controller - flatIndex: computedFlatIndex - - onClicked: { - if (root.controller) { - root.controller.executeItem(modelData); - } - } - - onRightClicked: (mouseX, mouseY) => { - root.itemRightClicked(computedFlatIndex, modelData, mouseX, mouseY); - } + onRightClicked: (mouseX, mouseY) => { + root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY); } } - } - Grid { - id: gridContent - width: parent.width - visible: sectionDelegate.isGridMode && !sectionDelegate.isCollapsed - columns: sectionDelegate.currentViewMode === "tile" ? 3 : root.gridColumns + TileItem { + width: parent.width - 4 + height: parent.height - 4 + anchors.centerIn: parent + visible: delegateRoot.modelData?.viewMode === "tile" + item: gridCellDelegate.modelData?.item ?? null + isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex + controller: root.controller + flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1 - readonly property real cellWidth: sectionDelegate.currentViewMode === "tile" ? Math.floor(width / 3) : Math.floor(width / root.gridColumns) - readonly property real cellHeight: sectionDelegate.currentViewMode === "tile" ? cellWidth * 0.75 : cellWidth + 24 - - Repeater { - model: ScriptModel { - values: sectionDelegate.isGridMode && !sectionDelegate.isCollapsed ? (sectionDelegate.modelData?.items ?? []) : [] - objectProp: "id" + onClicked: { + if (root.controller && gridCellDelegate.modelData?.item) { + root.controller.executeItem(gridCellDelegate.modelData.item); + } } - Item { - id: gridDelegateItem - required property var modelData - required property int index - - width: gridContent.cellWidth - height: gridContent.cellHeight - - readonly property int cachedFlatIndex: (sectionDelegate.modelData?.flatStartIndex ?? 0) + index - - GridItem { - width: parent.width - 4 - height: parent.height - 4 - anchors.centerIn: parent - visible: sectionDelegate.currentViewMode === "grid" - item: gridDelegateItem.modelData - isSelected: gridDelegateItem.cachedFlatIndex === root.controller?.selectedFlatIndex - controller: root.controller - flatIndex: gridDelegateItem.cachedFlatIndex - - onClicked: { - if (root.controller) { - root.controller.executeItem(gridDelegateItem.modelData); - } - } - - onRightClicked: (mouseX, mouseY) => { - root.itemRightClicked(gridDelegateItem.cachedFlatIndex, gridDelegateItem.modelData, mouseX, mouseY); - } - } - - TileItem { - width: parent.width - 4 - height: parent.height - 4 - anchors.centerIn: parent - visible: sectionDelegate.currentViewMode === "tile" - item: gridDelegateItem.modelData - isSelected: gridDelegateItem.cachedFlatIndex === root.controller?.selectedFlatIndex - controller: root.controller - flatIndex: gridDelegateItem.cachedFlatIndex - - onClicked: { - if (root.controller) { - root.controller.executeItem(gridDelegateItem.modelData); - } - } - - onRightClicked: (mouseX, mouseY) => { - root.itemRightClicked(gridDelegateItem.cachedFlatIndex, gridDelegateItem.modelData, mouseX, mouseY); - } - } + onRightClicked: (mouseX, mouseY) => { + root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY); } } } @@ -324,9 +310,9 @@ Item { height: 24 z: 100 visible: { - if (mainFlickable.contentHeight <= mainFlickable.height) + if (mainListView.contentHeight <= mainListView.height) return false; - var atBottom = mainFlickable.contentY >= mainFlickable.contentHeight - mainFlickable.height - 5; + var atBottom = mainListView.contentY >= mainListView.contentHeight - mainListView.height + mainListView.originY - 5; if (atBottom) return false; @@ -371,23 +357,31 @@ Item { readonly property var stickyHeaderSection: { if (!root.controller?.sections || root.controller.sections.length === 0) return null; - var sections = root.controller.sections; - if (sections.length === 0) - return null; - var scrollY = mainFlickable.contentY; + var scrollY = mainListView.contentY - mainListView.originY; if (scrollY <= 0) return null; - var y = 0; - for (var i = 0; i < sections.length; i++) { - var section = sections[i]; - var sectionHeight = root.getSectionHeight(section); - if (scrollY < y + sectionHeight) - return section; - y += sectionHeight; + var rows = root._visualRows; + var heights = root._cumulativeHeights; + if (rows.length === 0 || heights.length === 0) + return null; + + var lo = 0; + var hi = rows.length - 1; + while (lo < hi) { + var mid = (lo + hi + 1) >> 1; + if (mid < heights.length && heights[mid] <= scrollY) + lo = mid; + else + hi = mid - 1; } - return sections[sections.length - 1]; + + for (var i = lo; i >= 0; i--) { + if (rows[i].type === "header") + return rows[i].section; + } + return null; } SectionHeader { diff --git a/quickshell/Modals/DankLauncherV2/Scorer.js b/quickshell/Modals/DankLauncherV2/Scorer.js index bde47736..0e917e28 100644 --- a/quickshell/Modals/DankLauncherV2/Scorer.js +++ b/quickshell/Modals/DankLauncherV2/Scorer.js @@ -220,6 +220,7 @@ function groupBySection(scoredItems, sectionOrder, sortAlphabetically, maxPerSec function flattenSections(sections) { var flat = [] + var bounds = {} for (var i = 0; i < sections.length; i++) { var section = sections[i] @@ -231,7 +232,8 @@ function flattenSections(sections) { sectionIndex: i }) - section.flatStartIndex = flat.length + var itemStart = flat.length + section.flatStartIndex = itemStart if (!section.collapsed) { for (var j = 0; j < section.items.length; j++) { @@ -244,7 +246,18 @@ function flattenSections(sections) { }) } } + + var itemEnd = flat.length - 1 + var itemCount = flat.length - itemStart + if (itemCount > 0) { + bounds[section.id] = { + start: itemStart, + end: itemEnd, + count: itemCount + } + } } + flat._sectionBounds = bounds return flat } diff --git a/quickshell/Modals/DankLauncherV2/TileItem.qml b/quickshell/Modals/DankLauncherV2/TileItem.qml index 1869fe62..07c7bee1 100644 --- a/quickshell/Modals/DankLauncherV2/TileItem.qml +++ b/quickshell/Modals/DankLauncherV2/TileItem.qml @@ -23,21 +23,6 @@ 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) @@ -133,14 +118,8 @@ Rectangle { id: labelText anchors.fill: parent anchors.margins: Theme.spacingXS - 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 + text: root.item?._hName ?? root.item?.name ?? "" + textFormat: root.item?._hRich ? Text.RichText : Text.PlainText font.pixelSize: Theme.fontSizeSmall font.family: Theme.fontFamily color: Theme.surfaceText diff --git a/quickshell/Widgets/DankListView.qml b/quickshell/Widgets/DankListView.qml index 7b490717..888de7c7 100644 --- a/quickshell/Widgets/DankListView.qml +++ b/quickshell/Widgets/DankListView.qml @@ -6,6 +6,7 @@ import qs.Widgets ListView { id: listView + property real scrollBarTopMargin: 0 property real mouseWheelSpeed: 60 property real savedY: 0 property bool justChanged: false @@ -208,5 +209,6 @@ ListView { ScrollBar.vertical: DankScrollbar { id: vbar + topPadding: listView.scrollBarTopMargin } }