diff --git a/quickshell/Modals/DankLauncherV2/ActionPanel.qml b/quickshell/Modals/DankLauncherV2/ActionPanel.qml index 31fb40b1..2f861276 100644 --- a/quickshell/Modals/DankLauncherV2/ActionPanel.qml +++ b/quickshell/Modals/DankLauncherV2/ActionPanel.qml @@ -76,6 +76,13 @@ Rectangle { } } break; + case "clipboard": + if (selectedItem?.actions) { + for (var i = 0; i < selectedItem.actions.length; i++) { + result.push(selectedItem.actions[i]); + } + } + break; } return result; } diff --git a/quickshell/Modals/DankLauncherV2/ClipboardLauncherPreview.qml b/quickshell/Modals/DankLauncherV2/ClipboardLauncherPreview.qml new file mode 100644 index 00000000..fe892d74 --- /dev/null +++ b/quickshell/Modals/DankLauncherV2/ClipboardLauncherPreview.qml @@ -0,0 +1,67 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property var entry: null + property string cachedImageData: "" + property var _requestedEntryId: null + + readonly property bool canLoadImage: !!entry?.isImage && (entry?.mimeType ?? "").startsWith("image/") + readonly property string sourceUrl: cachedImageData.length > 0 ? "data:" + (entry?.mimeType ?? "image/png") + ";base64," + cachedImageData : "" + + radius: Math.max(6, Theme.cornerRadius - 2) + clip: true + color: Theme.surfaceContainerHigh + border.color: Theme.withAlpha(Theme.outline, 0.16) + border.width: 1 + + onEntryChanged: reloadPreview() + Component.onCompleted: reloadPreview() + + function reloadPreview() { + cachedImageData = ""; + if (!canLoadImage || !entry?.id) { + _requestedEntryId = null; + return; + } + + const entryId = entry.id; + _requestedEntryId = entryId; + DMSService.sendRequest("clipboard.getEntry", { + "id": entryId + }, function (response) { + if (_requestedEntryId !== entryId) + return; + if (response.error) + return; + const data = response.result?.data ?? ""; + if (data.length > 0) + cachedImageData = data; + }); + } + + Image { + id: previewImage + anchors.fill: parent + source: root.sourceUrl + asynchronous: true + cache: false + smooth: true + fillMode: Image.PreserveAspectCrop + visible: status === Image.Ready + } + + DankIcon { + anchors.centerIn: parent + name: "image" + size: Math.min(22, Math.max(16, root.height * 0.46)) + color: Theme.primary + visible: previewImage.status !== Image.Ready + } +} diff --git a/quickshell/Modals/DankLauncherV2/Controller.qml b/quickshell/Modals/DankLauncherV2/Controller.qml index cbf57a8b..97e208d5 100644 --- a/quickshell/Modals/DankLauncherV2/Controller.qml +++ b/quickshell/Modals/DankLauncherV2/Controller.qml @@ -44,7 +44,10 @@ Item { signal searchQueryRequested(string query) onActiveChanged: { - if (!active) { + if (active) { + if (clipboardSearchEnabledInAll()) + ClipboardService.ensureLauncherHistory(); + } else { SessionData.addLauncherHistory(searchQuery); sections = []; @@ -69,6 +72,28 @@ Item { AppSearchService.invalidateLauncherCache(); _clearModeCache(); } + function onLauncherPluginVisibilityChanged() { + AppSearchService.invalidateLauncherCache(); + _clearModeCache(); + if (active) + performSearch(); + } + function onBuiltInPluginSettingsChanged() { + AppSearchService.invalidateLauncherCache(); + _clearModeCache(); + if (active) + performSearch(); + } + } + + Connections { + target: ClipboardService + function onInternalEntriesChanged() { + if (!active || !clipboardSearchEnabledInAll()) + return; + if (searchMode === "all" && searchQuery.length >= 2) + performSearch(); + } } Connections { @@ -124,6 +149,18 @@ Item { function pasteSelected() { if (!selectedItem) return; + if (selectedItem.type === "clipboard") { + if (SettingsData.clipboardEnterToPaste) { + ClipboardService.copyEntry(selectedItem.data, function () { + root.itemExecuted(); + }); + } else { + ClipboardService.pasteEntry(selectedItem.data, function () { + root.itemExecuted(); + }); + } + return; + } if (!SessionService.wtypeAvailable) { ToastService.showError(I18n.tr("wtype not available - install wtype for paste support")); return; @@ -155,6 +192,20 @@ Item { priority: 2, defaultViewMode: "list" }, + { + id: "settings", + title: I18n.tr("Settings", "settings window title"), + icon: "settings", + priority: 2.35, + defaultViewMode: "list" + }, + { + id: "clipboard", + title: I18n.tr("Clipboard"), + icon: "content_paste", + priority: 2.45, + defaultViewMode: "list" + }, { id: "browse_plugins", title: I18n.tr("Browse"), @@ -352,6 +403,9 @@ Item { searchQuery = query; searchDebounce.restart(); + if (searchMode === "all" && clipboardSearchEnabledInAll() && query.length >= 2) + ClipboardService.ensureLauncherHistory(); + var filesInAll = searchMode === "all" && (SettingsData.dankLauncherV2IncludeFilesInAll || SettingsData.dankLauncherV2IncludeFoldersInAll); if (searchMode !== "plugins" && (searchMode === "files" || query.startsWith("/") || filesInAll) && query.length > 0) { fileSearchDebounce.restart(); @@ -370,6 +424,8 @@ Item { searchMode = mode; modeChanged(mode); performSearch(); + if (mode === "all" && clipboardSearchEnabledInAll() && searchQuery.length >= 2) + ClipboardService.ensureLauncherHistory(); var filesInAll = mode === "all" && (SettingsData.dankLauncherV2IncludeFilesInAll || SettingsData.dankLauncherV2IncludeFoldersInAll) && searchQuery.length > 0; if (mode === "files" || filesInAll) { fileSearchDebounce.restart(); @@ -612,7 +668,7 @@ Item { if (triggerMatch.isBuiltIn) { var builtInItems = AppSearchService.getBuiltInLauncherItems(triggerMatch.pluginId, triggerMatch.query); for (var j = 0; j < builtInItems.length; j++) { - allItems.push(transformBuiltInLauncherItem(builtInItems[j], triggerMatch.pluginId)); + allItems.push(transformBuiltInSearchItem(builtInItems[j], triggerMatch.pluginId)); } } @@ -748,7 +804,7 @@ Item { var builtInItems = AppSearchService.getBuiltInLauncherItems(pluginFilter, searchQuery); for (var j = 0; j < builtInItems.length; j++) { - allItems.push(transformBuiltInLauncherItem(builtInItems[j], pluginFilter)); + allItems.push(transformBuiltInSearchItem(builtInItems[j], pluginFilter)); } } else { var emptyTriggerPlugins = getEmptyTriggerPlugins(); @@ -764,7 +820,7 @@ Item { var pluginId = builtInLauncherPlugins[i]; var blItems = AppSearchService.getBuiltInLauncherItems(pluginId, searchQuery); for (var j = 0; j < blItems.length; j++) { - allItems.push(transformBuiltInLauncherItem(blItems[j], pluginId)); + allItems.push(transformBuiltInSearchItem(blItems[j], pluginId)); } } } @@ -799,6 +855,7 @@ Item { } if (searchMode === "all") { + appendSharedAllResults(allItems, searchQuery); if (searchQuery && searchQuery.length >= 2) { _pluginPhasePending = true; _phase1Items = allItems.slice(); @@ -814,7 +871,7 @@ Item { if (plugin.isBuiltIn) { var blItems = AppSearchService.getBuiltInLauncherItems(plugin.id, searchQuery); for (var j = 0; j < blItems.length; j++) - allItems.push(transformBuiltInLauncherItem(blItems[j], plugin.id)); + allItems.push(transformBuiltInSearchItem(blItems[j], plugin.id)); } else { var pItems = getPluginItems(plugin.id, searchQuery); for (var j = 0; j < pItems.length; j++) @@ -883,11 +940,13 @@ Item { if (currentVersion !== _searchVersion) return; var plugin = allPluginsOrdered[i]; + if (plugin.isBuiltIn && (plugin.id === "dms_settings_search" || plugin.id === "dms_clipboard_search")) + continue; if (plugin.isBuiltIn) { var blItems = AppSearchService.getBuiltInLauncherItems(plugin.id, searchQuery); var blLimit = Math.min(blItems.length, maxPerPlugin); for (var j = 0; j < blLimit; j++) { - var item = transformBuiltInLauncherItem(blItems[j], plugin.id); + var item = transformBuiltInSearchItem(blItems[j], plugin.id); item._preScored = 900 - j; allItems.push(item); } @@ -1110,10 +1169,56 @@ Item { return Transform.transformBuiltInLauncherItem(item, pluginId, I18n.tr("Open")); } + function transformBuiltInSearchItem(item, pluginId) { + if (pluginId === "dms_clipboard_search" || item.type === "clipboard") + return transformClipboardEntry(item.data || item); + return transformBuiltInLauncherItem(item, pluginId); + } + function transformFileResult(file) { return Transform.transformFileResult(file, I18n.tr("Open"), I18n.tr("Open folder"), I18n.tr("Copy path"), I18n.tr("Open in terminal")); } + function transformClipboardEntry(entry) { + var copyLabel = I18n.tr("Copy"); + var pasteLabel = I18n.tr("Paste"); + var primaryLabel = SettingsData.clipboardEnterToPaste ? pasteLabel : copyLabel; + var pasteHintLabel = SettingsData.clipboardEnterToPaste ? I18n.tr("Shift+Enter to copy") : I18n.tr("Shift+Enter to paste"); + return Transform.transformClipboardItem(entry, copyLabel, pasteLabel, primaryLabel, I18n.tr("Image"), I18n.tr("Text"), I18n.tr("Pinned"), pasteHintLabel, "", I18n.tr("Clipboard")); + } + + function builtInLauncherVisibleInAll(pluginId) { + return SettingsData.getBuiltInPluginSetting(pluginId, "enabled", true) && SettingsData.getPluginAllowWithoutTrigger(pluginId); + } + + function clipboardSearchEnabledInAll() { + return builtInLauncherVisibleInAll("dms_clipboard_search") && ClipboardService.clipboardAvailable; + } + + function appendSharedAllResults(allItems, query) { + if (!query || query.length < 2) + return; + + if (builtInLauncherVisibleInAll("dms_settings_search")) { + var settingsItems = AppSearchService.getBuiltInLauncherItems("dms_settings_search", query); + var settingsLimit = Math.min(settingsItems.length, 8); + for (var i = 0; i < settingsLimit; i++) { + settingsItems[i]._preScored = 890 - i; + allItems.push(transformBuiltInSearchItem(settingsItems[i], "dms_settings_search")); + } + } + + if (clipboardSearchEnabledInAll()) { + ClipboardService.ensureLauncherHistory(); + var clipboardItems = AppSearchService.getBuiltInLauncherItems("dms_clipboard_search", query); + var clipboardLimit = Math.min(clipboardItems.length, 8); + for (var j = 0; j < clipboardLimit; j++) { + clipboardItems[j]._preScored = 840 - j; + allItems.push(transformBuiltInSearchItem(clipboardItems[j], "dms_clipboard_search")); + } + } + } + function detectTrigger(query) { if (!query || query.length === 0) return { @@ -1308,7 +1413,9 @@ Item { } function buildDynamicSectionDefs(items) { - var baseDefs = sectionDefinitions.slice(); + var baseDefs = sectionDefinitions.map(function (def) { + return Object.assign({}, def); + }); var pluginSections = {}; var order = SettingsData.launcherPluginOrder || []; var orderMap = {}; @@ -1316,6 +1423,12 @@ Item { orderMap[order[k]] = k; var unorderedPriority = 2.6 + order.length * 0.01; + for (var d = 0; d < baseDefs.length; d++) { + var virtualId = baseDefs[d].id === "settings" ? "dms_settings_search" : baseDefs[d].id === "clipboard" ? "dms_clipboard_search" : ""; + if (virtualId && orderMap[virtualId] !== undefined) + baseDefs[d].priority = 2.6 + orderMap[virtualId] * 0.01; + } + for (var i = 0; i < items.length; i++) { var section = items[i].section; if (!section || !section.startsWith("plugin_")) @@ -1768,6 +1881,20 @@ Item { AppSearchService.executePluginItem(item.data, item.pluginId); } break; + case "setting": + AppSearchService.executeBuiltInLauncherItem(item.data); + break; + case "clipboard": + if (SettingsData.clipboardEnterToPaste) { + ClipboardService.pasteEntry(item.data, function () { + root.itemExecuted(); + }); + } else { + ClipboardService.copyEntry(item.data, function () { + root.itemExecuted(); + }); + } + return; case "file": openFile(item.data?.path); break; @@ -1803,6 +1930,16 @@ Item { case "execute": executeItem(item); break; + case "clipboard_copy": + ClipboardService.copyEntry(item.data, function () { + root.itemExecuted(); + }); + return; + case "clipboard_paste": + ClipboardService.pasteEntry(item.data, function () { + root.itemExecuted(); + }); + return; case "launch_dgpu": if (item.type === "app" && item.data) { launchAppWithNvidia(item.data); diff --git a/quickshell/Modals/DankLauncherV2/ItemTransformers.js b/quickshell/Modals/DankLauncherV2/ItemTransformers.js index d85dee98..f5a6eb24 100644 --- a/quickshell/Modals/DankLauncherV2/ItemTransformers.js +++ b/quickshell/Modals/DankLauncherV2/ItemTransformers.js @@ -94,17 +94,19 @@ function transformBuiltInLauncherItem(item, pluginId, openLabel) { return { id: item.action || "", - type: "plugin", + type: item.type || "plugin", name: item.name || "", subtitle: item.comment || "", icon: icon, iconType: iconType, - section: "plugin_" + pluginId, - data: item, + section: item.section || ("plugin_" + pluginId), + data: item.data || item, pluginId: pluginId, isBuiltInLauncher: true, keywords: item.keywords || [], actions: [], + source: item.source || "", + badgeLabel: item.badgeLabel || "", primaryAction: { name: openLabel, icon: "open_in_new", @@ -117,6 +119,58 @@ function transformBuiltInLauncherItem(item, pluginId, openLabel) { }; } +function transformClipboardItem(entry, copyLabel, pasteLabel, primaryLabel, imageLabel, textLabel, pinnedLabel, pasteHintLabel, copyHintLabel, clipboardLabel) { + var preview = (entry.preview || "").toString().replace(/\s+/g, " ").trim(); + var isImage = entry.isImage || false; + var title = preview.length > 0 ? preview : imageLabel; + if (title.length > 140) + title = title.substring(0, 137) + "..."; + + var typeLabel = isImage ? imageLabel : textLabel; + var subtitle = typeLabel; + if (entry.pinned) + subtitle = pinnedLabel + " - " + subtitle; + if (pasteHintLabel) + subtitle += " - " + pasteHintLabel; + if (copyHintLabel) + subtitle += " - " + copyHintLabel; + + return { + id: "clipboard_" + (entry.id || entry.hash || title), + type: "clipboard", + name: title, + subtitle: subtitle, + icon: isImage ? "image" : "content_paste", + iconType: "material", + section: "clipboard", + data: entry, + keywords: [preview, isImage ? "image" : "text", "clipboard"], + actions: [ + { + name: copyLabel, + icon: "content_copy", + action: "clipboard_copy" + }, + { + name: pasteLabel, + icon: "content_paste", + action: "clipboard_paste" + } + ], + source: clipboardLabel, + badgeLabel: clipboardLabel, + primaryAction: { + name: primaryLabel, + icon: primaryLabel === pasteLabel ? "content_paste" : "content_copy", + action: primaryLabel === pasteLabel ? "clipboard_paste" : "clipboard_copy" + }, + _hName: "", + _hSub: "", + _hRich: false, + _preScored: entry._preScored + }; +} + function transformFileResult(file, openLabel, openFolderLabel, copyPathLabel, openTerminalLabel) { var filename = file.path ? file.path.split("/").pop() : ""; var dirname = file.path ? file.path.substring(0, file.path.lastIndexOf("/")) : ""; diff --git a/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml b/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml index 79ec335e..7779c1e0 100644 --- a/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml +++ b/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml @@ -32,6 +32,8 @@ Popup { var actions = instance.getContextMenuActions(spotlightItem.data); return Array.isArray(actions) && actions.length > 0; } + if (spotlightItem.actions && spotlightItem.actions.length > 0) + return true; return false; } @@ -80,6 +82,13 @@ Popup { hide(); } + function executeLauncherAction(actionData) { + if (!controller || !item || !actionData) + return; + controller.executeAction(item, actionData); + hide(); + } + readonly property var menuItems: { var items = []; @@ -97,6 +106,19 @@ Popup { return items; } + if (item?.type !== "app" && item?.actions && item.actions.length > 0) { + for (var i = 0; i < item.actions.length; i++) { + var genericAct = item.actions[i]; + items.push({ + type: "item", + icon: genericAct.icon || "play_arrow", + text: genericAct.name || "", + launcherActionData: genericAct + }); + } + return items; + } + if (item?.type === "app") { items.push({ type: "item", @@ -293,6 +315,8 @@ Popup { menuItem.action(); else if (menuItem.pluginAction) executePluginAction(menuItem.pluginAction); + else if (menuItem.launcherActionData) + executeLauncherAction(menuItem.launcherActionData); else if (menuItem.actionData) executeDesktopAction(menuItem.actionData); return; @@ -500,6 +524,8 @@ Popup { menuItem.action(); else if (menuItem.pluginAction) root.executePluginAction(menuItem.pluginAction); + else if (menuItem.launcherActionData) + root.executeLauncherAction(menuItem.launcherActionData); else if (menuItem.actionData) root.executeDesktopAction(menuItem.actionData); } diff --git a/quickshell/Modals/DankLauncherV2/ResultItem.qml b/quickshell/Modals/DankLauncherV2/ResultItem.qml index 8f078842..7782f64c 100644 --- a/quickshell/Modals/DankLauncherV2/ResultItem.qml +++ b/quickshell/Modals/DankLauncherV2/ResultItem.qml @@ -33,6 +33,7 @@ Rectangle { return item.icon || ""; } } + readonly property bool hasClipboardPreview: item?.type === "clipboard" && item?.data?.isImage === true && (item?.data?.mimeType ?? "").startsWith("image/") width: parent?.width ?? 200 height: 52 @@ -154,6 +155,14 @@ Rectangle { anchors.verticalCenter: parent.verticalCenter spacing: Theme.spacingS + ClipboardLauncherPreview { + width: root.hasClipboardPreview ? 56 : 0 + height: 36 + visible: root.hasClipboardPreview + anchors.verticalCenter: parent.verticalCenter + entry: root.item?.data ?? null + } + Rectangle { id: allModeToggle visible: root.item?.type === "plugin_browse" @@ -208,9 +217,15 @@ Rectangle { text: { if (!root.item) return ""; + if ((root.item.badgeLabel ?? "").length > 0) + return root.item.badgeLabel; switch (root.item.type) { case "plugin": return I18n.tr("Plugin"); + case "setting": + return I18n.tr("Setting"); + case "clipboard": + return I18n.tr("Clipboard"); case "file": return root.item.data?.is_dir ? I18n.tr("Folder") : I18n.tr("File"); default: diff --git a/quickshell/Modals/DankLauncherV2/Scorer.js b/quickshell/Modals/DankLauncherV2/Scorer.js index 7d54df79..4d3a6707 100644 --- a/quickshell/Modals/DankLauncherV2/Scorer.js +++ b/quickshell/Modals/DankLauncherV2/Scorer.js @@ -10,6 +10,8 @@ const Weights = { typeBonus: { app: 1000, plugin: 900, + setting: 850, + clipboard: 825, file: 800, action: 600 } diff --git a/quickshell/Modals/DankLauncherV2/SpotlightResultRow.qml b/quickshell/Modals/DankLauncherV2/SpotlightResultRow.qml index e6335229..728490cf 100644 --- a/quickshell/Modals/DankLauncherV2/SpotlightResultRow.qml +++ b/quickshell/Modals/DankLauncherV2/SpotlightResultRow.qml @@ -48,17 +48,24 @@ Rectangle { return "file://" + raw; return raw; } - readonly property bool hasMediaPreview: previewSource.length > 0 + readonly property bool hasClipboardPreview: item?.type === "clipboard" && item?.data?.isImage === true && (item?.data?.mimeType ?? "").startsWith("image/") + readonly property bool hasMediaPreview: previewSource.length > 0 || hasClipboardPreview readonly property bool previewAnimated: previewSource.toLowerCase().indexOf(".gif") >= 0 readonly property string typeLabel: { if (!item) return ""; + if ((item.badgeLabel ?? "").length > 0) + return item.badgeLabel; switch (item.type) { case "plugin_browse": return I18n.tr("Browse"); case "plugin": return I18n.tr("Plugin"); + case "setting": + return I18n.tr("Setting"); + case "clipboard": + return I18n.tr("Clipboard"); case "file": return item.data?.is_dir ? I18n.tr("Folder") : I18n.tr("File"); default: @@ -278,7 +285,7 @@ Rectangle { source: root.previewSource asynchronous: true fillMode: Image.PreserveAspectCrop - visible: !root.previewAnimated + visible: !root.hasClipboardPreview && !root.previewAnimated } AnimatedImage { @@ -286,7 +293,13 @@ Rectangle { source: root.previewSource fillMode: Image.PreserveAspectCrop playing: visible - visible: root.previewAnimated + visible: !root.hasClipboardPreview && root.previewAnimated + } + + ClipboardLauncherPreview { + anchors.fill: parent + entry: root.item?.data ?? null + visible: root.hasClipboardPreview } } diff --git a/quickshell/Modules/Settings/LauncherTab.qml b/quickshell/Modules/Settings/LauncherTab.qml index af983b53..98790eab 100644 --- a/quickshell/Modules/Settings/LauncherTab.qml +++ b/quickshell/Modules/Settings/LauncherTab.qml @@ -565,7 +565,7 @@ Item { spacing: Theme.spacingS Repeater { - model: ["dms_settings", "dms_notepad", "dms_sysmon", "dms_settings_search"] + model: ["dms_settings", "dms_notepad", "dms_sysmon", "dms_settings_search", "dms_clipboard_search"] delegate: Rectangle { id: pluginDelegate diff --git a/quickshell/Services/AppSearchService.qml b/quickshell/Services/AppSearchService.qml index b6f840ff..5a22f6fd 100644 --- a/quickshell/Services/AppSearchService.qml +++ b/quickshell/Services/AppSearchService.qml @@ -211,11 +211,21 @@ Singleton { }, "dms_settings_search": { id: "dms_settings_search", - name: I18n.tr("Settings", "settings window title"), + name: I18n.tr("Settings Search"), cornerIcon: "search", - comment: "DMS", + comment: I18n.tr("DMS Settings"), defaultTrigger: "?", isLauncher: true + }, + "dms_clipboard_search": { + id: "dms_clipboard_search", + name: I18n.tr("Clipboard"), + cornerIcon: "content_paste", + comment: "DMS", + defaultTrigger: "cb", + isLauncher: true, + viewMode: "list", + viewModeEnforced: true } }) @@ -285,6 +295,16 @@ Singleton { } function getBuiltInLauncherItems(pluginId, query) { + if (pluginId === "dms_clipboard_search") { + ClipboardService.ensureLauncherHistory(); + const trimmed = (query || "").toString().trim(); + const entries = trimmed.length === 0 ? ClipboardService.getRecentLauncherEntries(20) : ClipboardService.getLauncherEntries(trimmed, 20, 1); + return entries.map(entry => ({ + type: "clipboard", + data: entry + })); + } + if (pluginId !== "dms_settings_search") return []; @@ -295,10 +315,15 @@ Singleton { const r = results[i]; items.push({ name: r.label, + type: "setting", + section: "settings", icon: "material:" + r.icon, - comment: r.category, + comment: r.description || r.category, action: "settings_nav:" + r.tabIndex + ":" + r.section, categories: ["Settings"], + keywords: r.keywords || [], + source: I18n.tr("Settings", "settings window title"), + badgeLabel: I18n.tr("Setting"), isCore: true, isBuiltInLauncher: true, builtInPluginId: pluginId diff --git a/quickshell/Services/ClipboardService.qml b/quickshell/Services/ClipboardService.qml index e49a44c6..46ddd2df 100644 --- a/quickshell/Services/ClipboardService.qml +++ b/quickshell/Services/ClipboardService.qml @@ -26,6 +26,7 @@ Singleton { property int selectedIndex: 0 property bool keyboardNavigationActive: false property int refCount: 0 + property real _launcherLastRefresh: 0 signal historyCopied signal historyCleared @@ -90,6 +91,68 @@ Singleton { }); } + function ensureLauncherHistory() { + if (!clipboardAvailable) { + return; + } + + const now = Date.now(); + if (internalEntries.length === 0 || now - _launcherLastRefresh > 5000) { + _launcherLastRefresh = now; + refresh(); + } + } + + function getLauncherEntries(query, limit, minLength) { + if (!clipboardAvailable) { + return []; + } + + const trimmed = (query || "").toString().trim(); + const requiredLength = minLength !== undefined ? minLength : 2; + if (trimmed.length < requiredLength) { + return []; + } + + const lowerQuery = trimmed.toLowerCase(); + const maxItems = limit > 0 ? limit : 8; + const matches = []; + + for (var i = 0; i < internalEntries.length; i++) { + const entry = internalEntries[i]; + const preview = getEntryPreview(entry).toString(); + const typeText = entry.isImage ? "image picture screenshot clipboard" : "text clipboard"; + const haystack = (preview + " " + typeText).toLowerCase(); + if (haystack.indexOf(lowerQuery) === -1) { + continue; + } + matches.push(entry); + } + + matches.sort((a, b) => { + if (a.pinned !== b.pinned) + return b.pinned ? 1 : -1; + return (b.id || 0) - (a.id || 0); + }); + + return matches.slice(0, maxItems); + } + + function getRecentLauncherEntries(limit) { + if (!clipboardAvailable) { + return []; + } + + const maxItems = limit > 0 ? limit : 20; + const entries = internalEntries.slice(); + entries.sort((a, b) => { + if (a.pinned !== b.pinned) + return b.pinned ? 1 : -1; + return (b.id || 0) - (a.id || 0); + }); + return entries.slice(0, maxItems); + } + function reset() { searchText = ""; selectedIndex = 0;