From a844e578a990c181b7f51797d86d4f0584294132 Mon Sep 17 00:00:00 2001 From: "Braian A. Diez" Date: Sat, 28 Feb 2026 12:12:39 -0300 Subject: [PATCH 1/5] clipboard: add filters by type & date Signed-off-by: Braian A. Diez --- Assets/Translations/en.json | 6 + Assets/settings-default.json | 3 + Commons/Settings.qml | 3 + Modules/Panels/Launcher/Launcher.qml | 3 +- Modules/Panels/Launcher/LauncherCore.qml | 2858 +++++++++-------- .../Panels/Launcher/LauncherOverlayWindow.qml | 737 ++--- .../Launcher/Providers/ClipboardProvider.qml | 824 +++-- .../Tabs/Launcher/ClipboardSubTab.qml | 147 +- Services/Keyboard/ClipboardService.qml | 940 +++--- 9 files changed, 2896 insertions(+), 2625 deletions(-) diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 5e438f085..e5748c4f8 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -1234,8 +1234,14 @@ "settings-annotation-tool-placeholder": "e.g. 'gradia', 'satty -f -'", "settings-auto-paste-description": "Automatically paste the selected clipboard item. Requires wtype.", "settings-auto-paste-label": "Auto paste", + "settings-clip-chips-description": "Show a tab bar to filter clipboard history by type (Images, Links, Files, Code, etc).", + "settings-clip-chips-label": "Enable Category Chips", + "settings-clip-date-headers-description": "Group clipboard history chronologically with visual headers.", + "settings-clip-date-headers-label": "Enable Date grouping", "settings-clip-preview-description": "Show a preview of the clipboard content when using the >clip command.", "settings-clip-preview-label": "Enable clip preview", + "settings-clip-smart-icons-description": "Show specific icons for links, files, colors, and code instead of a generic clipboard icon.", + "settings-clip-smart-icons-label": "Enable Smart Icons", "settings-clip-wrap-text-description": "Wrap text in the clipboard list instead of truncating it.", "settings-clip-wrap-text-label": "Wrap clipboard text", "settings-clipboard-history-description": "Access previously copied items from the launcher.", diff --git a/Assets/settings-default.json b/Assets/settings-default.json index c06699dac..42bd1b144 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -216,6 +216,9 @@ "autoPasteClipboard": false, "enableClipPreview": true, "clipboardWrapText": true, + "enableClipboardSmartIcons": true, + "enableClipboardChips": true, + "enableClipboardDateHeaders": true, "clipboardWatchTextCommand": "wl-paste --type text --watch cliphist store", "clipboardWatchImageCommand": "wl-paste --type image --watch cliphist store", "position": "center", diff --git a/Commons/Settings.qml b/Commons/Settings.qml index a45e786e6..69eacc6de 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -407,6 +407,9 @@ Singleton { property bool autoPasteClipboard: false property bool enableClipPreview: true property bool clipboardWrapText: true + property bool enableClipboardSmartIcons: true + property bool enableClipboardChips: true + property bool enableClipboardDateHeaders: true property string clipboardWatchTextCommand: "wl-paste --type text --watch cliphist store" property string clipboardWatchImageCommand: "wl-paste --type image --watch cliphist store" property string position: "center" // Position: center, top_left, top_right, bottom_left, bottom_right, bottom_center, top_center diff --git a/Modules/Panels/Launcher/Launcher.qml b/Modules/Panels/Launcher/Launcher.qml index b055fdf1b..2e593deea 100644 --- a/Modules/Panels/Launcher/Launcher.qml +++ b/Modules/Panels/Launcher/Launcher.qml @@ -40,7 +40,8 @@ SmartPanel { return false; if (!Settings.data.appLauncher.enableClipPreview) return false; - return selectedIndex >= 0 && results && !!results[selectedIndex]; + var item = results[selectedIndex]; + return selectedIndex >= 0 && results && !!item && !item.isHeader; } readonly property int previewPanelWidth: Math.round(400 * Style.uiScaleRatio) diff --git a/Modules/Panels/Launcher/LauncherCore.qml b/Modules/Panels/Launcher/LauncherCore.qml index 6c92f4907..7146b3b76 100644 --- a/Modules/Panels/Launcher/LauncherCore.qml +++ b/Modules/Panels/Launcher/LauncherCore.qml @@ -13,1469 +13,1541 @@ import qs.Widgets // Core launcher logic and UI - shared between SmartPanel (Launcher.qml) and overlay (LauncherOverlayWindow.qml) Rectangle { - id: root - color: "transparent" + id: root + color: "transparent" - // External interface - set by parent - property var screen: null - property bool isOpen: false - signal requestClose - signal requestCloseImmediately + // External interface - set by parent + property var screen: null + property bool isOpen: false + signal requestClose + signal requestCloseImmediately - function closeImmediately() { - requestCloseImmediately(); - } - - // Expose for preview panel positioning - readonly property var resultsView: resultsViewLoader.item - - // State - property string searchText: "" - property int selectedIndex: 0 - property var results: [] - property var providers: [] - property var activeProvider: null - property bool resultsReady: false - property var pluginProviderInstances: ({}) - property bool ignoreMouseHover: true // Transient flag, should always be true on init - - // Global mouse tracking for movement detection across delegates - property real globalLastMouseX: 0 - property real globalLastMouseY: 0 - property bool globalMouseInitialized: false - property bool mouseTrackingReady: false // Delay tracking until panel is settled - - Timer { - id: mouseTrackingDelayTimer - interval: Style.animationNormal + 50 // Wait for panel animation to complete + safety margin - repeat: false - onTriggered: { - root.mouseTrackingReady = true; - root.globalMouseInitialized = false; // Reset so we get fresh initial position - } - } - - readonly property var defaultProvider: appsProvider - readonly property var currentProvider: activeProvider || defaultProvider - - readonly property string launcherDensity: (currentProvider && currentProvider.ignoreDensity === false) ? (Settings.data.appLauncher.density || "default") : "comfortable" - readonly property int effectiveIconSize: launcherDensity === "comfortable" ? 48 : (launcherDensity === "default" ? 36 : 24) - readonly property int badgeSize: Math.round(effectiveIconSize * Style.uiScaleRatio) - readonly property int entryHeight: Math.round(badgeSize + (launcherDensity === "compact" ? (Style.marginL + Style.marginXXS) : (Style.marginXL + Style.marginS))) - - readonly property bool providerShowsCategories: currentProvider.showsCategories === true - - readonly property var providerCategories: { - if (currentProvider.availableCategories && currentProvider.availableCategories.length > 0) { - return currentProvider.availableCategories; - } - return currentProvider.categories || []; - } - - readonly property bool showProviderCategories: { - if (!providerShowsCategories || providerCategories.length === 0) - return false; - if (currentProvider === defaultProvider) - return Settings.data.appLauncher.showCategories; - return true; - } - - readonly property bool providerHasDisplayString: results.length > 0 && !!results[0].displayString - - readonly property string providerSupportedLayouts: { - if (activeProvider && activeProvider.supportedLayouts) - return activeProvider.supportedLayouts; - if (results.length > 0 && results[0].provider && results[0].provider.supportedLayouts) - return results[0].provider.supportedLayouts; - if (defaultProvider && defaultProvider.supportedLayouts) - return defaultProvider.supportedLayouts; - return "both"; - } - - readonly property bool showLayoutToggle: !providerHasDisplayString && providerSupportedLayouts === "both" - - readonly property string layoutMode: { - if (searchText === ">") - return "list"; - if (providerSupportedLayouts === "grid") - return "grid"; - if (providerSupportedLayouts === "list") - return "list"; - if (providerSupportedLayouts === "single") - return "single"; - if (providerHasDisplayString) - return "grid"; - return Settings.data.appLauncher.viewMode; - } - - readonly property bool isGridView: layoutMode === "grid" - readonly property bool isSingleView: layoutMode === "single" - readonly property bool isCompactDensity: launcherDensity === "compact" - - readonly property int targetGridColumns: { - let base = 5; - if (launcherDensity === "comfortable") - base = 4; - else if (launcherDensity === "compact") - base = 6; - - if (!activeProvider || activeProvider === defaultProvider) - return base; - - if (activeProvider.preferredGridColumns) { - let multiplier = base / 5.0; - return Math.max(1, Math.round(activeProvider.preferredGridColumns * multiplier)); + function closeImmediately() { + requestCloseImmediately(); } - return base; - } - readonly property int listPanelWidth: Math.round(500 * Style.uiScaleRatio) - readonly property int gridContentWidth: listPanelWidth - (2 * Style.marginXS) - readonly property int gridCellSize: Math.floor((gridContentWidth - ((targetGridColumns - 1) * Style.marginS)) / targetGridColumns) + // Expose for preview panel positioning + readonly property var resultsView: resultsViewLoader.item - property int gridColumns: 5 + // State + property string searchText: "" + property int selectedIndex: 0 + property var results: [] + property var providers: [] + property var activeProvider: null + property bool resultsReady: false + property var pluginProviderInstances: ({}) + property bool ignoreMouseHover: true // Transient flag, should always be true on init - // Check if current provider allows wrap navigation (default true) - readonly property bool allowWrapNavigation: { - var provider = activeProvider || currentProvider; - return provider && provider.wrapNavigation !== undefined ? provider.wrapNavigation : true; - } + // Global mouse tracking for movement detection across delegates + property real globalLastMouseX: 0 + property real globalLastMouseY: 0 + property bool globalMouseInitialized: false + property bool mouseTrackingReady: false // Delay tracking until panel is settled - // Listen for plugin provider registry changes - Connections { - target: LauncherProviderRegistry - function onPluginProviderRegistryUpdated() { - root.syncPluginProviders(); - } - } - - // Lifecycle - onIsOpenChanged: { - if (isOpen) { - onOpened(); - } else { - onClosed(); - } - } - - function onOpened() { - resultsReady = false; - ignoreMouseHover = true; - globalMouseInitialized = false; - mouseTrackingReady = false; - mouseTrackingDelayTimer.restart(); - syncPluginProviders(); - Qt.callLater(() => { - for (let provider of providers) { - if (provider.onOpened) - provider.onOpened(); - } - updateResults(); - resultsReady = true; - focusSearchInput(); - }); - } - - function onClosed() { - searchText = ""; - ignoreMouseHover = true; - for (let provider of providers) { - if (provider.onClosed) - provider.onClosed(); - } - } - - onSearchTextChanged: { - if (isOpen) - updateResults(); - } - - function close() { - requestClose(); - } - - // Public API - function setSearchText(text) { - searchText = text; - } - - function focusSearchInput() { - if (searchInput.inputItem) { - searchInput.inputItem.forceActiveFocus(); - } - } - - // Provider registration - function registerProvider(provider) { - providers.push(provider); - provider.launcher = root; - if (provider.init) - provider.init(); - } - - function syncPluginProviders() { - var registeredIds = LauncherProviderRegistry.getPluginProviders(); - - // Remove providers that are no longer registered - for (var existingId in pluginProviderInstances) { - if (registeredIds.indexOf(existingId) === -1) { - var idx = providers.indexOf(pluginProviderInstances[existingId]); - if (idx >= 0) - providers.splice(idx, 1); - pluginProviderInstances[existingId].destroy(); - delete pluginProviderInstances[existingId]; - Logger.d("Launcher", "Removed plugin provider:", existingId); - } - } - - // Add new providers - for (var i = 0; i < registeredIds.length; i++) { - var providerId = registeredIds[i]; - if (!pluginProviderInstances[providerId]) { - var component = LauncherProviderRegistry.getProviderComponent(providerId); - var pluginId = providerId.substring(7); // Remove "plugin:" prefix - var pluginApi = PluginService.getPluginAPI(pluginId); - if (component && pluginApi) { - var instance = component.createObject(root, { - pluginApi: pluginApi - }); - if (instance) { - pluginProviderInstances[providerId] = instance; - registerProvider(instance); - Logger.d("Launcher", "Registered plugin provider:", providerId); - } + Timer { + id: mouseTrackingDelayTimer + interval: Style.animationNormal + 50 // Wait for panel animation to complete + safety margin + repeat: false + onTriggered: { + root.mouseTrackingReady = true; + root.globalMouseInitialized = false; // Reset so we get fresh initial position } - } } - // Update results if launcher is open - if (root.isOpen) { - updateResults(); - } - } + readonly property var defaultProvider: appsProvider + readonly property var currentProvider: activeProvider || defaultProvider - // Search handling - function updateResults() { - results = []; - var newActiveProvider = null; + readonly property string launcherDensity: (currentProvider && currentProvider.ignoreDensity === false) ? (Settings.data.appLauncher.density || "default") : "comfortable" + readonly property int effectiveIconSize: launcherDensity === "comfortable" ? 48 : (launcherDensity === "default" ? 36 : 24) + readonly property int badgeSize: Math.round(effectiveIconSize * Style.uiScaleRatio) + readonly property int entryHeight: Math.round(badgeSize + (launcherDensity === "compact" ? (Style.marginL + Style.marginXXS) : (Style.marginXL + Style.marginS))) - // Check for command mode - if (searchText.startsWith(">")) { - for (let provider of providers) { - if (provider.handleCommand && provider.handleCommand(searchText)) { - newActiveProvider = provider; - results = provider.getResults(searchText); - break; + readonly property bool providerShowsCategories: currentProvider.showsCategories === true + + readonly property var providerCategories: { + if (currentProvider.availableCategories && currentProvider.availableCategories.length > 0) { + return currentProvider.availableCategories; } - } + return currentProvider.categories || []; + } - // Show available commands if just ">" or filter commands if partial match - if (!newActiveProvider) { - let allCommands = []; - for (let provider of providers) { - if (provider.commands) - allCommands = allCommands.concat(provider.commands()); + readonly property bool showProviderCategories: { + if (!providerShowsCategories || providerCategories.length === 0) + return false; + if (currentProvider === defaultProvider) + return Settings.data.appLauncher.showCategories; + return true; + } + + readonly property bool providerHasDisplayString: results.length > 0 && !!results[0].displayString + + readonly property string providerSupportedLayouts: { + if (activeProvider && activeProvider.supportedLayouts) + return activeProvider.supportedLayouts; + if (results.length > 0 && results[0].provider && results[0].provider.supportedLayouts) + return results[0].provider.supportedLayouts; + if (defaultProvider && defaultProvider.supportedLayouts) + return defaultProvider.supportedLayouts; + return "both"; + } + + readonly property bool showLayoutToggle: !providerHasDisplayString && providerSupportedLayouts === "both" + + readonly property string layoutMode: { + if (searchText === ">") + return "list"; + if (providerSupportedLayouts === "grid") + return "grid"; + if (providerSupportedLayouts === "list") + return "list"; + if (providerSupportedLayouts === "single") + return "single"; + if (providerHasDisplayString) + return "grid"; + return Settings.data.appLauncher.viewMode; + } + + readonly property bool isGridView: layoutMode === "grid" + readonly property bool isSingleView: layoutMode === "single" + readonly property bool isCompactDensity: launcherDensity === "compact" + + readonly property int targetGridColumns: { + let base = 5; + if (launcherDensity === "comfortable") + base = 4; + else if (launcherDensity === "compact") + base = 6; + + if (!activeProvider || activeProvider === defaultProvider) + return base; + + if (activeProvider.preferredGridColumns) { + let multiplier = base / 5.0; + return Math.max(1, Math.round(activeProvider.preferredGridColumns * multiplier)); } - if (searchText === ">") { - results = allCommands; - } else if (searchText.length > 1) { - const query = searchText.substring(1); - if (typeof FuzzySort !== 'undefined') { - const fuzzyResults = FuzzySort.go(query, allCommands, { - "keys": ["name"], - "limit": 50 - }); - results = fuzzyResults.map(result => result.obj); - } else { - const queryLower = query.toLowerCase(); - results = allCommands.filter(cmd => (cmd.name || "").toLowerCase().includes(queryLower)); - } + + return base; + } + readonly property int listPanelWidth: Math.round(500 * Style.uiScaleRatio) + readonly property int gridContentWidth: listPanelWidth - (2 * Style.marginXS) + readonly property int gridCellSize: Math.floor((gridContentWidth - ((targetGridColumns - 1) * Style.marginS)) / targetGridColumns) + + property int gridColumns: 5 + + // Check if current provider allows wrap navigation (default true) + readonly property bool allowWrapNavigation: { + var provider = activeProvider || currentProvider; + return provider && provider.wrapNavigation !== undefined ? provider.wrapNavigation : true; + } + + // Listen for plugin provider registry changes + Connections { + target: LauncherProviderRegistry + function onPluginProviderRegistryUpdated() { + root.syncPluginProviders(); } - } - } else { - // Regular search - let providers contribute results - let allResults = []; - for (let provider of providers) { - if (provider.handleSearch) { - const providerResults = provider.getResults(searchText); - allResults = allResults.concat(providerResults); - } - } - - // Sort by _score (higher = better match), items without _score go first - if (searchText.trim() !== "") { - allResults.sort((a, b) => { - const sa = a._score !== undefined ? a._score : 0; - const sb = b._score !== undefined ? b._score : 0; - return sb - sa; - }); - } - results = allResults; } - // Update activeProvider only after computing new state to avoid UI flicker - activeProvider = newActiveProvider; - selectedIndex = 0; - } - - // Navigation functions - function selectNext() { - if (results.length > 0 && selectedIndex < results.length - 1) { - selectedIndex++; - } - } - - function selectPrevious() { - if (results.length > 0 && selectedIndex > 0) { - selectedIndex--; - } - } - - function selectNextWrapped() { - if (results.length > 0) { - if (allowWrapNavigation) { - selectedIndex = (selectedIndex + 1) % results.length; - } else { - selectNext(); - } - } - } - - function selectPreviousWrapped() { - if (results.length > 0) { - if (allowWrapNavigation) { - selectedIndex = (((selectedIndex - 1) % results.length) + results.length) % results.length; - } else { - selectPrevious(); - } - } - } - - function selectFirst() { - selectedIndex = 0; - } - - function selectLast() { - selectedIndex = results.length > 0 ? results.length - 1 : 0; - } - - function selectNextPage() { - if (results.length > 0) { - const page = Math.max(1, Math.floor(600 / entryHeight)); - selectedIndex = Math.min(selectedIndex + page, results.length - 1); - } - } - - function selectPreviousPage() { - if (results.length > 0) { - const page = Math.max(1, Math.floor(600 / entryHeight)); - selectedIndex = Math.max(selectedIndex - page, 0); - } - } - - // Grid view navigation functions - function selectPreviousRow() { - if (results.length > 0 && isGridView && gridColumns > 0) { - const currentRow = Math.floor(selectedIndex / gridColumns); - const currentCol = selectedIndex % gridColumns; - - if (currentRow > 0) { - const targetRow = currentRow - 1; - const targetIndex = targetRow * gridColumns + currentCol; - const itemsInTargetRow = Math.min(gridColumns, results.length - targetRow * gridColumns); - if (currentCol < itemsInTargetRow) { - selectedIndex = targetIndex; + // Lifecycle + onIsOpenChanged: { + if (isOpen) { + onOpened(); } else { - selectedIndex = targetRow * gridColumns + itemsInTargetRow - 1; + onClosed(); } - } else { - // Wrap to last row, same column - const totalRows = Math.ceil(results.length / gridColumns); - const lastRow = totalRows - 1; - const itemsInLastRow = Math.min(gridColumns, results.length - lastRow * gridColumns); - if (currentCol < itemsInLastRow) { - selectedIndex = lastRow * gridColumns + currentCol; - } else { - selectedIndex = results.length - 1; - } - } } - } - function selectNextRow() { - if (results.length > 0 && isGridView && gridColumns > 0) { - const currentRow = Math.floor(selectedIndex / gridColumns); - const currentCol = selectedIndex % gridColumns; - const totalRows = Math.ceil(results.length / gridColumns); - - if (currentRow < totalRows - 1) { - const targetRow = currentRow + 1; - const targetIndex = targetRow * gridColumns + currentCol; - if (targetIndex < results.length) { - selectedIndex = targetIndex; - } else { - const itemsInTargetRow = results.length - targetRow * gridColumns; - if (itemsInTargetRow > 0) { - selectedIndex = targetRow * gridColumns + itemsInTargetRow - 1; - } else { - selectedIndex = Math.min(currentCol, results.length - 1); - } - } - } else { - // Wrap to first row, same column - selectedIndex = Math.min(currentCol, results.length - 1); - } - } - } - - function selectPreviousColumn() { - if (results.length > 0 && isGridView) { - const currentRow = Math.floor(selectedIndex / gridColumns); - const currentCol = selectedIndex % gridColumns; - if (currentCol > 0) { - selectedIndex = currentRow * gridColumns + (currentCol - 1); - } else if (currentRow > 0) { - selectedIndex = (currentRow - 1) * gridColumns + (gridColumns - 1); - } else { - const totalRows = Math.ceil(results.length / gridColumns); - const lastRowIndex = (totalRows - 1) * gridColumns + (gridColumns - 1); - selectedIndex = Math.min(lastRowIndex, results.length - 1); - } - } - } - - function selectNextColumn() { - if (results.length > 0 && isGridView) { - const currentRow = Math.floor(selectedIndex / gridColumns); - const currentCol = selectedIndex % gridColumns; - const itemsInCurrentRow = Math.min(gridColumns, results.length - currentRow * gridColumns); - - if (currentCol < itemsInCurrentRow - 1) { - selectedIndex = currentRow * gridColumns + (currentCol + 1); - } else { - const totalRows = Math.ceil(results.length / gridColumns); - if (currentRow < totalRows - 1) { - selectedIndex = (currentRow + 1) * gridColumns; - } else { - selectedIndex = 0; - } - } - } - } - - function activate() { - if (results.length > 0 && results[selectedIndex]) { - const item = results[selectedIndex]; - const provider = item.provider || currentProvider; - - // Check if auto-paste is enabled and provider/item supports it - if (Settings.data.appLauncher.autoPasteClipboard && provider && provider.supportsAutoPaste && item.autoPasteText) { - if (item.onAutoPaste) - item.onAutoPaste(); - closeImmediately(); + function onOpened() { + resultsReady = false; + ignoreMouseHover = true; + globalMouseInitialized = false; + mouseTrackingReady = false; + mouseTrackingDelayTimer.restart(); + syncPluginProviders(); Qt.callLater(() => { - ClipboardService.pasteText(item.autoPasteText); - }); - return; - } - - if (item.onActivate) - item.onActivate(); - } - } - - function checkKey(event, settingName) { - return Keybinds.checkKey(event, settingName, Settings); - } - - // Keyboard handler - function handleKeyPress(event) { - if (checkKey(event, 'escape')) { - close(); - event.accepted = true; - return; + for (let provider of providers) { + if (provider.onOpened) + provider.onOpened(); + } + updateResults(); + resultsReady = true; + focusSearchInput(); + }); } - if (checkKey(event, 'enter')) { - activate(); - event.accepted = true; - return; + function onClosed() { + searchText = ""; + ignoreMouseHover = true; + for (let provider of providers) { + if (provider.onClosed) + provider.onClosed(); + } } - if (checkKey(event, 'up')) { - if (!isSingleView) { - isGridView ? selectPreviousRow() : selectPreviousWrapped(); - } - event.accepted = true; - return; + onSearchTextChanged: { + if (isOpen) + updateResults(); } - if (checkKey(event, 'down')) { - if (!isSingleView) { - isGridView ? selectNextRow() : selectNextWrapped(); - } - event.accepted = true; - return; + function close() { + requestClose(); } - if (checkKey(event, 'left')) { - if (isGridView) { - selectPreviousColumn(); - event.accepted = true; - return; - } + // Public API + function setSearchText(text) { + searchText = text; } - if (checkKey(event, 'right')) { - if (isGridView) { - selectNextColumn(); - event.accepted = true; - return; - } - } - - // Static bindings - switch (event.key) { - case Qt.Key_Tab: - if (showProviderCategories) { - var cats = providerCategories; - var idx = cats.indexOf(currentProvider.selectedCategory); - currentProvider.selectCategory(cats[(idx + 1) % cats.length]); - } else { - selectNextWrapped(); - } - event.accepted = true; - break; - case Qt.Key_Backtab: - if (showProviderCategories) { - var cats2 = providerCategories; - var idx2 = cats2.indexOf(currentProvider.selectedCategory); - currentProvider.selectCategory(cats2[((idx2 - 1) % cats2.length + cats2.length) % cats2.length]); - } else { - selectPreviousWrapped(); - } - event.accepted = true; - break; - case Qt.Key_Home: - selectFirst(); - event.accepted = true; - break; - case Qt.Key_End: - selectLast(); - event.accepted = true; - break; - case Qt.Key_PageUp: - selectPreviousPage(); - event.accepted = true; - break; - case Qt.Key_PageDown: - selectNextPage(); - event.accepted = true; - break; - case Qt.Key_Delete: - if (selectedIndex >= 0 && results && results[selectedIndex]) { - var item = results[selectedIndex]; - var provider = item.provider || currentProvider; - if (provider && provider.canDeleteItem && provider.canDeleteItem(item)) - provider.deleteItem(item); - } - event.accepted = true; - break; - } - } - - // ----------------------- - // Provider components - // ----------------------- - ApplicationsProvider { - id: appsProvider - Component.onCompleted: { - registerProvider(this); - Logger.d("Launcher", "Registered: ApplicationsProvider"); - } - } - - ClipboardProvider { - id: clipProvider - Component.onCompleted: { - if (Settings.data.appLauncher.enableClipboardHistory) { - registerProvider(this); - Logger.d("Launcher", "Registered: ClipboardProvider"); - } - } - } - - CommandProvider { - id: cmdProvider - Component.onCompleted: { - registerProvider(this); - Logger.d("Launcher", "Registered: CommandProvider"); - } - } - - EmojiProvider { - id: emojiProvider - Component.onCompleted: { - registerProvider(this); - Logger.d("Launcher", "Registered: EmojiProvider"); - } - } - - CalculatorProvider { - id: calcProvider - Component.onCompleted: { - registerProvider(this); - Logger.d("Launcher", "Registered: CalculatorProvider"); - } - } - - SettingsProvider { - id: settingsProvider - Component.onCompleted: { - registerProvider(this); - Logger.d("Launcher", "Registered: SettingsProvider"); - } - } - - SessionProvider { - id: sessionProvider - Component.onCompleted: { - registerProvider(this); - Logger.d("Launcher", "Registered: SessionProvider"); - } - } - - WindowsProvider { - id: windowsProvider - Component.onCompleted: { - registerProvider(this); - Logger.d("Launcher", "Registered: WindowsProvider"); - } - } - - // ==================== UI Content ==================== - - opacity: resultsReady ? 1.0 : 0.0 - - Behavior on opacity { - NumberAnimation { - duration: Style.animationFast - easing.type: Easing.OutCirc - } - } - - HoverHandler { - id: globalHoverHandler - enabled: !Settings.data.appLauncher.ignoreMouseInput - - onPointChanged: { - if (!root.mouseTrackingReady) { - return; - } - - if (!root.globalMouseInitialized) { - root.globalLastMouseX = point.position.x; - root.globalLastMouseY = point.position.y; - root.globalMouseInitialized = true; - return; - } - - const deltaX = Math.abs(point.position.x - root.globalLastMouseX); - const deltaY = Math.abs(point.position.y - root.globalLastMouseY); - if (deltaX + deltaY >= 5) { - root.ignoreMouseHover = false; - root.globalLastMouseX = point.position.x; - root.globalLastMouseY = point.position.y; - } - } - } - - ColumnLayout { - anchors.fill: parent - anchors.topMargin: Style.marginL - anchors.bottomMargin: Style.marginL - spacing: Style.marginL - - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: Style.marginL - Layout.rightMargin: Style.marginL - spacing: Style.marginS - - NTextInput { - id: searchInput - Layout.fillWidth: true - radius: Style.iRadiusM - text: root.searchText - placeholderText: I18n.tr("placeholders.search-launcher") - fontSize: Style.fontSizeM - onTextChanged: root.searchText = text - - Component.onCompleted: { - if (searchInput.inputItem) { + function focusSearchInput() { + if (searchInput.inputItem) { searchInput.inputItem.forceActiveFocus(); - searchInput.inputItem.Keys.onPressed.connect(function (event) { - root.handleKeyPress(event); - }); - } } - } - - NIconButton { - visible: root.showLayoutToggle - icon: Settings.data.appLauncher.viewMode === "grid" ? "layout-list" : "layout-grid" - tooltipText: Settings.data.appLauncher.viewMode === "grid" ? I18n.tr("tooltips.list-view") : I18n.tr("tooltips.grid-view") - customRadius: Style.iRadiusM - Layout.preferredWidth: searchInput.height - Layout.preferredHeight: searchInput.height - onClicked: Settings.data.appLauncher.viewMode = Settings.data.appLauncher.viewMode === "grid" ? "list" : "grid" - } } - // Unified category tabs (works with any provider that has categories) - NTabBar { - id: categoryTabs - visible: root.showProviderCategories - Layout.fillWidth: true - Layout.leftMargin: Style.marginL - Layout.rightMargin: Style.marginL - margins: 0 - border.color: Style.boxBorderColor - border.width: Style.borderS - - property int computedCurrentIndex: visible && root.providerCategories.length > 0 ? root.providerCategories.indexOf(root.currentProvider.selectedCategory) : 0 - currentIndex: computedCurrentIndex - - Repeater { - model: root.providerCategories - NTabButton { - required property string modelData - required property int index - icon: root.currentProvider.categoryIcons ? (root.currentProvider.categoryIcons[modelData] || "star") : "star" - tooltipText: root.currentProvider.getCategoryName ? root.currentProvider.getCategoryName(modelData) : modelData - tabIndex: index - checked: categoryTabs.currentIndex === index - onClicked: root.currentProvider.selectCategory(modelData) - } - } + // Provider registration + function registerProvider(provider) { + providers.push(provider); + provider.launcher = root; + if (provider.init) + provider.init(); } - // Results view - Loader { - id: resultsViewLoader - Layout.fillWidth: true - Layout.leftMargin: Style.marginL - Layout.rightMargin: Style.marginL - Layout.fillHeight: true - sourceComponent: root.isSingleView ? singleViewComponent : (root.isGridView ? gridViewComponent : listViewComponent) + function syncPluginProviders() { + var registeredIds = LauncherProviderRegistry.getPluginProviders(); + + // Remove providers that are no longer registered + for (var existingId in pluginProviderInstances) { + if (registeredIds.indexOf(existingId) === -1) { + var idx = providers.indexOf(pluginProviderInstances[existingId]); + if (idx >= 0) + providers.splice(idx, 1); + pluginProviderInstances[existingId].destroy(); + delete pluginProviderInstances[existingId]; + Logger.d("Launcher", "Removed plugin provider:", existingId); + } + } + + // Add new providers + for (var i = 0; i < registeredIds.length; i++) { + var providerId = registeredIds[i]; + if (!pluginProviderInstances[providerId]) { + var component = LauncherProviderRegistry.getProviderComponent(providerId); + var pluginId = providerId.substring(7); // Remove "plugin:" prefix + var pluginApi = PluginService.getPluginAPI(pluginId); + if (component && pluginApi) { + var instance = component.createObject(root, { + pluginApi: pluginApi + }); + if (instance) { + pluginProviderInstances[providerId] = instance; + registerProvider(instance); + Logger.d("Launcher", "Registered plugin provider:", providerId); + } + } + } + } + + // Update results if launcher is open + if (root.isOpen) { + updateResults(); + } } - // -------------------------- - // LIST VIEW - Component { - id: listViewComponent - NListView { - id: resultsList + // Search handling + function updateResults() { + results = []; + var newActiveProvider = null; - horizontalPolicy: ScrollBar.AlwaysOff - verticalPolicy: ScrollBar.AlwaysOff - reserveScrollbarSpace: false - gradientColor: Color.mSurfaceVariant - wheelScrollMultiplier: 4.0 + // Check for command mode + if (searchText.startsWith(">")) { + for (let provider of providers) { + if (provider.handleCommand && provider.handleCommand(searchText)) { + newActiveProvider = provider; + results = provider.getResults(searchText); + break; + } + } - width: parent.width - height: parent.height - spacing: Style.marginS - model: root.results - currentIndex: root.selectedIndex - cacheBuffer: resultsList.height * 2 - interactive: !Settings.data.appLauncher.ignoreMouseInput - onCurrentIndexChanged: { - cancelFlick(); - if (currentIndex >= 0) { - positionViewAtIndex(currentIndex, ListView.Contain); - } + // Show available commands if just ">" or filter commands if partial match + if (!newActiveProvider) { + let allCommands = []; + for (let provider of providers) { + if (provider.commands) + allCommands = allCommands.concat(provider.commands()); + } + if (searchText === ">") { + results = allCommands; + } else if (searchText.length > 1) { + const query = searchText.substring(1); + if (typeof FuzzySort !== 'undefined') { + const fuzzyResults = FuzzySort.go(query, allCommands, { + "keys": ["name"], + "limit": 50 + }); + results = fuzzyResults.map(result => result.obj); + } else { + const queryLower = query.toLowerCase(); + results = allCommands.filter(cmd => (cmd.name || "").toLowerCase().includes(queryLower)); + } + } + } + } else { + // Regular search - let providers contribute results + let allResults = []; + for (let provider of providers) { + if (provider.handleSearch) { + const providerResults = provider.getResults(searchText); + allResults = allResults.concat(providerResults); + } + } + + // Sort by _score (higher = better match), items without _score go first + if (searchText.trim() !== "") { + allResults.sort((a, b) => { + const sa = a._score !== undefined ? a._score : 0; + const sb = b._score !== undefined ? b._score : 0; + return sb - sa; + }); + } + results = allResults; } - onModelChanged: {} - delegate: NBox { - id: entry + // Update activeProvider only after computing new state to avoid UI flicker + activeProvider = newActiveProvider; - property bool isSelected: (!root.ignoreMouseHover && mouseArea.containsMouse) || (index === root.selectedIndex) - - // Prepare item when it becomes visible (e.g., decode images) - Component.onCompleted: { - var provider = modelData.provider; - if (provider && provider.prepareItem) { - provider.prepareItem(modelData); - } - } - - width: resultsList.availableWidth - implicitHeight: root.entryHeight - clip: true - color: entry.isSelected ? Color.mHover : Color.mSurface - - Behavior on color { - ColorAnimation { - duration: Style.animationFast - easing.type: Easing.OutCirc - } - } - - ColumnLayout { - id: contentLayout - anchors.fill: parent - anchors.margins: root.isCompactDensity ? Style.marginXS : Style.marginM - spacing: root.isCompactDensity ? Style.marginXS : Style.marginM - - // Top row - Main entry content with action buttons - RowLayout { - Layout.fillWidth: true - spacing: root.isCompactDensity ? Style.marginS : Style.marginM - - // Icon badge or Image preview or Emoji - Item { - visible: !modelData.hideIcon - Layout.preferredWidth: modelData.hideIcon ? 0 : root.badgeSize - Layout.preferredHeight: modelData.hideIcon ? 0 : root.badgeSize - - // Icon background - Rectangle { - anchors.fill: parent - radius: Style.radiusXS - color: Color.mSurfaceVariant - visible: Settings.data.appLauncher.showIconBackground && !modelData.isImage - } - - // Image preview - uses provider's getImageUrl if available - NImageRounded { - id: imagePreview - anchors.fill: parent - visible: !!modelData.isImage && !modelData.displayString - radius: Style.radiusXS - borderColor: Color.mOnSurface - borderWidth: Style.borderM - imageFillMode: Image.PreserveAspectCrop - - // Use provider's image revision for reactive updates - readonly property int _rev: modelData.provider && modelData.provider.imageRevision ? modelData.provider.imageRevision : 0 - - // Get image URL from provider - imagePath: { - _rev; - var provider = modelData.provider; - if (provider && provider.getImageUrl) { - return provider.getImageUrl(modelData); - } - return ""; - } - - Rectangle { - anchors.fill: parent - visible: parent.status === Image.Loading - color: Color.mSurfaceVariant - - BusyIndicator { - anchors.centerIn: parent - running: true - width: Style.baseWidgetSize * 0.5 - height: width - } - } - - onStatusChanged: status => { - if (status === Image.Error) { - iconLoader.visible = true; - imagePreview.visible = false; - } - } - } - - Loader { - id: iconLoader - anchors.fill: parent - anchors.margins: Style.marginXS - - visible: (!modelData.isImage && !modelData.displayString) || (!!modelData.isImage && imagePreview.status === Image.Error) - active: visible - - sourceComponent: Component { - Loader { - anchors.fill: parent - sourceComponent: Settings.data.appLauncher.iconMode === "tabler" && modelData.isTablerIcon ? tablerIconComponent : systemIconComponent - } - } - - Component { - id: tablerIconComponent - NIcon { - icon: modelData.icon - pointSize: Style.fontSizeXXXL - visible: modelData.icon && !modelData.displayString - color: (entry.isSelected && !Settings.data.appLauncher.showIconBackground) ? Color.mOnHover : Color.mOnSurface - } - } - - Component { - id: systemIconComponent - IconImage { - anchors.fill: parent - source: modelData.icon ? ThemeIcons.iconFromName(modelData.icon, "application-x-executable") : "" - visible: modelData.icon && source !== "" && !modelData.displayString - asynchronous: true - } - } - } - - // String display - takes precedence when displayString is present - NText { - id: stringDisplay - anchors.centerIn: parent - visible: !!modelData.displayString || (!imagePreview.visible && !iconLoader.visible) - text: modelData.displayString ? modelData.displayString : (modelData.name ? modelData.name.charAt(0).toUpperCase() : "?") - pointSize: modelData.displayString ? (modelData.displayStringSize || Style.fontSizeXXXL) : Style.fontSizeXXL - font.weight: Style.fontWeightBold - color: modelData.displayString ? Color.mOnSurface : Color.mOnPrimary - } - - // Image type indicator overlay - Rectangle { - visible: !!modelData.isImage && imagePreview.visible - anchors.bottom: parent.bottom - anchors.right: parent.right - anchors.margins: 2 - width: formatLabel.width + Style.marginXS - height: formatLabel.height + Style.marginXXS - color: Color.mSurfaceVariant - radius: Style.radiusXXS - NText { - id: formatLabel - anchors.centerIn: parent - text: { - if (!modelData.isImage) - return ""; - const desc = modelData.description || ""; - const parts = desc.split(" \u2022 "); - return parts[0] || "IMG"; - } - pointSize: Style.fontSizeXXS - color: Color.mOnSurfaceVariant - } - } - - // Badge icon overlay (generic indicator for any provider) - Rectangle { - visible: !!modelData.badgeIcon - anchors.bottom: parent.bottom - anchors.right: parent.right - anchors.margins: 2 - width: height - height: Style.fontSizeM + Style.marginXS - color: Color.mSurfaceVariant - radius: Style.radiusXXS - NIcon { - anchors.centerIn: parent - icon: modelData.badgeIcon || "" - pointSize: Style.fontSizeS - color: Color.mOnSurfaceVariant - } - } - } - - // Text content - ColumnLayout { - Layout.fillWidth: true - spacing: 0 - - NText { - text: modelData.name || "Unknown" - pointSize: Style.fontSizeL - font.weight: Style.fontWeightBold - color: entry.isSelected ? Color.mOnHover : Color.mOnSurface - elide: Text.ElideRight - maximumLineCount: 1 - wrapMode: Text.Wrap - clip: true - Layout.fillWidth: true - } - - NText { - text: modelData.description || "" - pointSize: Style.fontSizeS - color: entry.isSelected ? Color.mOnHover : Color.mOnSurfaceVariant - elide: Text.ElideRight - maximumLineCount: 1 - Layout.fillWidth: true - visible: text !== "" && !root.isCompactDensity - } - } - - // Action buttons row - dynamically populated from provider - RowLayout { - Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - spacing: Style.marginXS - visible: entry.isSelected && itemActions.length > 0 - - property var itemActions: { - if (!entry.isSelected) - return []; - var provider = modelData.provider || root.currentProvider; - if (provider && provider.getItemActions) { - return provider.getItemActions(modelData); - } - return []; - } - - Repeater { - model: parent.itemActions - NIconButton { - icon: modelData.icon - baseSize: Style.baseWidgetSize * 0.75 - tooltipText: modelData.tooltip - z: 1 - onClicked: { - if (modelData.action) { - modelData.action(); - } - } - } - } - } - } - } - - MouseArea { - id: mouseArea - anchors.fill: parent - z: -1 - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - enabled: !Settings.data.appLauncher.ignoreMouseInput - onEntered: { - if (!root.ignoreMouseHover) { - root.selectedIndex = index; - } - } - onClicked: mouse => { - if (mouse.button === Qt.LeftButton) { - root.selectedIndex = index; - root.activate(); - mouse.accepted = true; - } - } - acceptedButtons: Qt.LeftButton - } + // Find first selectable item to default index + var initialIdx = 0; + while (initialIdx < results.length && results[initialIdx].isHeader) { + initialIdx++; } - } + selectedIndex = initialIdx < results.length ? initialIdx : 0; } - // -------------------------- - // SINGLE ITEM VIEW - Component { - id: singleViewComponent - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - - NBox { - anchors.fill: parent - color: Color.mSurfaceVariant - Layout.fillWidth: true - Layout.fillHeight: true - - ColumnLayout { - anchors.fill: parent - anchors.margins: Style.marginL - Layout.fillWidth: true - Layout.fillHeight: true - - Item { - Layout.alignment: Qt.AlignTop | Qt.AlignLeft - NText { - text: root.results.length > 0 ? root.results[0].name : "" - pointSize: Style.fontSizeL - font.weight: Font.Bold - color: Color.mPrimary - } + // Navigation functions + function selectNext() { + if (results.length > 0 && selectedIndex < results.length - 1) { + var nextIdx = selectedIndex + 1; + while (nextIdx < results.length && results[nextIdx].isHeader) { + nextIdx++; } - - NScrollView { - id: descriptionScrollView - Layout.alignment: Qt.AlignTop | Qt.AlignLeft - Layout.topMargin: Style.fontSizeL + Style.marginXL - Layout.fillWidth: true - Layout.fillHeight: true - horizontalPolicy: ScrollBar.AlwaysOff - reserveScrollbarSpace: false - - NText { - width: descriptionScrollView.availableWidth - text: root.results.length > 0 ? root.results[0].description : "" - pointSize: Style.fontSizeM - font.weight: Font.Bold - color: Color.mOnSurface - horizontalAlignment: Text.AlignHLeft - verticalAlignment: Text.AlignTop - wrapMode: Text.Wrap - markdownTextEnabled: true - } + if (nextIdx < results.length) { + selectedIndex = nextIdx; } - } } - } } - // -------------------------- - // GRID VIEW - Component { - id: gridViewComponent - NGridView { - id: resultsGrid - - horizontalPolicy: ScrollBar.AlwaysOff - verticalPolicy: ScrollBar.AlwaysOff - reserveScrollbarSpace: false - gradientColor: Color.mSurfaceVariant - wheelScrollMultiplier: 4.0 - trackedSelectionIndex: root.selectedIndex - - width: parent.width - height: parent.height - cellWidth: parent.width / root.targetGridColumns - cellHeight: { - var cellWidth = parent.width / root.targetGridColumns; - // Use provider's preferred ratio if available - if (root.currentProvider && root.currentProvider.preferredGridCellRatio) { - return cellWidth * root.currentProvider.preferredGridCellRatio; - } - return cellWidth; + function selectPrevious() { + if (results.length > 0 && selectedIndex > 0) { + var prevIdx = selectedIndex - 1; + while (prevIdx >= 0 && results[prevIdx].isHeader) { + prevIdx--; + } + if (prevIdx >= 0) { + selectedIndex = prevIdx; + } } - leftMargin: 0 - rightMargin: 0 - topMargin: 0 - bottomMargin: 0 - model: root.results - cacheBuffer: resultsGrid.height * 2 - keyNavigationEnabled: false - focus: false - interactive: !Settings.data.appLauncher.ignoreMouseInput + } - // Completely disable GridView key handling - Keys.enabled: false + function selectNextWrapped() { + if (results.length > 0) { + if (allowWrapNavigation) { + var nextIdx = (selectedIndex + 1) % results.length; + var scanned = 0; + while (scanned < results.length && results[nextIdx].isHeader) { + nextIdx = (nextIdx + 1) % results.length; + scanned++; + } + selectedIndex = nextIdx; + } else { + selectNext(); + } + } + } - Component.onCompleted: root.gridColumns = root.targetGridColumns - onWidthChanged: root.gridColumns = root.targetGridColumns - onModelChanged: {} + function selectPreviousWrapped() { + if (results.length > 0) { + if (allowWrapNavigation) { + var prevIdx = (((selectedIndex - 1) % results.length) + results.length) % results.length; + var scanned = 0; + while (scanned < results.length && results[prevIdx].isHeader) { + prevIdx = (((prevIdx - 1) % results.length) + results.length) % results.length; + scanned++; + } + selectedIndex = prevIdx; + } else { + selectPrevious(); + } + } + } - // Handle scrolling to show selected item when it changes - Connections { - target: root - enabled: root.isGridView - function onSelectedIndexChanged() { - if (!root.isGridView || root.selectedIndex < 0 || !resultsGrid) { - return; + function selectFirst() { + if (results.length === 0) + return; + var idx = 0; + while (idx < results.length && results[idx].isHeader) { + idx++; + } + selectedIndex = idx < results.length ? idx : 0; + } + + function selectLast() { + if (results.length === 0) + return; + var idx = results.length - 1; + while (idx >= 0 && results[idx].isHeader) { + idx--; + } + selectedIndex = idx >= 0 ? idx : Math.max(0, results.length - 1); + } + + function selectNextPage() { + if (results.length > 0) { + const page = Math.max(1, Math.floor(600 / entryHeight)); + selectedIndex = Math.min(selectedIndex + page, results.length - 1); + } + } + + function selectPreviousPage() { + if (results.length > 0) { + const page = Math.max(1, Math.floor(600 / entryHeight)); + selectedIndex = Math.max(selectedIndex - page, 0); + } + } + + // Grid view navigation functions + function selectPreviousRow() { + if (results.length > 0 && isGridView && gridColumns > 0) { + const currentRow = Math.floor(selectedIndex / gridColumns); + const currentCol = selectedIndex % gridColumns; + + if (currentRow > 0) { + const targetRow = currentRow - 1; + const targetIndex = targetRow * gridColumns + currentCol; + const itemsInTargetRow = Math.min(gridColumns, results.length - targetRow * gridColumns); + if (currentCol < itemsInTargetRow) { + selectedIndex = targetIndex; + } else { + selectedIndex = targetRow * gridColumns + itemsInTargetRow - 1; + } + } else { + // Wrap to last row, same column + const totalRows = Math.ceil(results.length / gridColumns); + const lastRow = totalRows - 1; + const itemsInLastRow = Math.min(gridColumns, results.length - lastRow * gridColumns); + if (currentCol < itemsInLastRow) { + selectedIndex = lastRow * gridColumns + currentCol; + } else { + selectedIndex = results.length - 1; + } + } + } + } + + function selectNextRow() { + if (results.length > 0 && isGridView && gridColumns > 0) { + const currentRow = Math.floor(selectedIndex / gridColumns); + const currentCol = selectedIndex % gridColumns; + const totalRows = Math.ceil(results.length / gridColumns); + + if (currentRow < totalRows - 1) { + const targetRow = currentRow + 1; + const targetIndex = targetRow * gridColumns + currentCol; + if (targetIndex < results.length) { + selectedIndex = targetIndex; + } else { + const itemsInTargetRow = results.length - targetRow * gridColumns; + if (itemsInTargetRow > 0) { + selectedIndex = targetRow * gridColumns + itemsInTargetRow - 1; + } else { + selectedIndex = Math.min(currentCol, results.length - 1); + } + } + } else { + // Wrap to first row, same column + selectedIndex = Math.min(currentCol, results.length - 1); + } + } + } + + function selectPreviousColumn() { + if (results.length > 0 && isGridView) { + const currentRow = Math.floor(selectedIndex / gridColumns); + const currentCol = selectedIndex % gridColumns; + if (currentCol > 0) { + selectedIndex = currentRow * gridColumns + (currentCol - 1); + } else if (currentRow > 0) { + selectedIndex = (currentRow - 1) * gridColumns + (gridColumns - 1); + } else { + const totalRows = Math.ceil(results.length / gridColumns); + const lastRowIndex = (totalRows - 1) * gridColumns + (gridColumns - 1); + selectedIndex = Math.min(lastRowIndex, results.length - 1); + } + } + } + + function selectNextColumn() { + if (results.length > 0 && isGridView) { + const currentRow = Math.floor(selectedIndex / gridColumns); + const currentCol = selectedIndex % gridColumns; + const itemsInCurrentRow = Math.min(gridColumns, results.length - currentRow * gridColumns); + + if (currentCol < itemsInCurrentRow - 1) { + selectedIndex = currentRow * gridColumns + (currentCol + 1); + } else { + const totalRows = Math.ceil(results.length / gridColumns); + if (currentRow < totalRows - 1) { + selectedIndex = (currentRow + 1) * gridColumns; + } else { + selectedIndex = 0; + } + } + } + } + + function activate() { + if (results.length > 0 && results[selectedIndex]) { + const item = results[selectedIndex]; + const provider = item.provider || currentProvider; + + // Check if auto-paste is enabled and provider/item supports it + if (Settings.data.appLauncher.autoPasteClipboard && provider && provider.supportsAutoPaste && item.autoPasteText) { + if (item.onAutoPaste) + item.onAutoPaste(); + closeImmediately(); + Qt.callLater(() => { + ClipboardService.pasteText(item.autoPasteText); + }); + return; } - Qt.callLater(() => { - if (root.isGridView && resultsGrid && resultsGrid.cancelFlick) { - resultsGrid.cancelFlick(); - resultsGrid.positionViewAtIndex(root.selectedIndex, GridView.Contain); - } - }); - } + if (item.onActivate) + item.onActivate(); + } + } + + function checkKey(event, settingName) { + return Keybinds.checkKey(event, settingName, Settings); + } + + // Keyboard handler + function handleKeyPress(event) { + if (checkKey(event, 'escape')) { + close(); + event.accepted = true; + return; } - delegate: Item { - id: gridEntryContainer - width: resultsGrid.cellWidth - height: resultsGrid.cellHeight - - property bool isSelected: (!root.ignoreMouseHover && mouseArea.containsMouse) || (index === root.selectedIndex) - - // Prepare item when it becomes visible (e.g., decode images) - Component.onCompleted: { - var provider = modelData.provider; - if (provider && provider.prepareItem) { - provider.prepareItem(modelData); - } - } - - NBox { - id: gridEntry - anchors.fill: parent - anchors.margins: Style.marginXXS - color: gridEntryContainer.isSelected ? Color.mHover : Color.mSurface - - Behavior on color { - ColorAnimation { - duration: Style.animationFast - easing.type: Easing.OutCirc - } - } - - ColumnLayout { - anchors.fill: parent - anchors.margins: root.isCompactDensity ? Style.marginXS : Style.marginS - anchors.bottomMargin: root.isCompactDensity ? Style.marginXS : Style.marginS - spacing: root.isCompactDensity ? 0 : Style.marginXXS - - // Icon badge or Image preview or Emoji - Item { - // Use consistent 65% sizing for all items - Layout.preferredWidth: Math.round(gridEntry.width * 0.65) - Layout.preferredHeight: Math.round(gridEntry.width * 0.65) - Layout.alignment: Qt.AlignHCenter - - // Icon background - Rectangle { - anchors.fill: parent - radius: Style.radiusM - color: Color.mSurfaceVariant - visible: Settings.data.appLauncher.showIconBackground && !modelData.isImage - } - - // Image preview - uses provider's getImageUrl if available - NImageRounded { - id: gridImagePreview - anchors.fill: parent - visible: !!modelData.isImage && !modelData.displayString - radius: Style.radiusM - - // Use provider's image revision for reactive updates - readonly property int _rev: modelData.provider && modelData.provider.imageRevision ? modelData.provider.imageRevision : 0 - - // Get image URL from provider - imagePath: { - _rev; - var provider = modelData.provider; - if (provider && provider.getImageUrl) { - return provider.getImageUrl(modelData); - } - return ""; - } - - Rectangle { - anchors.fill: parent - visible: parent.status === Image.Loading - color: Color.mSurfaceVariant - - BusyIndicator { - anchors.centerIn: parent - running: true - width: Style.baseWidgetSize * 0.5 - height: width - } - } - - onStatusChanged: status => { - if (status === Image.Error) { - gridIconLoader.visible = true; - gridImagePreview.visible = false; - } - } - } - - Loader { - id: gridIconLoader - anchors.fill: parent - anchors.margins: Style.marginXS - - visible: (!modelData.isImage && !modelData.displayString) || (!!modelData.isImage && gridImagePreview.status === Image.Error) - active: visible - - sourceComponent: Settings.data.appLauncher.iconMode === "tabler" && modelData.isTablerIcon ? gridTablerIconComponent : gridSystemIconComponent - - Component { - id: gridTablerIconComponent - NIcon { - icon: modelData.icon - pointSize: Style.fontSizeXXXL - visible: modelData.icon && !modelData.displayString - color: (gridEntryContainer.isSelected && !Settings.data.appLauncher.showIconBackground) ? Color.mOnHover : Color.mOnSurface - } - } - - Component { - id: gridSystemIconComponent - IconImage { - anchors.fill: parent - source: modelData.icon ? ThemeIcons.iconFromName(modelData.icon, "application-x-executable") : "" - visible: modelData.icon && source !== "" && !modelData.displayString - asynchronous: true - } - } - } - - // String display - NText { - id: gridStringDisplay - anchors.centerIn: parent - visible: !!modelData.displayString || (!gridImagePreview.visible && !gridIconLoader.visible) - text: modelData.displayString ? modelData.displayString : (modelData.name ? modelData.name.charAt(0).toUpperCase() : "?") - pointSize: { - if (modelData.displayString) { - // Use custom size if provided, otherwise default scaling - if (modelData.displayStringSize) { - return modelData.displayStringSize * Style.uiScaleRatio; - } - if (root.providerHasDisplayString) { - // Scale with cell width but cap at reasonable maximum - const cellBasedSize = gridEntry.width * 0.4; - const maxSize = Style.fontSizeXXXL * Style.uiScaleRatio; - return Math.min(cellBasedSize, maxSize); - } - return Style.fontSizeXXL * 2 * Style.uiScaleRatio; - } - // Scale font size relative to cell width for low res, but cap at maximum - const cellBasedSize = gridEntry.width * 0.25; - const baseSize = Style.fontSizeXL * Style.uiScaleRatio; - const maxSize = Style.fontSizeXXL * Style.uiScaleRatio; - return Math.min(Math.max(cellBasedSize, baseSize), maxSize); - } - font.weight: Style.fontWeightBold - color: modelData.displayString ? Color.mOnSurface : Color.mOnPrimary - } - - // Badge icon overlay (generic indicator for any provider) - Rectangle { - visible: !!modelData.badgeIcon - anchors.bottom: parent.bottom - anchors.right: parent.right - anchors.margins: 2 - width: height - height: Style.fontSizeM + Style.marginXS - color: Color.mSurfaceVariant - radius: Style.radiusXXS - NIcon { - anchors.centerIn: parent - icon: modelData.badgeIcon || "" - pointSize: Style.fontSizeS - color: Color.mOnSurfaceVariant - } - } - } - - // Text content (hidden when hideLabel is true) - NText { - visible: !modelData.hideLabel - text: modelData.name || "Unknown" - pointSize: { - if (root.providerHasDisplayString && modelData.displayString) { - return Style.fontSizeS * Style.uiScaleRatio; - } - // Scale font size relative to cell width for low res, but cap at maximum - const cellBasedSize = gridEntry.width * 0.12; - const baseSize = Style.fontSizeS * Style.uiScaleRatio; - const maxSize = Style.fontSizeM * Style.uiScaleRatio; - return Math.min(Math.max(cellBasedSize, baseSize), maxSize); - } - font.weight: Style.fontWeightSemiBold - color: gridEntryContainer.isSelected ? Color.mOnHover : Color.mOnSurface - elide: Text.ElideRight - Layout.fillWidth: true - Layout.maximumWidth: gridEntry.width - 8 - Layout.leftMargin: (root.providerHasDisplayString && modelData.displayString) ? Style.marginS : 0 - Layout.rightMargin: (root.providerHasDisplayString && modelData.displayString) ? Style.marginS : 0 - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.NoWrap - maximumLineCount: 1 - } - } - - // Action buttons (overlay in top-right corner) - dynamically populated from provider - Row { - visible: gridEntryContainer.isSelected && gridItemActions.length > 0 - anchors.top: parent.top - anchors.right: parent.right - anchors.margins: Style.marginXS - z: 10 - spacing: Style.marginXXS - - property var gridItemActions: { - if (!gridEntryContainer.isSelected) - return []; - var provider = modelData.provider || root.currentProvider; - if (provider && provider.getItemActions) { - return provider.getItemActions(modelData); - } - return []; - } - - Repeater { - model: parent.gridItemActions - NIconButton { - icon: modelData.icon - baseSize: Style.baseWidgetSize * 0.75 - tooltipText: modelData.tooltip - z: 11 - onClicked: { - if (modelData.action) { - modelData.action(); - } - } - } - } - } - } - - MouseArea { - id: mouseArea - anchors.fill: parent - z: -1 - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - enabled: !Settings.data.appLauncher.ignoreMouseInput - - onEntered: { - if (!root.ignoreMouseHover) { - root.selectedIndex = index; - } - } - onClicked: mouse => { - if (mouse.button === Qt.LeftButton) { - root.selectedIndex = index; - root.activate(); - mouse.accepted = true; - } - } - acceptedButtons: Qt.LeftButton - } + if (checkKey(event, 'enter')) { + activate(); + event.accepted = true; + return; + } + + if (checkKey(event, 'up')) { + if (!isSingleView) { + isGridView ? selectPreviousRow() : selectPreviousWrapped(); + } + event.accepted = true; + return; + } + + if (checkKey(event, 'down')) { + if (!isSingleView) { + isGridView ? selectNextRow() : selectNextWrapped(); + } + event.accepted = true; + return; + } + + if (checkKey(event, 'left')) { + if (isGridView) { + selectPreviousColumn(); + event.accepted = true; + return; + } + } + + if (checkKey(event, 'right')) { + if (isGridView) { + selectNextColumn(); + event.accepted = true; + return; + } + } + + // Static bindings + switch (event.key) { + case Qt.Key_Tab: + if (showProviderCategories) { + var cats = providerCategories; + var idx = cats.indexOf(currentProvider.selectedCategory); + currentProvider.selectCategory(cats[(idx + 1) % cats.length]); + } else { + selectNextWrapped(); + } + event.accepted = true; + break; + case Qt.Key_Backtab: + if (showProviderCategories) { + var cats2 = providerCategories; + var idx2 = cats2.indexOf(currentProvider.selectedCategory); + currentProvider.selectCategory(cats2[((idx2 - 1) % cats2.length + cats2.length) % cats2.length]); + } else { + selectPreviousWrapped(); + } + event.accepted = true; + break; + case Qt.Key_Home: + selectFirst(); + event.accepted = true; + break; + case Qt.Key_End: + selectLast(); + event.accepted = true; + break; + case Qt.Key_PageUp: + selectPreviousPage(); + event.accepted = true; + break; + case Qt.Key_PageDown: + selectNextPage(); + event.accepted = true; + break; + case Qt.Key_Delete: + if (selectedIndex >= 0 && results && results[selectedIndex]) { + var item = results[selectedIndex]; + var provider = item.provider || currentProvider; + if (provider && provider.canDeleteItem && provider.canDeleteItem(item)) + provider.deleteItem(item); + } + event.accepted = true; + break; + } + } + + // ----------------------- + // Provider components + // ----------------------- + ApplicationsProvider { + id: appsProvider + Component.onCompleted: { + registerProvider(this); + Logger.d("Launcher", "Registered: ApplicationsProvider"); + } + } + + ClipboardProvider { + id: clipProvider + Component.onCompleted: { + if (Settings.data.appLauncher.enableClipboardHistory) { + registerProvider(this); + Logger.d("Launcher", "Registered: ClipboardProvider"); + } + } + } + + CommandProvider { + id: cmdProvider + Component.onCompleted: { + registerProvider(this); + Logger.d("Launcher", "Registered: CommandProvider"); + } + } + + EmojiProvider { + id: emojiProvider + Component.onCompleted: { + registerProvider(this); + Logger.d("Launcher", "Registered: EmojiProvider"); + } + } + + CalculatorProvider { + id: calcProvider + Component.onCompleted: { + registerProvider(this); + Logger.d("Launcher", "Registered: CalculatorProvider"); + } + } + + SettingsProvider { + id: settingsProvider + Component.onCompleted: { + registerProvider(this); + Logger.d("Launcher", "Registered: SettingsProvider"); + } + } + + SessionProvider { + id: sessionProvider + Component.onCompleted: { + registerProvider(this); + Logger.d("Launcher", "Registered: SessionProvider"); + } + } + + WindowsProvider { + id: windowsProvider + Component.onCompleted: { + registerProvider(this); + Logger.d("Launcher", "Registered: WindowsProvider"); + } + } + + // ==================== UI Content ==================== + + opacity: resultsReady ? 1.0 : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCirc + } + } + + HoverHandler { + id: globalHoverHandler + enabled: !Settings.data.appLauncher.ignoreMouseInput + + onPointChanged: { + if (!root.mouseTrackingReady) { + return; + } + + if (!root.globalMouseInitialized) { + root.globalLastMouseX = point.position.x; + root.globalLastMouseY = point.position.y; + root.globalMouseInitialized = true; + return; + } + + const deltaX = Math.abs(point.position.x - root.globalLastMouseX); + const deltaY = Math.abs(point.position.y - root.globalLastMouseY); + if (deltaX + deltaY >= 5) { + root.ignoreMouseHover = false; + root.globalLastMouseX = point.position.x; + root.globalLastMouseY = point.position.y; + } } - } } ColumnLayout { - Layout.leftMargin: Style.marginL - Layout.rightMargin: Style.marginL + anchors.fill: parent + anchors.topMargin: Style.marginL + anchors.bottomMargin: Style.marginL + spacing: Style.marginL - NDivider { - Layout.fillWidth: true - Layout.bottomMargin: Style.marginS - } + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: Style.marginL + Layout.rightMargin: Style.marginL + spacing: Style.marginS - NText { - Layout.fillWidth: true - text: { - if (root.results.length === 0) { - if (root.searchText) { - return I18n.tr("common.no-results"); + NTextInput { + id: searchInput + Layout.fillWidth: true + radius: Style.iRadiusM + text: root.searchText + placeholderText: I18n.tr("placeholders.search-launcher") + fontSize: Style.fontSizeM + onTextChanged: root.searchText = text + + Component.onCompleted: { + if (searchInput.inputItem) { + searchInput.inputItem.forceActiveFocus(); + searchInput.inputItem.Keys.onPressed.connect(function (event) { + root.handleKeyPress(event); + }); + } + } } - // Use provider's empty browsing message if available - var provider = root.currentProvider; - if (provider && provider.emptyBrowsingMessage) { - return provider.emptyBrowsingMessage; + + NIconButton { + id: dateFilterButton + visible: root.activeProvider && root.activeProvider.hasDateFilter + icon: root.activeProvider && root.activeProvider.dateFilter !== "all" ? "filter-check" : "filter" + tooltipText: I18n.tr("tooltips.date-filter") || "Date Filter" + customRadius: Style.iRadiusM + Layout.preferredWidth: searchInput.height + Layout.preferredHeight: searchInput.height + + onClicked: { + dateFilterMenu.openAtItem(dateFilterButton, 0, dateFilterButton.height + Style.marginS); + } + + NContextMenu { + id: dateFilterMenu + parent: Overlay.overlay + model: root.activeProvider ? root.activeProvider.availableDateFilters : [] + onTriggered: action => { + if (root.activeProvider && root.activeProvider.selectDateFilter) { + root.activeProvider.selectDateFilter(action); + } + } + } + } + + NIconButton { + visible: root.showLayoutToggle + icon: Settings.data.appLauncher.viewMode === "grid" ? "layout-list" : "layout-grid" + tooltipText: Settings.data.appLauncher.viewMode === "grid" ? I18n.tr("tooltips.list-view") : I18n.tr("tooltips.grid-view") + customRadius: Style.iRadiusM + Layout.preferredWidth: searchInput.height + Layout.preferredHeight: searchInput.height + onClicked: Settings.data.appLauncher.viewMode = Settings.data.appLauncher.viewMode === "grid" ? "list" : "grid" + } + } + + // Unified category tabs (works with any provider that has categories) + NTabBar { + id: categoryTabs + visible: root.showProviderCategories + Layout.fillWidth: true + Layout.leftMargin: Style.marginL + Layout.rightMargin: Style.marginL + margins: 0 + border.color: Style.boxBorderColor + border.width: Style.borderS + + property int computedCurrentIndex: visible && root.providerCategories.length > 0 ? root.providerCategories.indexOf(root.currentProvider.selectedCategory) : 0 + currentIndex: computedCurrentIndex + + Repeater { + model: root.providerCategories + NTabButton { + required property string modelData + required property int index + icon: root.currentProvider.categoryIcons ? (root.currentProvider.categoryIcons[modelData] || "star") : "star" + tooltipText: root.currentProvider.getCategoryName ? root.currentProvider.getCategoryName(modelData) : modelData + tabIndex: index + checked: categoryTabs.currentIndex === index + onClicked: root.currentProvider.selectCategory(modelData) + } + } + } + + // Results view + Loader { + id: resultsViewLoader + Layout.fillWidth: true + Layout.leftMargin: Style.marginL + Layout.rightMargin: Style.marginL + Layout.fillHeight: true + sourceComponent: root.isSingleView ? singleViewComponent : (root.isGridView ? gridViewComponent : listViewComponent) + } + + // -------------------------- + // LIST VIEW + Component { + id: listViewComponent + NListView { + id: resultsList + + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AlwaysOff + reserveScrollbarSpace: false + gradientColor: Color.mSurfaceVariant + wheelScrollMultiplier: 4.0 + + width: parent.width + height: parent.height + spacing: Style.marginS + model: root.results + currentIndex: root.selectedIndex + cacheBuffer: resultsList.height * 2 + interactive: !Settings.data.appLauncher.ignoreMouseInput + onCurrentIndexChanged: { + cancelFlick(); + if (currentIndex >= 0) { + positionViewAtIndex(currentIndex, ListView.Contain); + } + } + onModelChanged: {} + + delegate: NBox { + id: entry + + property bool isSelected: (!modelData.isHeader) && ((!root.ignoreMouseHover && mouseArea.containsMouse) || (index === root.selectedIndex)) + + // Prepare item when it becomes visible (e.g., decode images) + Component.onCompleted: { + var provider = modelData.provider; + if (provider && provider.prepareItem) { + provider.prepareItem(modelData); + } + } + + width: resultsList.availableWidth + implicitHeight: modelData.isHeader ? Math.round(root.entryHeight * 0.45) : root.entryHeight + clip: true + color: entry.isSelected ? Color.mHover : Color.mSurface + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCirc + } + } + + ColumnLayout { + id: contentLayout + anchors.fill: parent + anchors.margins: modelData.isHeader ? Style.marginXXS : (root.isCompactDensity ? Style.marginXS : Style.marginM) + spacing: root.isCompactDensity ? Style.marginXS : Style.marginM + + // Top row - Main entry content with action buttons + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: root.isCompactDensity ? Style.marginS : Style.marginM + + // Icon badge or Image preview or Emoji + Item { + visible: !modelData.hideIcon + Layout.preferredWidth: modelData.hideIcon ? 0 : root.badgeSize + Layout.preferredHeight: modelData.hideIcon ? 0 : root.badgeSize + + // Icon background + Rectangle { + anchors.fill: parent + radius: Style.radiusXS + color: modelData.colorHex ? modelData.colorHex : Color.mSurfaceVariant + visible: (Settings.data.appLauncher.showIconBackground && !modelData.isImage) || !!modelData.colorHex + border.width: modelData.colorHex ? 1 : 0 + border.color: modelData.colorHex ? Color.mOnSurfaceVariant : "transparent" + } + + // Image preview - uses provider's getImageUrl if available + NImageRounded { + id: imagePreview + anchors.fill: parent + visible: !!modelData.isImage && !modelData.displayString + radius: Style.radiusXS + borderColor: Color.mOnSurface + borderWidth: Style.borderM + imageFillMode: Image.PreserveAspectCrop + + // Use provider's image revision for reactive updates + readonly property int _rev: modelData.provider && modelData.provider.imageRevision ? modelData.provider.imageRevision : 0 + + // Get image URL from provider + imagePath: { + _rev; + var provider = modelData.provider; + if (provider && provider.getImageUrl) { + return provider.getImageUrl(modelData); + } + return ""; + } + + Rectangle { + anchors.fill: parent + visible: parent.status === Image.Loading + color: Color.mSurfaceVariant + + BusyIndicator { + anchors.centerIn: parent + running: true + width: Style.baseWidgetSize * 0.5 + height: width + } + } + + onStatusChanged: status => { + if (status === Image.Error) { + iconLoader.visible = true; + imagePreview.visible = false; + } + } + } + + Loader { + id: iconLoader + anchors.fill: parent + anchors.margins: Style.marginXS + + visible: (!modelData.isImage && !modelData.displayString && !modelData.colorHex) || (!!modelData.isImage && imagePreview.status === Image.Error) + active: visible + + sourceComponent: Component { + Loader { + anchors.fill: parent + sourceComponent: Settings.data.appLauncher.iconMode === "tabler" && modelData.isTablerIcon ? tablerIconComponent : systemIconComponent + } + } + + Component { + id: tablerIconComponent + NIcon { + icon: modelData.icon + pointSize: Style.fontSizeXXXL + visible: modelData.icon && !modelData.displayString + color: (entry.isSelected && !Settings.data.appLauncher.showIconBackground) ? Color.mOnHover : Color.mOnSurface + } + } + + Component { + id: systemIconComponent + IconImage { + anchors.fill: parent + source: modelData.icon ? ThemeIcons.iconFromName(modelData.icon, "application-x-executable") : "" + visible: modelData.icon && source !== "" && !modelData.displayString + asynchronous: true + } + } + } + + // String display - takes precedence when displayString is present + NText { + id: stringDisplay + anchors.centerIn: parent + visible: !!modelData.displayString || (!imagePreview.visible && !iconLoader.visible && !modelData.colorHex) + text: modelData.displayString ? modelData.displayString : (modelData.name ? modelData.name.charAt(0).toUpperCase() : "?") + pointSize: modelData.displayString ? (modelData.displayStringSize || Style.fontSizeXXXL) : Style.fontSizeXXL + font.weight: Style.fontWeightBold + color: modelData.displayString ? Color.mOnSurface : Color.mOnPrimary + } + + // Image type indicator overlay + Rectangle { + visible: !!modelData.isImage && imagePreview.visible + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.margins: 2 + width: formatLabel.width + Style.marginXS + height: formatLabel.height + Style.marginXXS + color: Color.mSurfaceVariant + radius: Style.radiusXXS + NText { + id: formatLabel + anchors.centerIn: parent + text: { + if (!modelData.isImage) + return ""; + const desc = modelData.description || ""; + const parts = desc.split(" \u2022 "); + return parts[0] || "IMG"; + } + pointSize: Style.fontSizeXXS + color: Color.mOnSurfaceVariant + } + } + + // Badge icon overlay (generic indicator for any provider) + Rectangle { + visible: !!modelData.badgeIcon + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.margins: 2 + width: height + height: Style.fontSizeM + Style.marginXS + color: Color.mSurfaceVariant + radius: Style.radiusXXS + NIcon { + anchors.centerIn: parent + icon: modelData.badgeIcon || "" + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + } + } + } + + // Text content + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + Layout.leftMargin: modelData.isHeader ? Style.marginM : 0 + spacing: 0 + + NText { + text: modelData.name || "Unknown" + pointSize: modelData.isHeader ? Style.fontSizeM : Style.fontSizeL + font.weight: Style.fontWeightBold + color: entry.isSelected ? Color.mOnHover : Color.mOnSurface + elide: Text.ElideRight + maximumLineCount: 1 + wrapMode: Text.Wrap + clip: true + Layout.fillWidth: true + } + + NText { + text: modelData.description || "" + pointSize: Style.fontSizeS + color: entry.isSelected ? Color.mOnHover : Color.mOnSurfaceVariant + elide: Text.ElideRight + maximumLineCount: 1 + Layout.fillWidth: true + visible: text !== "" && !root.isCompactDensity + } + } + + // Action buttons row - dynamically populated from provider + RowLayout { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + spacing: Style.marginXS + visible: entry.isSelected && itemActions.length > 0 + + property var itemActions: { + if (!entry.isSelected) + return []; + var provider = modelData.provider || root.currentProvider; + if (provider && provider.getItemActions) { + return provider.getItemActions(modelData); + } + return []; + } + + Repeater { + model: parent.itemActions + NIconButton { + icon: modelData.icon + baseSize: Style.baseWidgetSize * 0.75 + tooltipText: modelData.tooltip + z: 1 + onClicked: { + if (modelData.action) { + modelData.action(); + } + } + } + } + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + z: -1 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: !Settings.data.appLauncher.ignoreMouseInput + onEntered: { + if (!root.ignoreMouseHover && !modelData.isHeader) { + root.selectedIndex = index; + } + } + onClicked: mouse => { + if (mouse.button === Qt.LeftButton && !modelData.isHeader) { + root.selectedIndex = index; + root.activate(); + mouse.accepted = true; + } + } + acceptedButtons: Qt.LeftButton + } + } + } + } + + // -------------------------- + // SINGLE ITEM VIEW + Component { + id: singleViewComponent + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + NBox { + anchors.fill: parent + color: Color.mSurfaceVariant + Layout.fillWidth: true + Layout.fillHeight: true + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginL + Layout.fillWidth: true + Layout.fillHeight: true + + Item { + Layout.alignment: Qt.AlignTop | Qt.AlignLeft + NText { + text: root.results.length > 0 ? root.results[0].name : "" + pointSize: Style.fontSizeL + font.weight: Font.Bold + color: Color.mPrimary + } + } + + NScrollView { + id: descriptionScrollView + Layout.alignment: Qt.AlignTop | Qt.AlignLeft + Layout.topMargin: Style.fontSizeL + Style.marginXL + Layout.fillWidth: true + Layout.fillHeight: true + horizontalPolicy: ScrollBar.AlwaysOff + reserveScrollbarSpace: false + + NText { + width: descriptionScrollView.availableWidth + text: root.results.length > 0 ? root.results[0].description : "" + pointSize: Style.fontSizeM + font.weight: Font.Bold + color: Color.mOnSurface + horizontalAlignment: Text.AlignHLeft + verticalAlignment: Text.AlignTop + wrapMode: Text.Wrap + markdownTextEnabled: true + } + } + } + } + } + } + + // -------------------------- + // GRID VIEW + Component { + id: gridViewComponent + NGridView { + id: resultsGrid + + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AlwaysOff + reserveScrollbarSpace: false + gradientColor: Color.mSurfaceVariant + wheelScrollMultiplier: 4.0 + trackedSelectionIndex: root.selectedIndex + + width: parent.width + height: parent.height + cellWidth: parent.width / root.targetGridColumns + cellHeight: { + var cellWidth = parent.width / root.targetGridColumns; + // Use provider's preferred ratio if available + if (root.currentProvider && root.currentProvider.preferredGridCellRatio) { + return cellWidth * root.currentProvider.preferredGridCellRatio; + } + return cellWidth; + } + leftMargin: 0 + rightMargin: 0 + topMargin: 0 + bottomMargin: 0 + model: root.results + cacheBuffer: resultsGrid.height * 2 + keyNavigationEnabled: false + focus: false + interactive: !Settings.data.appLauncher.ignoreMouseInput + + // Completely disable GridView key handling + Keys.enabled: false + + Component.onCompleted: root.gridColumns = root.targetGridColumns + onWidthChanged: root.gridColumns = root.targetGridColumns + onModelChanged: {} + + // Handle scrolling to show selected item when it changes + Connections { + target: root + enabled: root.isGridView + function onSelectedIndexChanged() { + if (!root.isGridView || root.selectedIndex < 0 || !resultsGrid) { + return; + } + + Qt.callLater(() => { + if (root.isGridView && resultsGrid && resultsGrid.cancelFlick) { + resultsGrid.cancelFlick(); + resultsGrid.positionViewAtIndex(root.selectedIndex, GridView.Contain); + } + }); + } + } + + delegate: Item { + id: gridEntryContainer + width: resultsGrid.cellWidth + height: resultsGrid.cellHeight + + property bool isSelected: (!root.ignoreMouseHover && mouseArea.containsMouse) || (index === root.selectedIndex) + + // Prepare item when it becomes visible (e.g., decode images) + Component.onCompleted: { + var provider = modelData.provider; + if (provider && provider.prepareItem) { + provider.prepareItem(modelData); + } + } + + NBox { + id: gridEntry + anchors.fill: parent + anchors.margins: Style.marginXXS + color: gridEntryContainer.isSelected ? Color.mHover : Color.mSurface + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCirc + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: root.isCompactDensity ? Style.marginXS : Style.marginS + anchors.bottomMargin: root.isCompactDensity ? Style.marginXS : Style.marginS + spacing: root.isCompactDensity ? 0 : Style.marginXXS + + // Icon badge or Image preview or Emoji + Item { + // Use consistent 65% sizing for all items + Layout.preferredWidth: Math.round(gridEntry.width * 0.65) + Layout.preferredHeight: Math.round(gridEntry.width * 0.65) + Layout.alignment: Qt.AlignHCenter + + // Icon background + Rectangle { + anchors.fill: parent + radius: Style.radiusM + color: Color.mSurfaceVariant + visible: Settings.data.appLauncher.showIconBackground && !modelData.isImage + } + + // Image preview - uses provider's getImageUrl if available + NImageRounded { + id: gridImagePreview + anchors.fill: parent + visible: !!modelData.isImage && !modelData.displayString + radius: Style.radiusM + + // Use provider's image revision for reactive updates + readonly property int _rev: modelData.provider && modelData.provider.imageRevision ? modelData.provider.imageRevision : 0 + + // Get image URL from provider + imagePath: { + _rev; + var provider = modelData.provider; + if (provider && provider.getImageUrl) { + return provider.getImageUrl(modelData); + } + return ""; + } + + Rectangle { + anchors.fill: parent + visible: parent.status === Image.Loading + color: Color.mSurfaceVariant + + BusyIndicator { + anchors.centerIn: parent + running: true + width: Style.baseWidgetSize * 0.5 + height: width + } + } + + onStatusChanged: status => { + if (status === Image.Error) { + gridIconLoader.visible = true; + gridImagePreview.visible = false; + } + } + } + + Loader { + id: gridIconLoader + anchors.fill: parent + anchors.margins: Style.marginXS + + visible: (!modelData.isImage && !modelData.displayString) || (!!modelData.isImage && gridImagePreview.status === Image.Error) + active: visible + + sourceComponent: Settings.data.appLauncher.iconMode === "tabler" && modelData.isTablerIcon ? gridTablerIconComponent : gridSystemIconComponent + + Component { + id: gridTablerIconComponent + NIcon { + icon: modelData.icon + pointSize: Style.fontSizeXXXL + visible: modelData.icon && !modelData.displayString + color: (gridEntryContainer.isSelected && !Settings.data.appLauncher.showIconBackground) ? Color.mOnHover : Color.mOnSurface + } + } + + Component { + id: gridSystemIconComponent + IconImage { + anchors.fill: parent + source: modelData.icon ? ThemeIcons.iconFromName(modelData.icon, "application-x-executable") : "" + visible: modelData.icon && source !== "" && !modelData.displayString + asynchronous: true + } + } + } + + // String display + NText { + id: gridStringDisplay + anchors.centerIn: parent + visible: !!modelData.displayString || (!gridImagePreview.visible && !gridIconLoader.visible) + text: modelData.displayString ? modelData.displayString : (modelData.name ? modelData.name.charAt(0).toUpperCase() : "?") + pointSize: { + if (modelData.displayString) { + // Use custom size if provided, otherwise default scaling + if (modelData.displayStringSize) { + return modelData.displayStringSize * Style.uiScaleRatio; + } + if (root.providerHasDisplayString) { + // Scale with cell width but cap at reasonable maximum + const cellBasedSize = gridEntry.width * 0.4; + const maxSize = Style.fontSizeXXXL * Style.uiScaleRatio; + return Math.min(cellBasedSize, maxSize); + } + return Style.fontSizeXXL * 2 * Style.uiScaleRatio; + } + // Scale font size relative to cell width for low res, but cap at maximum + const cellBasedSize = gridEntry.width * 0.25; + const baseSize = Style.fontSizeXL * Style.uiScaleRatio; + const maxSize = Style.fontSizeXXL * Style.uiScaleRatio; + return Math.min(Math.max(cellBasedSize, baseSize), maxSize); + } + font.weight: Style.fontWeightBold + color: modelData.displayString ? Color.mOnSurface : Color.mOnPrimary + } + + // Badge icon overlay (generic indicator for any provider) + Rectangle { + visible: !!modelData.badgeIcon + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.margins: 2 + width: height + height: Style.fontSizeM + Style.marginXS + color: Color.mSurfaceVariant + radius: Style.radiusXXS + NIcon { + anchors.centerIn: parent + icon: modelData.badgeIcon || "" + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + } + } + } + + // Text content (hidden when hideLabel is true) + NText { + visible: !modelData.hideLabel + text: modelData.name || "Unknown" + pointSize: { + if (root.providerHasDisplayString && modelData.displayString) { + return Style.fontSizeS * Style.uiScaleRatio; + } + // Scale font size relative to cell width for low res, but cap at maximum + const cellBasedSize = gridEntry.width * 0.12; + const baseSize = Style.fontSizeS * Style.uiScaleRatio; + const maxSize = Style.fontSizeM * Style.uiScaleRatio; + return Math.min(Math.max(cellBasedSize, baseSize), maxSize); + } + font.weight: Style.fontWeightSemiBold + color: gridEntryContainer.isSelected ? Color.mOnHover : Color.mOnSurface + elide: Text.ElideRight + Layout.fillWidth: true + Layout.maximumWidth: gridEntry.width - 8 + Layout.leftMargin: (root.providerHasDisplayString && modelData.displayString) ? Style.marginS : 0 + Layout.rightMargin: (root.providerHasDisplayString && modelData.displayString) ? Style.marginS : 0 + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.NoWrap + maximumLineCount: 1 + } + } + + // Action buttons (overlay in top-right corner) - dynamically populated from provider + Row { + visible: gridEntryContainer.isSelected && gridItemActions.length > 0 + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Style.marginXS + z: 10 + spacing: Style.marginXXS + + property var gridItemActions: { + if (!gridEntryContainer.isSelected) + return []; + var provider = modelData.provider || root.currentProvider; + if (provider && provider.getItemActions) { + return provider.getItemActions(modelData); + } + return []; + } + + Repeater { + model: parent.gridItemActions + NIconButton { + icon: modelData.icon + baseSize: Style.baseWidgetSize * 0.75 + tooltipText: modelData.tooltip + z: 11 + onClicked: { + if (modelData.action) { + modelData.action(); + } + } + } + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + z: -1 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: !Settings.data.appLauncher.ignoreMouseInput + + onEntered: { + if (!root.ignoreMouseHover && !modelData.isHeader) { + root.selectedIndex = index; + } + } + onClicked: mouse => { + if (mouse.button === Qt.LeftButton && !modelData.isHeader) { + root.selectedIndex = index; + root.activate(); + mouse.accepted = true; + } + } + acceptedButtons: Qt.LeftButton + } + } + } + } + + ColumnLayout { + Layout.leftMargin: Style.marginL + Layout.rightMargin: Style.marginL + + NDivider { + Layout.fillWidth: true + Layout.bottomMargin: Style.marginS + } + + NText { + Layout.fillWidth: true + text: { + if (root.results.length === 0) { + if (root.searchText) { + return I18n.tr("common.no-results"); + } + // Use provider's empty browsing message if available + var provider = root.currentProvider; + if (provider && provider.emptyBrowsingMessage) { + return provider.emptyBrowsingMessage; + } + return ""; + } + var prefix = root.activeProvider && root.activeProvider.name ? root.activeProvider.name + ": " : ""; + return prefix + I18n.trp("common.result-count", root.results.length); + } + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + horizontalAlignment: Text.AlignCenter } - return ""; - } - var prefix = root.activeProvider && root.activeProvider.name ? root.activeProvider.name + ": " : ""; - return prefix + I18n.trp("common.result-count", root.results.length); } - pointSize: Style.fontSizeXS - color: Color.mOnSurfaceVariant - horizontalAlignment: Text.AlignCenter - } } - } } diff --git a/Modules/Panels/Launcher/LauncherOverlayWindow.qml b/Modules/Panels/Launcher/LauncherOverlayWindow.qml index 7aa2bc005..29ebf4638 100644 --- a/Modules/Panels/Launcher/LauncherOverlayWindow.qml +++ b/Modules/Panels/Launcher/LauncherOverlayWindow.qml @@ -11,385 +11,386 @@ import qs.Widgets // Standalone launcher window for Overlay layer mode. // This window appears above fullscreen windows and does not attach to the bar. Variants { - id: launcherVariants + id: launcherVariants - model: Quickshell.screens.filter(screen => Settings.data.appLauncher.overviewLayer) + model: Quickshell.screens.filter(screen => Settings.data.appLauncher.overviewLayer) - delegate: Loader { - id: windowLoader + delegate: Loader { + id: windowLoader - required property ShellScreen modelData + required property ShellScreen modelData - active: PanelService.overlayLauncherOpen && PanelService.overlayLauncherScreen === modelData + active: PanelService.overlayLauncherOpen && PanelService.overlayLauncherScreen === modelData - sourceComponent: PanelWindow { - id: launcherWindow - screen: windowLoader.modelData + sourceComponent: PanelWindow { + id: launcherWindow + screen: windowLoader.modelData - anchors { - top: true - bottom: true - left: true - right: true - } - - color: "transparent" - - WlrLayershell.namespace: "noctalia-launcher-overlay-" + (screen?.name || "unknown") - WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive - WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.exclusionMode: ExclusionMode.Ignore - - // Positioning logic (respects settings but doesn't attach to bar) - readonly property string barPosition: Settings.data.bar.position - readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" - readonly property int barThickness: Math.round(Style.barHeight + Style.marginL) - - readonly property string panelPosition: { - var pos = Settings.data.appLauncher.position; - if (pos === "follow_bar") { - if (barIsVertical) { - return "center_" + barPosition; - } else { - return barPosition + "_center"; - } - } - return pos; - } - - // Preview panel support - readonly property int listPanelWidth: Math.round(500 * Style.uiScaleRatio) - readonly property int previewPanelWidth: Math.round(400 * Style.uiScaleRatio) - readonly property bool previewActive: { - if (!launcherCore) - return false; - var provider = launcherCore.activeProvider; - if (!provider || !provider.hasPreview) - return false; - if (!Settings.data.appLauncher.enableClipPreview) - return false; - return launcherCore.selectedIndex >= 0 && launcherCore.results && !!launcherCore.results[launcherCore.selectedIndex]; - } - - // Dimmer background (click to close) - Rectangle { - anchors.fill: parent - color: Qt.alpha(Color.mSurface, Settings.data.general.dimmerOpacity) - - MouseArea { - anchors.fill: parent - onClicked: PanelService.closeOverlayLauncher() - } - } - - // Shadow for launcher panel - NDropShadow { - source: launcherPanel - anchors.fill: launcherPanel - autoPaddingEnabled: true - } - - // Launcher panel with position-based anchoring - Item { - id: launcherPanel - width: Math.round(Math.max(parent.width * 0.25, launcherWindow.listPanelWidth + Style.margin2L * 2)) - height: Math.round(Math.max(parent.height * 0.5, 600 * Style.uiScaleRatio)) - clip: false - - // Entrance animation - opacity: 0 - transformOrigin: { - if (touchingTop && touchingLeft) - return Item.TopLeft; - if (touchingTop && touchingRight) - return Item.TopRight; - if (touchingBottom && touchingLeft) - return Item.BottomLeft; - if (touchingBottom && touchingRight) - return Item.BottomRight; - if (touchingTop) - return Item.Top; - if (touchingBottom) - return Item.Bottom; - if (touchingLeft) - return Item.Left; - if (touchingRight) - return Item.Right; - return Item.Center; - } - - Component.onCompleted: { - opacity = 1; - } - - Behavior on opacity { - NumberAnimation { - duration: Style.animationNormal - easing.type: Easing.OutCubic - } - } - - // Horizontal positioning - anchors.horizontalCenter: (panelPosition === "center" || panelPosition.endsWith("_center")) ? parent.horizontalCenter : undefined - anchors.left: panelPosition.endsWith("_left") ? parent.left : undefined - anchors.right: panelPosition.endsWith("_right") ? parent.right : undefined - - // Vertical positioning - anchors.verticalCenter: (panelPosition === "center" || panelPosition.startsWith("center_")) ? parent.verticalCenter : undefined - anchors.top: panelPosition.startsWith("top_") ? parent.top : undefined - anchors.bottom: panelPosition.startsWith("bottom_") ? parent.bottom : undefined - - // Margins - only add bar clearance on the bar's edge - anchors.leftMargin: barPosition === "left" ? barThickness : 0 - anchors.rightMargin: barPosition === "right" ? barThickness : 0 - anchors.topMargin: barPosition === "top" ? barThickness : 0 - anchors.bottomMargin: barPosition === "bottom" ? barThickness : 0 - - // Edge detection - based on position setting and bar location - readonly property bool touchingLeft: panelPosition.endsWith("_left") && barPosition !== "left" - readonly property bool touchingRight: panelPosition.endsWith("_right") && barPosition !== "right" - readonly property bool touchingTop: panelPosition.startsWith("top_") && barPosition !== "top" - readonly property bool touchingBottom: panelPosition.startsWith("bottom_") && barPosition !== "bottom" - - // Corner states based on edge touching - // State 0: Normal rounded, State 1: Horizontal inversion, State 2: Vertical inversion - readonly property int topLeftCornerState: { - if (touchingLeft && touchingTop) - return 0; - if (touchingLeft) - return 2; - if (touchingTop) - return 1; - return 0; - } - readonly property int topRightCornerState: { - if (touchingRight && touchingTop) - return 0; - if (touchingRight) - return 2; - if (touchingTop) - return 1; - return 0; - } - readonly property int bottomLeftCornerState: { - if (touchingLeft && touchingBottom) - return 0; - if (touchingLeft) - return 2; - if (touchingBottom) - return 1; - return 0; - } - readonly property int bottomRightCornerState: { - if (touchingRight && touchingBottom) - return 0; - if (touchingRight) - return 2; - if (touchingBottom) - return 1; - return 0; - } - - // Background with inverted corners - extends beyond panel for inverted corners - Shape { - id: panelShape - // Extend shape to allow inverted corners to render outside panel bounds - x: -radius - y: -radius - width: launcherPanel.width + radius * 2 - height: launcherPanel.height + radius * 2 - opacity: launcherPanel.opacity - layer.enabled: true - - readonly property real radius: Style.radiusL - - // Panel dimensions (for path calculations) - readonly property real panelW: launcherPanel.width - readonly property real panelH: launcherPanel.height - - // Helper functions for corner rendering - function getMultX(state) { - return state === 1 ? -1 : 1; - } - function getMultY(state) { - return state === 2 ? -1 : 1; - } - function getArcDir(multX, multY) { - return ((multX < 0) !== (multY < 0)) ? PathArc.Counterclockwise : PathArc.Clockwise; - } - - readonly property real tlMultX: getMultX(launcherPanel.topLeftCornerState) - readonly property real tlMultY: getMultY(launcherPanel.topLeftCornerState) - readonly property real trMultX: getMultX(launcherPanel.topRightCornerState) - readonly property real trMultY: getMultY(launcherPanel.topRightCornerState) - readonly property real blMultX: getMultX(launcherPanel.bottomLeftCornerState) - readonly property real blMultY: getMultY(launcherPanel.bottomLeftCornerState) - readonly property real brMultX: getMultX(launcherPanel.bottomRightCornerState) - readonly property real brMultY: getMultY(launcherPanel.bottomRightCornerState) - - ShapePath { - strokeWidth: -1 - fillColor: Color.mSurfaceVariant - - // Offset by radius to account for Shape's extended bounds - startX: panelShape.radius + panelShape.radius * panelShape.tlMultX - startY: panelShape.radius - - // Top edge - PathLine { - relativeX: panelShape.panelW - panelShape.radius * panelShape.tlMultX - panelShape.radius * panelShape.trMultX - relativeY: 0 - } - // Top-right corner - PathArc { - relativeX: panelShape.radius * panelShape.trMultX - relativeY: panelShape.radius * panelShape.trMultY - radiusX: panelShape.radius - radiusY: panelShape.radius - direction: panelShape.getArcDir(panelShape.trMultX, panelShape.trMultY) - } - // Right edge - PathLine { - relativeX: 0 - relativeY: panelShape.panelH - panelShape.radius * panelShape.trMultY - panelShape.radius * panelShape.brMultY - } - // Bottom-right corner - PathArc { - relativeX: -panelShape.radius * panelShape.brMultX - relativeY: panelShape.radius * panelShape.brMultY - radiusX: panelShape.radius - radiusY: panelShape.radius - direction: panelShape.getArcDir(panelShape.brMultX, panelShape.brMultY) - } - // Bottom edge - PathLine { - relativeX: -(panelShape.panelW - panelShape.radius * panelShape.brMultX - panelShape.radius * panelShape.blMultX) - relativeY: 0 - } - // Bottom-left corner - PathArc { - relativeX: -panelShape.radius * panelShape.blMultX - relativeY: -panelShape.radius * panelShape.blMultY - radiusX: panelShape.radius - radiusY: panelShape.radius - direction: panelShape.getArcDir(panelShape.blMultX, panelShape.blMultY) - } - // Left edge - PathLine { - relativeX: 0 - relativeY: -(panelShape.panelH - panelShape.radius * panelShape.blMultY - panelShape.radius * panelShape.tlMultY) - } - // Top-left corner - PathArc { - relativeX: panelShape.radius * panelShape.tlMultX - relativeY: -panelShape.radius * panelShape.tlMultY - radiusX: panelShape.radius - radiusY: panelShape.radius - direction: panelShape.getArcDir(panelShape.tlMultX, panelShape.tlMultY) - } - } - } - - // Border - Rectangle { - anchors.fill: parent - color: "transparent" - radius: Style.radiusL - border.color: Style.boxBorderColor - border.width: Style.borderS - visible: !launcherPanel.touchingLeft && !launcherPanel.touchingRight && !launcherPanel.touchingTop && !launcherPanel.touchingBottom - } - - LauncherCore { - id: launcherCore - anchors.fill: parent - screen: windowLoader.modelData - isOpen: true - onRequestClose: PanelService.closeOverlayLauncher() - onRequestCloseImmediately: PanelService.closeOverlayLauncherImmediately() - - Component.onCompleted: PanelService.overlayLauncherCore = launcherCore - Component.onDestruction: PanelService.overlayLauncherCore = null - } - - // Preview Panel - clipboard preview positioned outside panel bounds - NDropShadow { - source: previewBox - anchors.fill: previewBox - autoPaddingEnabled: true - visible: previewBox.visible - } - - NBox { - id: previewBox - visible: launcherWindow.previewActive - width: launcherWindow.previewPanelWidth - height: Math.round(400 * Style.uiScaleRatio) - x: panelPosition.endsWith("_right") ? -(launcherWindow.previewPanelWidth + Style.marginM) : launcherPanel.width + Style.marginM - y: { - var view = launcherCore.resultsView; - if (!view) - return Style.marginL; - var row = launcherCore.isGridView ? Math.floor(launcherCore.selectedIndex / launcherCore.gridColumns) : launcherCore.selectedIndex; - var gridCellSize = Math.floor((launcherWindow.listPanelWidth - (2 * Style.marginXS) - ((launcherCore.targetGridColumns - 1) * Style.marginS)) / launcherCore.targetGridColumns); - var itemHeight = launcherCore.isGridView ? (gridCellSize + Style.marginXXS) : (launcherCore.entryHeight + (view.spacing || 0)); - var yPos = row * itemHeight - (view.contentY || 0); - var mapped = view.mapToItem(launcherPanel, 0, yPos); - return Math.max(Style.marginL, Math.min(mapped.y, launcherPanel.height - previewBox.height - Style.marginL)); - } - z: -1 - - opacity: visible ? 1.0 : 0.0 - Behavior on opacity { - NumberAnimation { - duration: Style.animationFast - } - } - Behavior on y { - NumberAnimation { - duration: Style.animationFast - easing.type: Easing.OutCubic - } - } - - Loader { - id: previewLoader - anchors.fill: parent - active: launcherWindow.previewActive - source: { - if (!active) - return ""; - var provider = launcherCore.activeProvider; - if (provider && provider.previewComponentPath) - return provider.previewComponentPath; - return ""; + anchors { + top: true + bottom: true + left: true + right: true } - onLoaded: updatePreviewItem() - onItemChanged: updatePreviewItem() + color: "transparent" - function updatePreviewItem() { - if (!item || launcherCore.selectedIndex < 0 || !launcherCore.results[launcherCore.selectedIndex]) - return; - var provider = launcherCore.activeProvider; - if (provider && provider.getPreviewData) { - item.currentItem = provider.getPreviewData(launcherCore.results[launcherCore.selectedIndex]); - } else { - item.currentItem = launcherCore.results[launcherCore.selectedIndex]; - } + WlrLayershell.namespace: "noctalia-launcher-overlay-" + (screen?.name || "unknown") + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.exclusionMode: ExclusionMode.Ignore + + // Positioning logic (respects settings but doesn't attach to bar) + readonly property string barPosition: Settings.data.bar.position + readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" + readonly property int barThickness: Math.round(Style.barHeight + Style.marginL) + + readonly property string panelPosition: { + var pos = Settings.data.appLauncher.position; + if (pos === "follow_bar") { + if (barIsVertical) { + return "center_" + barPosition; + } else { + return barPosition + "_center"; + } + } + return pos; } - } - } - // Update preview when selection changes - Connections { - target: launcherCore - function onSelectedIndexChanged() { - if (previewLoader.item) - previewLoader.updatePreviewItem(); - } + // Preview panel support + readonly property int listPanelWidth: Math.round(500 * Style.uiScaleRatio) + readonly property int previewPanelWidth: Math.round(400 * Style.uiScaleRatio) + readonly property bool previewActive: { + if (!launcherCore) + return false; + var provider = launcherCore.activeProvider; + if (!provider || !provider.hasPreview) + return false; + if (!Settings.data.appLauncher.enableClipPreview) + return false; + var item = launcherCore.results[launcherCore.selectedIndex]; + return launcherCore.selectedIndex >= 0 && launcherCore.results && !!item && !item.isHeader; + } + + // Dimmer background (click to close) + Rectangle { + anchors.fill: parent + color: Qt.alpha(Color.mSurface, Settings.data.general.dimmerOpacity) + + MouseArea { + anchors.fill: parent + onClicked: PanelService.closeOverlayLauncher() + } + } + + // Shadow for launcher panel + NDropShadow { + source: launcherPanel + anchors.fill: launcherPanel + autoPaddingEnabled: true + } + + // Launcher panel with position-based anchoring + Item { + id: launcherPanel + width: Math.round(Math.max(parent.width * 0.25, launcherWindow.listPanelWidth + Style.margin2L * 2)) + height: Math.round(Math.max(parent.height * 0.5, 600 * Style.uiScaleRatio)) + clip: false + + // Entrance animation + opacity: 0 + transformOrigin: { + if (touchingTop && touchingLeft) + return Item.TopLeft; + if (touchingTop && touchingRight) + return Item.TopRight; + if (touchingBottom && touchingLeft) + return Item.BottomLeft; + if (touchingBottom && touchingRight) + return Item.BottomRight; + if (touchingTop) + return Item.Top; + if (touchingBottom) + return Item.Bottom; + if (touchingLeft) + return Item.Left; + if (touchingRight) + return Item.Right; + return Item.Center; + } + + Component.onCompleted: { + opacity = 1; + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + + // Horizontal positioning + anchors.horizontalCenter: (panelPosition === "center" || panelPosition.endsWith("_center")) ? parent.horizontalCenter : undefined + anchors.left: panelPosition.endsWith("_left") ? parent.left : undefined + anchors.right: panelPosition.endsWith("_right") ? parent.right : undefined + + // Vertical positioning + anchors.verticalCenter: (panelPosition === "center" || panelPosition.startsWith("center_")) ? parent.verticalCenter : undefined + anchors.top: panelPosition.startsWith("top_") ? parent.top : undefined + anchors.bottom: panelPosition.startsWith("bottom_") ? parent.bottom : undefined + + // Margins - only add bar clearance on the bar's edge + anchors.leftMargin: barPosition === "left" ? barThickness : 0 + anchors.rightMargin: barPosition === "right" ? barThickness : 0 + anchors.topMargin: barPosition === "top" ? barThickness : 0 + anchors.bottomMargin: barPosition === "bottom" ? barThickness : 0 + + // Edge detection - based on position setting and bar location + readonly property bool touchingLeft: panelPosition.endsWith("_left") && barPosition !== "left" + readonly property bool touchingRight: panelPosition.endsWith("_right") && barPosition !== "right" + readonly property bool touchingTop: panelPosition.startsWith("top_") && barPosition !== "top" + readonly property bool touchingBottom: panelPosition.startsWith("bottom_") && barPosition !== "bottom" + + // Corner states based on edge touching + // State 0: Normal rounded, State 1: Horizontal inversion, State 2: Vertical inversion + readonly property int topLeftCornerState: { + if (touchingLeft && touchingTop) + return 0; + if (touchingLeft) + return 2; + if (touchingTop) + return 1; + return 0; + } + readonly property int topRightCornerState: { + if (touchingRight && touchingTop) + return 0; + if (touchingRight) + return 2; + if (touchingTop) + return 1; + return 0; + } + readonly property int bottomLeftCornerState: { + if (touchingLeft && touchingBottom) + return 0; + if (touchingLeft) + return 2; + if (touchingBottom) + return 1; + return 0; + } + readonly property int bottomRightCornerState: { + if (touchingRight && touchingBottom) + return 0; + if (touchingRight) + return 2; + if (touchingBottom) + return 1; + return 0; + } + + // Background with inverted corners - extends beyond panel for inverted corners + Shape { + id: panelShape + // Extend shape to allow inverted corners to render outside panel bounds + x: -radius + y: -radius + width: launcherPanel.width + radius * 2 + height: launcherPanel.height + radius * 2 + opacity: launcherPanel.opacity + layer.enabled: true + + readonly property real radius: Style.radiusL + + // Panel dimensions (for path calculations) + readonly property real panelW: launcherPanel.width + readonly property real panelH: launcherPanel.height + + // Helper functions for corner rendering + function getMultX(state) { + return state === 1 ? -1 : 1; + } + function getMultY(state) { + return state === 2 ? -1 : 1; + } + function getArcDir(multX, multY) { + return ((multX < 0) !== (multY < 0)) ? PathArc.Counterclockwise : PathArc.Clockwise; + } + + readonly property real tlMultX: getMultX(launcherPanel.topLeftCornerState) + readonly property real tlMultY: getMultY(launcherPanel.topLeftCornerState) + readonly property real trMultX: getMultX(launcherPanel.topRightCornerState) + readonly property real trMultY: getMultY(launcherPanel.topRightCornerState) + readonly property real blMultX: getMultX(launcherPanel.bottomLeftCornerState) + readonly property real blMultY: getMultY(launcherPanel.bottomLeftCornerState) + readonly property real brMultX: getMultX(launcherPanel.bottomRightCornerState) + readonly property real brMultY: getMultY(launcherPanel.bottomRightCornerState) + + ShapePath { + strokeWidth: -1 + fillColor: Color.mSurfaceVariant + + // Offset by radius to account for Shape's extended bounds + startX: panelShape.radius + panelShape.radius * panelShape.tlMultX + startY: panelShape.radius + + // Top edge + PathLine { + relativeX: panelShape.panelW - panelShape.radius * panelShape.tlMultX - panelShape.radius * panelShape.trMultX + relativeY: 0 + } + // Top-right corner + PathArc { + relativeX: panelShape.radius * panelShape.trMultX + relativeY: panelShape.radius * panelShape.trMultY + radiusX: panelShape.radius + radiusY: panelShape.radius + direction: panelShape.getArcDir(panelShape.trMultX, panelShape.trMultY) + } + // Right edge + PathLine { + relativeX: 0 + relativeY: panelShape.panelH - panelShape.radius * panelShape.trMultY - panelShape.radius * panelShape.brMultY + } + // Bottom-right corner + PathArc { + relativeX: -panelShape.radius * panelShape.brMultX + relativeY: panelShape.radius * panelShape.brMultY + radiusX: panelShape.radius + radiusY: panelShape.radius + direction: panelShape.getArcDir(panelShape.brMultX, panelShape.brMultY) + } + // Bottom edge + PathLine { + relativeX: -(panelShape.panelW - panelShape.radius * panelShape.brMultX - panelShape.radius * panelShape.blMultX) + relativeY: 0 + } + // Bottom-left corner + PathArc { + relativeX: -panelShape.radius * panelShape.blMultX + relativeY: -panelShape.radius * panelShape.blMultY + radiusX: panelShape.radius + radiusY: panelShape.radius + direction: panelShape.getArcDir(panelShape.blMultX, panelShape.blMultY) + } + // Left edge + PathLine { + relativeX: 0 + relativeY: -(panelShape.panelH - panelShape.radius * panelShape.blMultY - panelShape.radius * panelShape.tlMultY) + } + // Top-left corner + PathArc { + relativeX: panelShape.radius * panelShape.tlMultX + relativeY: -panelShape.radius * panelShape.tlMultY + radiusX: panelShape.radius + radiusY: panelShape.radius + direction: panelShape.getArcDir(panelShape.tlMultX, panelShape.tlMultY) + } + } + } + + // Border + Rectangle { + anchors.fill: parent + color: "transparent" + radius: Style.radiusL + border.color: Style.boxBorderColor + border.width: Style.borderS + visible: !launcherPanel.touchingLeft && !launcherPanel.touchingRight && !launcherPanel.touchingTop && !launcherPanel.touchingBottom + } + + LauncherCore { + id: launcherCore + anchors.fill: parent + screen: windowLoader.modelData + isOpen: true + onRequestClose: PanelService.closeOverlayLauncher() + onRequestCloseImmediately: PanelService.closeOverlayLauncherImmediately() + + Component.onCompleted: PanelService.overlayLauncherCore = launcherCore + Component.onDestruction: PanelService.overlayLauncherCore = null + } + + // Preview Panel - clipboard preview positioned outside panel bounds + NDropShadow { + source: previewBox + anchors.fill: previewBox + autoPaddingEnabled: true + visible: previewBox.visible + } + + NBox { + id: previewBox + visible: launcherWindow.previewActive + width: launcherWindow.previewPanelWidth + height: Math.round(400 * Style.uiScaleRatio) + x: panelPosition.endsWith("_right") ? -(launcherWindow.previewPanelWidth + Style.marginM) : launcherPanel.width + Style.marginM + y: { + var view = launcherCore.resultsView; + if (!view) + return Style.marginL; + var row = launcherCore.isGridView ? Math.floor(launcherCore.selectedIndex / launcherCore.gridColumns) : launcherCore.selectedIndex; + var gridCellSize = Math.floor((launcherWindow.listPanelWidth - (2 * Style.marginXS) - ((launcherCore.targetGridColumns - 1) * Style.marginS)) / launcherCore.targetGridColumns); + var itemHeight = launcherCore.isGridView ? (gridCellSize + Style.marginXXS) : (launcherCore.entryHeight + (view.spacing || 0)); + var yPos = row * itemHeight - (view.contentY || 0); + var mapped = view.mapToItem(launcherPanel, 0, yPos); + return Math.max(Style.marginL, Math.min(mapped.y, launcherPanel.height - previewBox.height - Style.marginL)); + } + z: -1 + + opacity: visible ? 1.0 : 0.0 + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + Behavior on y { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + + Loader { + id: previewLoader + anchors.fill: parent + active: launcherWindow.previewActive + source: { + if (!active) + return ""; + var provider = launcherCore.activeProvider; + if (provider && provider.previewComponentPath) + return provider.previewComponentPath; + return ""; + } + + onLoaded: updatePreviewItem() + onItemChanged: updatePreviewItem() + + function updatePreviewItem() { + if (!item || launcherCore.selectedIndex < 0 || !launcherCore.results[launcherCore.selectedIndex]) + return; + var provider = launcherCore.activeProvider; + if (provider && provider.getPreviewData) { + item.currentItem = provider.getPreviewData(launcherCore.results[launcherCore.selectedIndex]); + } else { + item.currentItem = launcherCore.results[launcherCore.selectedIndex]; + } + } + } + } + + // Update preview when selection changes + Connections { + target: launcherCore + function onSelectedIndexChanged() { + if (previewLoader.item) + previewLoader.updatePreviewItem(); + } + } + } } - } } - } } diff --git a/Modules/Panels/Launcher/Providers/ClipboardProvider.qml b/Modules/Panels/Launcher/Providers/ClipboardProvider.qml index 2f746a2f8..3d1c31ea9 100644 --- a/Modules/Panels/Launcher/Providers/ClipboardProvider.qml +++ b/Modules/Panels/Launcher/Providers/ClipboardProvider.qml @@ -2,389 +2,527 @@ import QtQuick import Quickshell import qs.Commons import qs.Services.Keyboard +import qs.Services.Noctalia Item { - id: root + id: root - // Provider metadata - property string name: I18n.tr("launcher.providers.clipboard") - property var launcher: null - property string iconMode: Settings.data.appLauncher.iconMode - property string supportedLayouts: "list" // List view for clipboard content - property bool wrapNavigation: false // Don't wrap at end of list + // Provider metadata + property string name: I18n.tr("launcher.providers.clipboard") + property var launcher: null + property string iconMode: Settings.data.appLauncher.iconMode + property string supportedLayouts: "list" // List view for clipboard content + property bool wrapNavigation: false // Don't wrap at end of list - // Provider capabilities - property bool handleSearch: false // Don't handle regular search + // Provider capabilities + property bool handleSearch: false // Don't handle regular search - // Preview support - property bool hasPreview: Settings.data.appLauncher.enableClipPreview - property string previewComponentPath: "./ClipboardPreview.qml" + // Preview support + property bool hasPreview: Settings.data.appLauncher.enableClipPreview + property string previewComponentPath: "./ClipboardPreview.qml" - // Image handling - expose revision for reactive updates in delegates - readonly property int imageRevision: ClipboardService.revision + // Image handling - expose revision for reactive updates in delegates + readonly property int imageRevision: ClipboardService.revision - // Internal state - property bool isWaitingForData: false - property bool gotResults: false - property string lastSearchText: "" + // Categories + property var availableCategories: Settings.data.appLauncher.enableClipboardChips ? ["All", "Images", "Links", "Files", "Code", "Colors"] : [] + property bool showsCategories: Settings.data.appLauncher.enableClipboardChips + property string selectedCategory: "All" - // Listen for clipboard data updates - Connections { - target: ClipboardService - function onListCompleted() { - if (gotResults && (lastSearchText === searchText)) { - // Do not update results after the first fetch. - // This will avoid the list resetting every 2seconds when the service updates. - return; - } - // Refresh results if we're waiting for data or if clipboard plugin is active - if (isWaitingForData || (launcher && launcher.searchText.startsWith(">clip"))) { - isWaitingForData = false; - gotResults = true; - if (launcher) { - launcher.updateResults(); + function selectCategory(cat) { + if (selectedCategory !== cat) { + selectedCategory = cat; + if (launcher) { + launcher.updateResults(); + } } - } } - function onActiveChanged() { - // When active state changes (e.g. dependency check completes), refresh results - if (ClipboardService.active && launcher && launcher.searchText.startsWith(">clip")) { + + // Date Filtering + property bool hasDateFilter: true + property string dateFilter: "all" + property var availableDateFilters: [ + { + "label": "All Time", + "action": "all", + "icon": iconMode === "tabler" ? "calendar" : "x-office-calendar" + }, + { + "label": "Today", + "action": "today", + "icon": iconMode === "tabler" ? "calendar-event" : "view-calendar-timeline" + }, + { + "label": "Yesterday", + "action": "yesterday", + "icon": iconMode === "tabler" ? "calendar-time" : "view-calendar" + }, + { + "label": "Previous 7 Days", + "action": "week", + "icon": iconMode === "tabler" ? "calendar-week" : "view-calendar-week" + } + ] + + function selectDateFilter(filter) { + if (dateFilter !== filter) { + dateFilter = filter; + if (launcher) { + launcher.updateResults(); + } + } + } + + property var categoryIcons: { + "All": iconMode === "tabler" ? "border-all" : "view-grid", + "Images": iconMode === "tabler" ? "photo" : "image", + "Links": iconMode === "tabler" ? "link" : "insert-link", + "Files": iconMode === "tabler" ? "file" : "text-x-generic", + "Code": iconMode === "tabler" ? "code" : "text-x-script", + "Colors": iconMode === "tabler" ? "palette" : "color-picker" + } + + // Internal state + property bool isWaitingForData: false + property bool gotResults: false + property string lastSearchText: "" + + // Listen for clipboard data updates + Connections { + target: ClipboardService + function onListCompleted() { + if (gotResults && (lastSearchText === searchText)) { + // Do not update results after the first fetch. + // This will avoid the list resetting every 2seconds when the service updates. + return; + } + // Refresh results if we're waiting for data or if clipboard plugin is active + if (isWaitingForData || (launcher && launcher.searchText.startsWith(">clip"))) { + isWaitingForData = false; + gotResults = true; + if (launcher) { + launcher.updateResults(); + } + } + } + function onActiveChanged() { + // When active state changes (e.g. dependency check completes), refresh results + if (ClipboardService.active && launcher && launcher.searchText.startsWith(">clip")) { + isWaitingForData = true; + gotResults = false; + ClipboardService.list(100); + } + } + } + + // Initialize provider + function init() { + Logger.d("ClipboardProvider", "Initialized"); + // Pre-load clipboard data if service is active + if (ClipboardService.active) { + ClipboardService.list(100); + } + } + + // Called when launcher opens + function onOpened() { isWaitingForData = true; gotResults = false; - ClipboardService.list(100); - } - } - } + lastSearchText = ""; - // Initialize provider - function init() { - Logger.d("ClipboardProvider", "Initialized"); - // Pre-load clipboard data if service is active - if (ClipboardService.active) { - ClipboardService.list(100); - } - } - - // Called when launcher opens - function onOpened() { - isWaitingForData = true; - gotResults = false; - lastSearchText = ""; - - // Refresh clipboard history when launcher opens - if (ClipboardService.active) { - ClipboardService.list(100); - } - } - - // Check if this provider handles the command - function handleCommand(searchText) { - return searchText.startsWith(">clip"); - } - - // Return available commands when user types ">" - function commands() { - return [ - { - "name": ">clip", - "description": I18n.tr("launcher.providers.clipboard-search-description"), - "icon": iconMode === "tabler" ? "clipboard" : "diodon", - "isTablerIcon": true, - "isImage": false, - "onActivate": function () { - launcher.setSearchText(">clip "); - } - }, - { - "name": ">clip clear", - "description": I18n.tr("launcher.providers.clipboard-clear-description"), - "icon": iconMode === "tabler" ? "trash" : "user-trash", - "isTablerIcon": true, - "isImage": false, - "onActivate": function () { - ClipboardService.wipeAll(); - launcher.close(); - } - } - ]; - } - - // Get search results - function getResults(searchText) { - if (!searchText.startsWith(">clip")) { - return []; + // Refresh clipboard history when launcher opens + if (ClipboardService.active) { + ClipboardService.list(100); + } } - lastSearchText = searchText; - const results = []; - const query = searchText.slice(5).trim(); + // Check if this provider handles the command + function handleCommand(searchText) { + return searchText.startsWith(">clip"); + } - // Check if clipboard service is not active - if (!ClipboardService.active) { - // If dependency check hasn't completed yet, show loading instead of disabled - if (!ClipboardService.dependencyChecked) { + // Return available commands when user types ">" + function commands() { return [ - { - "name": I18n.tr("launcher.providers.clipboard-loading"), - "description": I18n.tr("launcher.providers.emoji-loading-description"), - "icon": iconMode === "tabler" ? "refresh" : "view-refresh", + { + "name": ">clip", + "description": I18n.tr("launcher.providers.clipboard-search-description"), + "icon": iconMode === "tabler" ? "clipboard" : "diodon", "isTablerIcon": true, "isImage": false, - "onActivate": function () {} - } - ]; - } - return [ + "onActivate": function () { + launcher.setSearchText(">clip "); + } + }, { - "name": I18n.tr("launcher.providers.clipboard-history-disabled"), - "description": I18n.tr("launcher.providers.clipboard-history-disabled-description"), - "icon": iconMode === "tabler" ? "refresh" : "view-refresh", - "isTablerIcon": true, - "isImage": false, - "onActivate": function () {} + "name": ">clip clear", + "description": I18n.tr("launcher.providers.clipboard-clear-description"), + "icon": iconMode === "tabler" ? "trash" : "user-trash", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () { + ClipboardService.wipeAll(); + launcher.close(); + } } - ]; + ]; } - // Special command: clear - if (query === "clear") { - return [ - { - "name": I18n.tr("launcher.providers.clipboard-clear-history"), - "description": I18n.tr("launcher.providers.clipboard-clear-description-full"), - "icon": iconMode === "tabler" ? "trash" : "user-trash", - "isTablerIcon": true, - "isImage": false, - "onActivate": function () { - ClipboardService.wipeAll(); - launcher.close(); - } - } - ]; - } - - // Show loading state if data is being loaded - if (ClipboardService.loading || isWaitingForData) { - return [ - { - "name": I18n.tr("launcher.providers.clipboard-loading"), - "description": I18n.tr("launcher.providers.emoji-loading-description"), - "icon": iconMode === "tabler" ? "refresh" : "view-refresh", - "isTablerIcon": true, - "isImage": false, - "onActivate": function () {} - } - ]; - } - - // Get clipboard items - const items = ClipboardService.items || []; - - // If no items and we haven't tried loading yet, trigger a load - if (items.count === 0 && !ClipboardService.loading) { - isWaitingForData = true; - ClipboardService.list(100); - return [ - { - "name": I18n.tr("launcher.providers.clipboard-loading"), - "description": I18n.tr("launcher.providers.emoji-loading-description"), - "icon": iconMode === "tabler" ? "refresh" : "view-refresh", - "isTablerIcon": true, - "isImage": false, - "onActivate": function () {} - } - ]; - } - - // Search clipboard items - const searchTerm = query.toLowerCase(); - - // Filter and format results - items.forEach(function (item) { - const preview = (item.preview || "").toLowerCase(); - - // Skip if search term doesn't match - if (searchTerm && preview.indexOf(searchTerm) === -1) { - return; - } - - // Format the result based on type - let entry; - if (item.isImage) { - entry = formatImageEntry(item); - } else { - entry = formatTextEntry(item); - } - - // Add activation handler - entry.onActivate = function () { - if (Settings.data.appLauncher.autoPasteClipboard) { - launcher.closeImmediately(); - Qt.callLater(() => { - ClipboardService.pasteFromClipboard(item.id, item.mime); - }); - } else { - ClipboardService.copyToClipboard(item.id); - launcher.close(); + // Get search results + function getResults(searchText) { + if (!searchText.startsWith(">clip")) { + return []; } - }; - results.push(entry); - }); + lastSearchText = searchText; + const results = []; + const query = searchText.slice(5).trim(); - // Show empty state if no results - if (results.length === 0) { - results.push({ - "name": searchTerm ? "No matching clipboard items" : "Clipboard is empty", - "description": searchTerm ? `No items containing "${query}"` : "Copy something to see it here", - "icon": iconMode === "tabler" ? "clipboard" : "text-x-generic", - "isTablerIcon": true, - "isImage": false, - "onActivate": function () {// Do nothing - } - }); + // Check if clipboard service is not active + if (!ClipboardService.active) { + // If dependency check hasn't completed yet, show loading instead of disabled + if (!ClipboardService.dependencyChecked) { + return [ + { + "name": I18n.tr("launcher.providers.clipboard-loading"), + "description": I18n.tr("launcher.providers.emoji-loading-description"), + "icon": iconMode === "tabler" ? "refresh" : "view-refresh", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () {} + } + ]; + } + return [ + { + "name": I18n.tr("launcher.providers.clipboard-history-disabled"), + "description": I18n.tr("launcher.providers.clipboard-history-disabled-description"), + "icon": iconMode === "tabler" ? "refresh" : "view-refresh", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () {} + } + ]; + } + + // Special command: clear + if (query === "clear") { + return [ + { + "name": I18n.tr("launcher.providers.clipboard-clear-history"), + "description": I18n.tr("launcher.providers.clipboard-clear-description-full"), + "icon": iconMode === "tabler" ? "trash" : "user-trash", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () { + ClipboardService.wipeAll(); + launcher.close(); + } + } + ]; + } + + // Show loading state if data is being loaded + if (ClipboardService.loading || isWaitingForData) { + return [ + { + "name": I18n.tr("launcher.providers.clipboard-loading"), + "description": I18n.tr("launcher.providers.emoji-loading-description"), + "icon": iconMode === "tabler" ? "refresh" : "view-refresh", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () {} + } + ]; + } + + // Get clipboard items + const items = ClipboardService.items || []; + + // If no items and we haven't tried loading yet, trigger a load + if (items.count === 0 && !ClipboardService.loading) { + isWaitingForData = true; + ClipboardService.list(100); + return [ + { + "name": I18n.tr("launcher.providers.clipboard-loading"), + "description": I18n.tr("launcher.providers.emoji-loading-description"), + "icon": iconMode === "tabler" ? "refresh" : "view-refresh", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () {} + } + ]; + } + + // Search clipboard items + const searchTerm = query.toLowerCase(); + + // Date grouping trackers + const headersEnabled = Settings.data.appLauncher.enableClipboardDateHeaders; + const now = Date.now() / 1000; + const todayStart = new Date(); + todayStart.setHours(0, 0, 0, 0); + const todayStartTs = todayStart.getTime() / 1000; + const yesterdayStartTs = todayStartTs - 86400; + + let currentGroup = ""; + + // Filter and format results + items.forEach(function (item) { + // Category filter + if (Settings.data.appLauncher.enableClipboardChips && root.selectedCategory !== "All") { + const catMap = { + "Images": "image", + "Links": "link", + "Files": "file", + "Code": "code", + "Colors": "color" + }; + if (item.contentType !== catMap[root.selectedCategory]) { + return; + } + } + + const preview = (item.preview || "").toLowerCase(); + + // Skip if search term doesn't match + if (searchTerm && preview.indexOf(searchTerm) === -1) { + return; + } + + // Date Filter + const firstSeen = ClipboardService.firstSeenById[item.id] || now; + if (root.dateFilter !== "all") { + if (root.dateFilter === "today" && firstSeen < todayStartTs) + return; + if (root.dateFilter === "yesterday" && (firstSeen >= todayStartTs || firstSeen < yesterdayStartTs)) + return; + if (root.dateFilter === "week" && (firstSeen >= yesterdayStartTs || firstSeen < (todayStartTs - (86400 * 7)))) + return; + } + + // Check date group logic + if (headersEnabled && !searchTerm && root.selectedCategory === "All" && root.dateFilter === "all") { + let groupName = "Older"; + if (firstSeen >= todayStartTs) { + groupName = "Today"; + } else if (firstSeen >= yesterdayStartTs) { + groupName = "Yesterday"; + } else if (firstSeen >= todayStartTs - (86400 * 7)) { + groupName = "Previous 7 Days"; + } + + if (groupName !== currentGroup) { + currentGroup = groupName; + results.push({ + "name": currentGroup, + "description": "", + "icon": iconMode === "tabler" ? "calendar" : "x-office-calendar", + "isTablerIcon": true, + "isImage": false, + "hideIcon": true, + "isHeader": true, + "clipboardId": "", + "onActivate": function () {} + }); + } + } + + // Format the result based on type + let entry; + if (item.isImage) { + entry = formatImageEntry(item); + } else { + entry = formatTextEntry(item); + } + + // Add activation handler + entry.onActivate = function () { + if (Settings.data.appLauncher.autoPasteClipboard) { + launcher.closeImmediately(); + Qt.callLater(() => { + ClipboardService.pasteFromClipboard(item.id, item.mime); + }); + } else { + ClipboardService.copyToClipboard(item.id); + launcher.close(); + } + }; + + results.push(entry); + }); + + // Show empty state if no results + if (results.length === 0) { + results.push({ + "name": searchTerm ? "No matching clipboard items" : "Clipboard is empty", + "description": searchTerm ? `No items containing "${query}"` : "Copy something to see it here", + "icon": iconMode === "tabler" ? "clipboard" : "text-x-generic", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () {// Do nothing + } + }); + } + + //Logger.i("ClipboardPlugin", `Returning ${results.length} results for query: "${query}"`) + return results; } - //Logger.i("ClipboardPlugin", `Returning ${results.length} results for query: "${query}"`) - return results; - } + function formatImageEntry(item) { + const meta = ClipboardService.parseImageMeta(item.preview); - function formatImageEntry(item) { - const meta = ClipboardService.parseImageMeta(item.preview); - - return { - "name": meta ? `Image ${meta.w}×${meta.h}` : "Image", - "description": meta ? `${meta.fmt} • ${meta.size}` : item.mime || "Image data", - "icon": iconMode === "tabler" ? "photo" : "image", - "isTablerIcon": true, - "isImage": true, - "imageWidth": meta ? meta.w : 0, - "imageHeight": meta ? meta.h : 0, - "clipboardId": item.id, - "mime": item.mime, - "preview": item.preview, - "provider": root - }; - } - - function formatTextEntry(item) { - const preview = (item.preview || "").trim(); - const lines = preview.split('\n').filter(l => l.trim()); - - let title = lines[0] || "Empty text"; - if (title.length > 60) { - title = title.substring(0, 57) + "..."; + return { + "name": meta ? `Image ${meta.w}×${meta.h}` : "Image", + "description": meta ? `${meta.fmt} • ${meta.size}` : item.mime || "Image data", + "icon": iconMode === "tabler" ? "photo" : "image", + "isTablerIcon": true, + "isImage": true, + "imageWidth": meta ? meta.w : 0, + "imageHeight": meta ? meta.h : 0, + "clipboardId": item.id, + "mime": item.mime, + "preview": item.preview, + "provider": root + }; } - let description = ""; - if (lines.length > 1) { - description = lines[1]; - if (description.length > 80) { - description = description.substring(0, 77) + "..."; - } - } else { - // Preview is truncated at ~100 chars, so we can't show exact count - if (preview.length >= 100) { - description = I18n.tr("toast.clipboard.long-text"); - } else { - const chars = preview.length; - const words = preview.split(/\s+/).length; - description = `${chars} characters, ${words} word${words !== 1 ? 's' : ''}`; - } + function formatTextEntry(item) { + const preview = (item.preview || "").trim(); + const lines = preview.split('\n').filter(l => l.trim()); + + let title = lines[0] || "Empty text"; + if (title.length > 60) { + title = title.substring(0, 57) + "..."; + } + + let description = ""; + if (lines.length > 1) { + description = lines[1]; + if (description.length > 80) { + description = description.substring(0, 77) + "..."; + } + } else { + // Preview is truncated at ~100 chars, so we can't show exact count + if (preview.length >= 100) { + description = I18n.tr("toast.clipboard.long-text"); + } else { + const chars = preview.length; + const words = preview.split(/\s+/).length; + description = `${chars} characters, ${words} word${words !== 1 ? 's' : ''}`; + } + } + + let defaultIcon = iconMode === "tabler" ? "clipboard" : "text-x-generic"; + let colorHex = ""; + if (Settings.data.appLauncher.enableClipboardSmartIcons) { + if (item.contentType === "link") + defaultIcon = iconMode === "tabler" ? "link" : "insert-link"; + else if (item.contentType === "file") + defaultIcon = iconMode === "tabler" ? "file" : "text-x-generic"; + else if (item.contentType === "code") + defaultIcon = iconMode === "tabler" ? "code" : "text-x-script"; + else if (item.contentType === "color") { + defaultIcon = iconMode === "tabler" ? "palette" : "color-picker"; + colorHex = preview; + } + } + + return { + "name": title, + "description": description, + "icon": defaultIcon, + "isTablerIcon": true, + "isImage": false, + "clipboardId": item.id, + "preview": preview, + "contentType": item.contentType, + "colorHex": colorHex, + "provider": root + }; } - return { - "name": title, - "description": description, - "icon": iconMode === "tabler" ? "clipboard" : "text-x-generic", - "isTablerIcon": true, - "isImage": false, - "clipboardId": item.id, - "preview": preview, - "provider": root - }; - } - - function getImageForItem(clipboardId) { - return ClipboardService.getImageData ? ClipboardService.getImageData(clipboardId) : null; - } - - // ------------------------- - // Item actions for launcher delegate - function getItemActions(item) { - if (!item || !item.clipboardId) - return []; - - var actions = []; - - // Annotation tool for images - if (item.isImage && Settings.data.appLauncher.screenshotAnnotationTool !== "") { - actions.push({ - "icon": "pencil", - "tooltip": I18n.tr("tooltips.open-annotation-tool"), - "action": function () { - var tool = Settings.data.appLauncher.screenshotAnnotationTool; - Quickshell.execDetached(["sh", "-c", "cliphist decode " + item.clipboardId + " | " + tool]); - if (launcher) - launcher.close(); - } - }); + function getImageForItem(clipboardId) { + return ClipboardService.getImageData ? ClipboardService.getImageData(clipboardId) : null; } - // Delete action - actions.push({ - "icon": "trash", - "tooltip": I18n.tr("launcher.providers.clipboard-delete"), - "action": function () { - deleteItem(item); - } - }); + // ------------------------- + // Item actions for launcher delegate + function getItemActions(item) { + if (!item || !item.clipboardId) + return []; - return actions; - } + var actions = []; - function canDeleteItem(item) { - return item && !!item.clipboardId; - } + // Annotation tool for images + if (item.isImage && Settings.data.appLauncher.screenshotAnnotationTool !== "") { + actions.push({ + "icon": "pencil", + "tooltip": I18n.tr("tooltips.open-annotation-tool"), + "action": function () { + var tool = Settings.data.appLauncher.screenshotAnnotationTool; + Quickshell.execDetached(["sh", "-c", "cliphist decode " + item.clipboardId + " | " + tool]); + if (launcher) + launcher.close(); + } + }); + } - function deleteItem(item) { - if (!item || !item.clipboardId) - return; + // Delete action + actions.push({ + "icon": "trash", + "tooltip": I18n.tr("launcher.providers.clipboard-delete"), + "action": function () { + deleteItem(item); + } + }); - // Set provider state before deletion so refresh works - gotResults = false; - isWaitingForData = true; - lastSearchText = launcher ? launcher.searchText : ""; - - // Delete the item - ClipboardService.deleteById(String(item.clipboardId)); - } - - // Prepare item for display (handles image decoding) - function prepareItem(item) { - if (item && item.isImage && item.clipboardId) { - if (!ClipboardService.getImageData(item.clipboardId)) { - ClipboardService.decodeToDataUrl(item.clipboardId, item.mime, null); - } + return actions; } - } - // Get image URL for item (used by delegates) - function getImageUrl(item) { - if (!item || !item.clipboardId) - return ""; - return ClipboardService.getImageData(item.clipboardId) || ""; - } + function canDeleteItem(item) { + return item && !!item.clipboardId; + } - // Get preview data for the preview panel - function getPreviewData(item) { - if (!item) - return null; - return { - "clipboardId": item.clipboardId, - "isImage": item.isImage, - "mime": item.mime, - "preview": item.preview - }; - } + function deleteItem(item) { + if (!item || !item.clipboardId) + return; + + // Set provider state before deletion so refresh works + gotResults = false; + isWaitingForData = true; + lastSearchText = launcher ? launcher.searchText : ""; + + // Delete the item + ClipboardService.deleteById(String(item.clipboardId)); + } + + // Prepare item for display (handles image decoding) + function prepareItem(item) { + if (item && item.isImage && item.clipboardId) { + if (!ClipboardService.getImageData(item.clipboardId)) { + ClipboardService.decodeToDataUrl(item.clipboardId, item.mime, null); + } + } + } + + // Get image URL for item (used by delegates) + function getImageUrl(item) { + if (!item || !item.clipboardId) + return ""; + return ClipboardService.getImageData(item.clipboardId) || ""; + } + + // Get preview data for the preview panel + function getPreviewData(item) { + if (!item || item.isHeader) + return null; + return { + "clipboardId": item.clipboardId, + "isImage": item.isImage, + "mime": item.mime, + "preview": item.preview + }; + } } diff --git a/Modules/Panels/Settings/Tabs/Launcher/ClipboardSubTab.qml b/Modules/Panels/Settings/Tabs/Launcher/ClipboardSubTab.qml index 4c1a100a9..fd7899a46 100644 --- a/Modules/Panels/Settings/Tabs/Launcher/ClipboardSubTab.qml +++ b/Modules/Panels/Settings/Tabs/Launcher/ClipboardSubTab.qml @@ -6,67 +6,94 @@ import qs.Services.System import qs.Widgets ColumnLayout { - id: root - spacing: Style.marginL - Layout.fillWidth: true - - NToggle { - label: I18n.tr("panels.launcher.settings-clipboard-history-label") - description: I18n.tr("panels.launcher.settings-clipboard-history-description") - checked: Settings.data.appLauncher.enableClipboardHistory - onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked - defaultValue: Settings.getDefaultValue("appLauncher.enableClipboardHistory") - } - - NToggle { - label: I18n.tr("panels.launcher.settings-clip-preview-label") - description: I18n.tr("panels.launcher.settings-clip-preview-description") - checked: Settings.data.appLauncher.enableClipPreview - onToggled: checked => Settings.data.appLauncher.enableClipPreview = checked - defaultValue: Settings.getDefaultValue("appLauncher.enableClipPreview") - enabled: Settings.data.appLauncher.enableClipboardHistory - } - - NToggle { - label: I18n.tr("panels.launcher.settings-clip-wrap-text-label") - description: I18n.tr("panels.launcher.settings-clip-wrap-text-description") - checked: Settings.data.appLauncher.clipboardWrapText - onToggled: checked => Settings.data.appLauncher.clipboardWrapText = checked - defaultValue: Settings.getDefaultValue("appLauncher.clipboardWrapText") - enabled: Settings.data.appLauncher.enableClipboardHistory - } - - NToggle { - label: I18n.tr("panels.launcher.settings-auto-paste-label") - description: I18n.tr("panels.launcher.settings-auto-paste-description") - checked: Settings.data.appLauncher.autoPasteClipboard - onToggled: checked => Settings.data.appLauncher.autoPasteClipboard = checked - defaultValue: Settings.getDefaultValue("appLauncher.autoPasteClipboard") - enabled: Settings.data.appLauncher.enableClipboardHistory && ProgramCheckerService.wtypeAvailable - } - - NDivider { + id: root + spacing: Style.marginL Layout.fillWidth: true - visible: Settings.data.appLauncher.enableClipboardHistory - } - NTextInput { - label: I18n.tr("panels.launcher.settings-clipboard-watch-text-label") - description: I18n.tr("panels.launcher.settings-clipboard-watch-text-description") - Layout.fillWidth: true - text: Settings.data.appLauncher.clipboardWatchTextCommand - onEditingFinished: Settings.data.appLauncher.clipboardWatchTextCommand = text - enabled: Settings.data.appLauncher.enableClipboardHistory - visible: Settings.data.appLauncher.enableClipboardHistory - } + NToggle { + label: I18n.tr("panels.launcher.settings-clipboard-history-label") + description: I18n.tr("panels.launcher.settings-clipboard-history-description") + checked: Settings.data.appLauncher.enableClipboardHistory + onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked + defaultValue: Settings.getDefaultValue("appLauncher.enableClipboardHistory") + } - NTextInput { - label: I18n.tr("panels.launcher.settings-clipboard-watch-image-label") - description: I18n.tr("panels.launcher.settings-clipboard-watch-image-description") - Layout.fillWidth: true - text: Settings.data.appLauncher.clipboardWatchImageCommand - onEditingFinished: Settings.data.appLauncher.clipboardWatchImageCommand = text - enabled: Settings.data.appLauncher.enableClipboardHistory - visible: Settings.data.appLauncher.enableClipboardHistory - } + NToggle { + label: I18n.tr("panels.launcher.settings-clip-preview-label") + description: I18n.tr("panels.launcher.settings-clip-preview-description") + checked: Settings.data.appLauncher.enableClipPreview + onToggled: checked => Settings.data.appLauncher.enableClipPreview = checked + defaultValue: Settings.getDefaultValue("appLauncher.enableClipPreview") + enabled: Settings.data.appLauncher.enableClipboardHistory + } + + NToggle { + label: I18n.tr("panels.launcher.settings-clip-wrap-text-label") + description: I18n.tr("panels.launcher.settings-clip-wrap-text-description") + checked: Settings.data.appLauncher.clipboardWrapText + onToggled: checked => Settings.data.appLauncher.clipboardWrapText = checked + defaultValue: Settings.getDefaultValue("appLauncher.clipboardWrapText") + enabled: Settings.data.appLauncher.enableClipboardHistory + } + + NToggle { + label: I18n.tr("panels.launcher.settings-auto-paste-label") + description: I18n.tr("panels.launcher.settings-auto-paste-description") + checked: Settings.data.appLauncher.autoPasteClipboard + onToggled: checked => Settings.data.appLauncher.autoPasteClipboard = checked + defaultValue: Settings.getDefaultValue("appLauncher.autoPasteClipboard") + enabled: Settings.data.appLauncher.enableClipboardHistory && ProgramCheckerService.wtypeAvailable + } + + NToggle { + label: I18n.tr("panels.launcher.settings-clip-smart-icons-label") + description: I18n.tr("panels.launcher.settings-clip-smart-icons-description") + checked: Settings.data.appLauncher.enableClipboardSmartIcons + onToggled: checked => Settings.data.appLauncher.enableClipboardSmartIcons = checked + defaultValue: Settings.getDefaultValue("appLauncher.enableClipboardSmartIcons") + enabled: Settings.data.appLauncher.enableClipboardHistory + } + + NToggle { + label: I18n.tr("panels.launcher.settings-clip-chips-label") + description: I18n.tr("panels.launcher.settings-clip-chips-description") + checked: Settings.data.appLauncher.enableClipboardChips + onToggled: checked => Settings.data.appLauncher.enableClipboardChips = checked + defaultValue: Settings.getDefaultValue("appLauncher.enableClipboardChips") + enabled: Settings.data.appLauncher.enableClipboardHistory + } + + NToggle { + label: I18n.tr("panels.launcher.settings-clip-date-headers-label") + description: I18n.tr("panels.launcher.settings-clip-date-headers-description") + checked: Settings.data.appLauncher.enableClipboardDateHeaders + onToggled: checked => Settings.data.appLauncher.enableClipboardDateHeaders = checked + defaultValue: Settings.getDefaultValue("appLauncher.enableClipboardDateHeaders") + enabled: Settings.data.appLauncher.enableClipboardHistory + } + + NDivider { + Layout.fillWidth: true + visible: Settings.data.appLauncher.enableClipboardHistory + } + + NTextInput { + label: I18n.tr("panels.launcher.settings-clipboard-watch-text-label") + description: I18n.tr("panels.launcher.settings-clipboard-watch-text-description") + Layout.fillWidth: true + text: Settings.data.appLauncher.clipboardWatchTextCommand + onEditingFinished: Settings.data.appLauncher.clipboardWatchTextCommand = text + enabled: Settings.data.appLauncher.enableClipboardHistory + visible: Settings.data.appLauncher.enableClipboardHistory + } + + NTextInput { + label: I18n.tr("panels.launcher.settings-clipboard-watch-image-label") + description: I18n.tr("panels.launcher.settings-clipboard-watch-image-description") + Layout.fillWidth: true + text: Settings.data.appLauncher.clipboardWatchImageCommand + onEditingFinished: Settings.data.appLauncher.clipboardWatchImageCommand = text + enabled: Settings.data.appLauncher.enableClipboardHistory + visible: Settings.data.appLauncher.enableClipboardHistory + } } diff --git a/Services/Keyboard/ClipboardService.qml b/Services/Keyboard/ClipboardService.qml index db646c9a2..f9fb2f772 100644 --- a/Services/Keyboard/ClipboardService.qml +++ b/Services/Keyboard/ClipboardService.qml @@ -8,508 +8,528 @@ import qs.Services.UI // Clipboard history service using cliphist + local content cache Singleton { - id: root + id: root - // Public API - property bool active: Settings.data.appLauncher.enableClipboardHistory && cliphistAvailable - property bool loading: false - property var items: [] // [{id, preview, mime, isImage}] + // Public API + property bool active: Settings.data.appLauncher.enableClipboardHistory && cliphistAvailable + property bool loading: false + property var items: [] // [{id, preview, mime, isImage}] - // Check if cliphist is available on the system - property bool cliphistAvailable: false - property bool dependencyChecked: false + // Check if cliphist is available on the system + property bool cliphistAvailable: false + property bool dependencyChecked: false - // Optional automatic watchers to feed cliphist DB - property bool autoWatch: true - property bool watchersStarted: false + // Optional automatic watchers to feed cliphist DB + property bool autoWatch: true + property bool watchersStarted: false - // Expose decoded thumbnails by id and a revision to notify bindings - property var imageDataById: ({}) - property var _imageDataInsertOrder: [] // insertion-order IDs for LRU eviction - readonly property int _imageDataMaxEntries: 20 // max decoded images held in RAM at once - property int revision: 0 + // Expose decoded thumbnails by id and a revision to notify bindings + property var imageDataById: ({}) + property var _imageDataInsertOrder: [] // insertion-order IDs for LRU eviction + readonly property int _imageDataMaxEntries: 20 // max decoded images held in RAM at once + property int revision: 0 - // Local content cache - stores full text content by ID - // This avoids relying on cliphist decode which can be unreliable - property var contentCache: ({}) + // Local content cache - stores full text content by ID + // This avoids relying on cliphist decode which can be unreliable + property var contentCache: ({}) - // Track the most recent clipboard content for instant access - property string _latestTextContent: "" - property string _latestTextId: "" + // Track the most recent clipboard content for instant access + property string _latestTextContent: "" + property string _latestTextId: "" - // Approximate first-seen timestamps for entries this session (seconds) - property var firstSeenById: ({}) + // Approximate first-seen timestamps for entries this session (seconds) + property var firstSeenById: ({}) - // Internal: store callback for decode - property var _decodeCallback: null - property int _decodeRequestId: 0 + // Internal: store callback for decode + property var _decodeCallback: null + property int _decodeRequestId: 0 - // Queue for base64 decodes - property var _b64Queue: [] - property var _b64CurrentCb: null - property string _b64CurrentMime: "" - property string _b64CurrentId: "" + // Queue for base64 decodes + property var _b64Queue: [] + property var _b64CurrentCb: null + property string _b64CurrentMime: "" + property string _b64CurrentId: "" - signal listCompleted + signal listCompleted - // Check if cliphist is available - Component.onCompleted: { - checkCliphistAvailability(); - } - - // Check dependency availability - function checkCliphistAvailability() { - if (dependencyChecked) - return; - dependencyCheckProcess.command = ["sh", "-c", "command -v cliphist"]; - dependencyCheckProcess.running = true; - } - - // Process to check if cliphist is available - Process { - id: dependencyCheckProcess - stdout: StdioCollector {} - onExited: (exitCode, exitStatus) => { - root.dependencyChecked = true; - if (exitCode === 0) { - root.cliphistAvailable = true; - // Start watchers if feature is enabled - if (root.active) { - startWatchers(); - } - } else { - root.cliphistAvailable = false; - // Show toast notification if feature is enabled but cliphist is missing - if (Settings.data.appLauncher.enableClipboardHistory) { - ToastService.showWarning(I18n.tr("toast.clipboard.unavailable"), I18n.tr("toast.clipboard.unavailable-desc"), 6000); - } - } + // Check if cliphist is available + Component.onCompleted: { + checkCliphistAvailability(); } - } - // Start/stop watchers when enabled changes - onActiveChanged: { - if (root.active) { - startWatchers(); - } else { - stopWatchers(); - loading = false; - items = []; + // Check dependency availability + function checkCliphistAvailability() { + if (dependencyChecked) + return; + dependencyCheckProcess.command = ["sh", "-c", "command -v cliphist"]; + dependencyCheckProcess.running = true; } - } - // Fallback: periodically refresh list so UI updates even if not in clip mode - Timer { - interval: 5000 - repeat: true - running: root.active - onTriggered: list() - } - - // Internal process objects - Process { - id: listProc - stdout: StdioCollector {} - onExited: (exitCode, exitStatus) => { - const out = String(stdout.text); - const lines = out.split('\n').filter(l => l.length > 0); - // cliphist list default format: " " or "\t" - const parsed = lines.map(l => { - let id = ""; - let preview = ""; - const m = l.match(/^(\d+)\s+(.+)$/); - if (m) { - id = m[1]; - preview = m[2]; - } else { - const tab = l.indexOf('\t'); - id = tab > -1 ? l.slice(0, tab) : l; - preview = tab > -1 ? l.slice(tab + 1) : ""; - } - const lower = preview.toLowerCase(); - const isImage = lower.startsWith("[image]") || lower.includes(" binary data "); - // Best-effort mime guess from preview - var mime = "text/plain"; - if (isImage) { - if (lower.includes(" png")) - mime = "image/png"; - else if (lower.includes(" jpg") || lower.includes(" jpeg")) - mime = "image/jpeg"; - else if (lower.includes(" webp")) - mime = "image/webp"; - else if (lower.includes(" gif")) - mime = "image/gif"; - else - mime = "image/*"; - } - // Record first seen time for new ids (approximate copy time) - if (!root.firstSeenById[id]) { - root.firstSeenById[id] = Time.timestamp; - } - return { - "id": id, - "preview": preview, - "isImage": isImage, - "mime": mime - }; - }); - - // Filter out browser junk when copying images - const filtered = parsed.filter(item => { - if (item.isImage) - return true; - const p = item.preview; - // Skip UTF-16 encoded text (has null bytes between chars), chromium browser artifact - const nullCount = (p.match(/\x00/g) || []).length; - if (nullCount > p.length * 0.2) - return false; - // Skip browser-generated HTML wrapper, firefox - if (p.toLowerCase().startsWith(" root._imageDataMaxEntries) { - const evicted = root._imageDataInsertOrder.shift(); - delete root.imageDataById[evicted]; - } - root.revision += 1; - } - root._b64CurrentCb = null; - root._b64CurrentMime = ""; - root._b64CurrentId = ""; - Qt.callLater(root._startNextB64); - } - } - - // Text watcher - stores to cliphist and triggers content capture - Process { - id: watchText - stdout: StdioCollector {} - onExited: (exitCode, exitStatus) => { - if (root.autoWatch && root.watchersStarted && Settings.data.appLauncher.clipboardWatchTextCommand.trim() !== "") { - watchTextRestartTimer.restart(); - } - } - } - - Timer { - id: watchTextRestartTimer - interval: 1000 - repeat: false - onTriggered: { - if (root.autoWatch && root.watchersStarted) - watchText.running = true; - } - } - - // Image watcher - Process { - id: watchImage - stdout: StdioCollector {} - onExited: (exitCode, exitStatus) => { - if (root.autoWatch && root.watchersStarted && Settings.data.appLauncher.clipboardWatchImageCommand.trim() !== "") { - watchImageRestartTimer.restart(); - } - } - } - - Timer { - id: watchImageRestartTimer - interval: 1000 - repeat: false - onTriggered: { - if (root.autoWatch && root.watchersStarted) - watchImage.running = true; - } - } - - // Capture current clipboard text when needed - Process { - id: captureTextProc - stdout: StdioCollector {} - onExited: (exitCode, exitStatus) => { - if (exitCode === 0) { - const content = String(stdout.text); - if (content.length > 0) { - root._latestTextContent = content; - // Associate with newest item if we have one - if (root.items.length > 0 && !root.items[0].isImage) { - const newestId = root.items[0].id; - if (!root.contentCache[newestId]) { - root.contentCache[newestId] = content; - root.revision++; + // Process to check if cliphist is available + Process { + id: dependencyCheckProcess + stdout: StdioCollector {} + onExited: (exitCode, exitStatus) => { + root.dependencyChecked = true; + if (exitCode === 0) { + root.cliphistAvailable = true; + // Start watchers if feature is enabled + if (root.active) { + startWatchers(); + } + } else { + root.cliphistAvailable = false; + // Show toast notification if feature is enabled but cliphist is missing + if (Settings.data.appLauncher.enableClipboardHistory) { + ToastService.showWarning(I18n.tr("toast.clipboard.unavailable"), I18n.tr("toast.clipboard.unavailable-desc"), 6000); + } } - } } - } } - } - function startWatchers() { - if (!root.active || !autoWatch || watchersStarted || !root.cliphistAvailable) - return; - watchersStarted = true; + // Start/stop watchers when enabled changes + onActiveChanged: { + if (root.active) { + startWatchers(); + } else { + stopWatchers(); + loading = false; + items = []; + } + } - // Text watcher - watchText.command = ["sh", "-c", Settings.data.appLauncher.clipboardWatchTextCommand]; - watchText.running = true; + // Fallback: periodically refresh list so UI updates even if not in clip mode + Timer { + interval: 5000 + repeat: true + running: root.active + onTriggered: list() + } + + // Internal process objects + Process { + id: listProc + stdout: StdioCollector {} + onExited: (exitCode, exitStatus) => { + const out = String(stdout.text); + const lines = out.split('\n').filter(l => l.length > 0); + // cliphist list default format: " " or "\t" + const parsed = lines.map((l, i) => { + let id = ""; + let preview = ""; + const m = l.match(/^(\d+)\s+(.+)$/); + if (m) { + id = m[1]; + preview = m[2]; + } else { + const tab = l.indexOf('\t'); + id = tab > -1 ? l.slice(0, tab) : l; + preview = tab > -1 ? l.slice(tab + 1) : ""; + } + const lower = preview.toLowerCase(); + const isImage = lower.startsWith("[image]") || lower.includes(" binary data "); + // Best-effort mime guess from preview + var mime = "text/plain"; + if (isImage) { + if (lower.includes(" png")) + mime = "image/png"; + else if (lower.includes(" jpg") || lower.includes(" jpeg")) + mime = "image/jpeg"; + else if (lower.includes(" webp")) + mime = "image/webp"; + else if (lower.includes(" gif")) + mime = "image/gif"; + else + mime = "image/*"; + } + // Record first seen time for new ids (approximate copy time) + if (!root.firstSeenById[id]) { + const assumedAge = i * 15 * 60; + root.firstSeenById[id] = Time.timestamp - assumedAge; + } + // Smart type detection + var contentType = "text"; + if (isImage) { + contentType = "image"; + } else { + const t = preview.trim(); + const tLower = t.toLowerCase(); + if (/^#([a-f0-9]{3}|[a-f0-9]{6}|[a-f0-9]{8})$/.test(tLower)) { + contentType = "color"; + } else if (/^https?:\/\//i.test(t)) { + contentType = "link"; + } else if (/^(\/|~\/|file:\/\/)/i.test(t) && !t.startsWith('//') && !t.includes('\n')) { + contentType = "file"; + } else if ((t.includes('{') && t.includes('}') && (t.includes(';') || t.includes('='))) || t.includes('') || t.includes('function') || t.includes('import ') || t.includes('export ')) { + contentType = "code"; + } + } + + return { + "id": id, + "preview": preview, + "isImage": isImage, + "mime": mime, + "contentType": contentType + }; + }); + + // Filter out browser junk when copying images + const filtered = parsed.filter(item => { + if (item.isImage) + return true; + const p = item.preview; + // Skip UTF-16 encoded text (has null bytes between chars), chromium browser artifact + const nullCount = (p.match(/\x00/g) || []).length; + if (nullCount > p.length * 0.2) + return false; + // Skip browser-generated HTML wrapper, firefox + if (p.toLowerCase().startsWith(" root._imageDataMaxEntries) { + const evicted = root._imageDataInsertOrder.shift(); + delete root.imageDataById[evicted]; + } + root.revision += 1; + } + root._b64CurrentCb = null; + root._b64CurrentMime = ""; + root._b64CurrentId = ""; + Qt.callLater(root._startNextB64); + } + } + + // Text watcher - stores to cliphist and triggers content capture + Process { + id: watchText + stdout: StdioCollector {} + onExited: (exitCode, exitStatus) => { + if (root.autoWatch && root.watchersStarted && Settings.data.appLauncher.clipboardWatchTextCommand.trim() !== "") { + watchTextRestartTimer.restart(); + } + } + } + + Timer { + id: watchTextRestartTimer + interval: 1000 + repeat: false + onTriggered: { + if (root.autoWatch && root.watchersStarted) + watchText.running = true; + } + } // Image watcher - watchImage.command = ["sh", "-c", Settings.data.appLauncher.clipboardWatchImageCommand]; - watchImage.running = true; - } - - function stopWatchers() { - if (!watchersStarted) - return; - watchText.running = false; - watchImage.running = false; - watchersStarted = false; - } - - // Capture current clipboard text and cache it - function captureCurrentClipboard() { - if (captureTextProc.running) - return; - captureTextProc.command = ["wl-paste", "--no-newline"]; - captureTextProc.running = true; - } - - function list(maxPreviewWidth) { - if (!root.active || !root.cliphistAvailable) { - return; - } - if (listProc.running) - return; - loading = true; - const width = maxPreviewWidth || 100; - listProc.command = ["cliphist", "list", "-preview-width", String(width)]; - listProc.running = true; - } - - // Get content for an ID - uses cache first, falls back to cliphist decode - function getContent(id) { - if (root.contentCache[id]) { - return root.contentCache[id]; - } - return null; - } - - // Async decode - checks cache first, then falls back to cliphist - function decode(id, cb) { - if (!root.cliphistAvailable) { - if (cb) - cb(""); - return; + Process { + id: watchImage + stdout: StdioCollector {} + onExited: (exitCode, exitStatus) => { + if (root.autoWatch && root.watchersStarted && Settings.data.appLauncher.clipboardWatchImageCommand.trim() !== "") { + watchImageRestartTimer.restart(); + } + } } - // Check cache first - const cached = root.contentCache[id]; - if (cached) { - if (cb) - cb(cached); - return; + Timer { + id: watchImageRestartTimer + interval: 1000 + repeat: false + onTriggered: { + if (root.autoWatch && root.watchersStarted) + watchImage.running = true; + } } - // Fall back to cliphist decode - if (decodeProc.running) { - decodeProc.running = false; + // Capture current clipboard text when needed + Process { + id: captureTextProc + stdout: StdioCollector {} + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + const content = String(stdout.text); + if (content.length > 0) { + root._latestTextContent = content; + // Associate with newest item if we have one + if (root.items.length > 0 && !root.items[0].isImage) { + const newestId = root.items[0].id; + if (!root.contentCache[newestId]) { + root.contentCache[newestId] = content; + root.revision++; + } + } + } + } + } } - root._decodeRequestId++; - decodeProc.requestId = root._decodeRequestId; - root._decodeCallback = function (content) { - // Cache the result if successful - if (content && content.trim()) { - root.contentCache[id] = content; - } - if (cb) - cb(content); - }; - const idStr = String(id); - decodeProc.command = ["cliphist", "decode", idStr]; - decodeProc.running = true; - } - function decodeToDataUrl(id, mime, cb) { - if (!root.cliphistAvailable) { - if (cb) - cb(""); - return; + function startWatchers() { + if (!root.active || !autoWatch || watchersStarted || !root.cliphistAvailable) + return; + watchersStarted = true; + + // Text watcher + watchText.command = ["sh", "-c", Settings.data.appLauncher.clipboardWatchTextCommand]; + watchText.running = true; + + // Image watcher + watchImage.command = ["sh", "-c", Settings.data.appLauncher.clipboardWatchImageCommand]; + watchImage.running = true; } - // If cached, return immediately - if (root.imageDataById[id]) { - if (cb) - cb(root.imageDataById[id]); - return; + + function stopWatchers() { + if (!watchersStarted) + return; + watchText.running = false; + watchImage.running = false; + watchersStarted = false; } - // Queue request; ensures single process handles sequentially - root._b64Queue.push({ - "id": id, - "mime": mime || "image/*", - "cb": cb - }); - if (!decodeB64Proc.running && root._b64CurrentCb === null) { - _startNextB64(); + + // Capture current clipboard text and cache it + function captureCurrentClipboard() { + if (captureTextProc.running) + return; + captureTextProc.command = ["wl-paste", "--no-newline"]; + captureTextProc.running = true; } - } - function getImageData(id) { - if (id === undefined) { - return null; + function list(maxPreviewWidth) { + if (!root.active || !root.cliphistAvailable) { + return; + } + if (listProc.running) + return; + loading = true; + const width = maxPreviewWidth || 100; + listProc.command = ["cliphist", "list", "-preview-width", String(width)]; + listProc.running = true; } - return root.imageDataById[id]; - } - function _startNextB64() { - if (root._b64Queue.length === 0 || !root.cliphistAvailable) - return; - const job = root._b64Queue.shift(); - root._b64CurrentCb = job.cb; - root._b64CurrentMime = job.mime; - root._b64CurrentId = job.id; - decodeB64Proc.command = ["sh", "-c", `cliphist decode ${job.id} | base64 -w 0`]; - decodeB64Proc.running = true; - } - - function copyToClipboard(id) { - if (!root.cliphistAvailable) { - return; + // Get content for an ID - uses cache first, falls back to cliphist decode + function getContent(id) { + if (root.contentCache[id]) { + return root.contentCache[id]; + } + return null; } - copyProc.command = ["sh", "-c", `cliphist decode ${id} | wl-copy`]; - copyProc.running = true; - } - function pasteFromClipboard(id, mime) { - if (!root.cliphistAvailable) { - return; + // Async decode - checks cache first, then falls back to cliphist + function decode(id, cb) { + if (!root.cliphistAvailable) { + if (cb) + cb(""); + return; + } + + // Check cache first + const cached = root.contentCache[id]; + if (cached) { + if (cb) + cb(cached); + return; + } + + // Fall back to cliphist decode + if (decodeProc.running) { + decodeProc.running = false; + } + root._decodeRequestId++; + decodeProc.requestId = root._decodeRequestId; + root._decodeCallback = function (content) { + // Cache the result if successful + if (content && content.trim()) { + root.contentCache[id] = content; + } + if (cb) + cb(content); + }; + const idStr = String(id); + decodeProc.command = ["cliphist", "decode", idStr]; + decodeProc.running = true; } - const isImage = mime && mime.startsWith("image/"); - const typeArg = isImage ? ` --type ${mime}` : ""; - const pasteKeys = isImage ? "wtype -M ctrl -k v" : "wtype -M ctrl -M shift v"; - const cmd = `cliphist decode ${id} | wl-copy${typeArg} && ${pasteKeys}`; - pasteProc.command = ["sh", "-c", cmd]; - pasteProc.running = true; - } - function pasteText(text) { - if (!text) - return; - const escaped = text.replace(/'/g, "'\\''"); - const cmd = `printf '%s' '${escaped}' | wl-copy && wtype -M ctrl -M shift v`; - pasteProc.command = ["sh", "-c", cmd]; - pasteProc.running = true; - } - - function deleteById(id) { - if (!root.cliphistAvailable) { - return; + function decodeToDataUrl(id, mime, cb) { + if (!root.cliphistAvailable) { + if (cb) + cb(""); + return; + } + // If cached, return immediately + if (root.imageDataById[id]) { + if (cb) + cb(root.imageDataById[id]); + return; + } + // Queue request; ensures single process handles sequentially + root._b64Queue.push({ + "id": id, + "mime": mime || "image/*", + "cb": cb + }); + if (!decodeB64Proc.running && root._b64CurrentCb === null) { + _startNextB64(); + } } - if (deleteProc.running) { - return; + + function getImageData(id) { + if (id === undefined) { + return null; + } + return root.imageDataById[id]; } - const idStr = String(id).trim(); - // Remove from caches - delete root.contentCache[idStr]; - delete root.imageDataById[idStr]; - const orderIdx = root._imageDataInsertOrder.indexOf(idStr); - if (orderIdx !== -1) - root._imageDataInsertOrder.splice(orderIdx, 1); - deleteProc.command = ["sh", "-c", `echo ${idStr} | cliphist delete`]; - deleteProc.running = true; - } - function wipeAll() { - if (!root.cliphistAvailable) { - return; + function _startNextB64() { + if (root._b64Queue.length === 0 || !root.cliphistAvailable) + return; + const job = root._b64Queue.shift(); + root._b64CurrentCb = job.cb; + root._b64CurrentMime = job.mime; + root._b64CurrentId = job.id; + decodeB64Proc.command = ["sh", "-c", `cliphist decode ${job.id} | base64 -w 0`]; + decodeB64Proc.running = true; } - // Clear caches - root.contentCache = {}; - root.imageDataById = {}; - root._imageDataInsertOrder = []; - root._latestTextContent = ""; - root._latestTextId = ""; - Quickshell.execDetached(["cliphist", "wipe"]); - revision++; - Qt.callLater(() => list()); - } + function copyToClipboard(id) { + if (!root.cliphistAvailable) { + return; + } + copyProc.command = ["sh", "-c", `cliphist decode ${id} | wl-copy`]; + copyProc.running = true; + } - // Parse image metadata from cliphist preview string - function parseImageMeta(preview) { - const re = /\[\[\s*binary data\s+([\d\.]+\s*(?:KiB|MiB|GiB|B))\s+(\w+)\s+(\d+)x(\d+)\s*\]\]/i; - const match = (preview || "").match(re); - if (!match) - return null; - return { - "size": match[1], - "fmt": (match[2] || "").toUpperCase(), - "w": Number(match[3]), - "h": Number(match[4]) - }; - } + function pasteFromClipboard(id, mime) { + if (!root.cliphistAvailable) { + return; + } + const isImage = mime && mime.startsWith("image/"); + const typeArg = isImage ? ` --type ${mime}` : ""; + const pasteKeys = isImage ? "wtype -M ctrl -k v" : "wtype -M ctrl -M shift v"; + const cmd = `cliphist decode ${id} | wl-copy${typeArg} && ${pasteKeys}`; + pasteProc.command = ["sh", "-c", cmd]; + pasteProc.running = true; + } + + function pasteText(text) { + if (!text) + return; + const escaped = text.replace(/'/g, "'\\''"); + const cmd = `printf '%s' '${escaped}' | wl-copy && wtype -M ctrl -M shift v`; + pasteProc.command = ["sh", "-c", cmd]; + pasteProc.running = true; + } + + function deleteById(id) { + if (!root.cliphistAvailable) { + return; + } + if (deleteProc.running) { + return; + } + const idStr = String(id).trim(); + // Remove from caches + delete root.contentCache[idStr]; + delete root.imageDataById[idStr]; + const orderIdx = root._imageDataInsertOrder.indexOf(idStr); + if (orderIdx !== -1) + root._imageDataInsertOrder.splice(orderIdx, 1); + deleteProc.command = ["sh", "-c", `echo ${idStr} | cliphist delete`]; + deleteProc.running = true; + } + + function wipeAll() { + if (!root.cliphistAvailable) { + return; + } + // Clear caches + root.contentCache = {}; + root.imageDataById = {}; + root._imageDataInsertOrder = []; + root._latestTextContent = ""; + root._latestTextId = ""; + + Quickshell.execDetached(["cliphist", "wipe"]); + revision++; + Qt.callLater(() => list()); + } + + // Parse image metadata from cliphist preview string + function parseImageMeta(preview) { + const re = /\[\[\s*binary data\s+([\d\.]+\s*(?:KiB|MiB|GiB|B))\s+(\w+)\s+(\d+)x(\d+)\s*\]\]/i; + const match = (preview || "").match(re); + if (!match) + return null; + return { + "size": match[1], + "fmt": (match[2] || "").toUpperCase(), + "w": Number(match[3]), + "h": Number(match[4]) + }; + } } From a913e95d0ae8d700fee694ae1805dc500fe607cf Mon Sep 17 00:00:00 2001 From: "Braian A. Diez" Date: Sat, 28 Feb 2026 12:32:34 -0300 Subject: [PATCH 2/5] chore: missing translation for clipboard Signed-off-by: Braian A. Diez --- Assets/Translations/de.json | 5 ++++ Assets/Translations/en.json | 7 +++++- Assets/Translations/es.json | 5 ++++ Assets/Translations/fr.json | 5 ++++ Assets/Translations/hu.json | 5 ++++ Assets/Translations/it.json | 5 ++++ Assets/Translations/ja.json | 5 ++++ Assets/Translations/ko-KR.json | 5 ++++ Assets/Translations/ku.json | 5 ++++ Assets/Translations/nl.json | 5 ++++ Assets/Translations/nn-HN.json | 9 +++++++ Assets/Translations/nn-NO.json | 5 ++++ Assets/Translations/pl.json | 5 ++++ Assets/Translations/pt.json | 5 ++++ Assets/Translations/ru.json | 5 ++++ Assets/Translations/sv.json | 5 ++++ Assets/Translations/tr.json | 5 ++++ Assets/Translations/uk-UA.json | 5 ++++ Assets/Translations/zh-CN.json | 5 ++++ Assets/Translations/zh-TW.json | 5 ++++ .../Launcher/Providers/ClipboardProvider.qml | 24 +++++++++---------- 21 files changed, 117 insertions(+), 13 deletions(-) diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index 68be66cda..80607945f 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -583,6 +583,10 @@ "system": "System", "webbrowser": "Webbrowser" }, + "date-filter-all-time": "Gesamte Zeit", + "date-filter-previous-7-days": "Letzte 7 Tage", + "date-filter-today": "Heute", + "date-filter-yesterday": "Gestern", "providers": { "applications": "Anwendungen", "calculator": "Rechner", @@ -1856,6 +1860,7 @@ "click-to-stop-recording": "Bildschirmrekorder (Aufnahme stoppen)", "collapse": "Seitenleiste einklappen", "copy-address": "Adresse kopieren", + "date-filter": "Datumsfilter", "delete-notification": "Benachrichtigung löschen", "dismiss-notification": "Benachrichtigung schließen", "do-not-disturb-enabled": "Nicht stören", diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index e5748c4f8..9806caf3a 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -607,7 +607,11 @@ "emoji-search-description": "Search and copy emojis", "settings-search-description": "Search and navigate to settings", "windows-search-description": "Search and focus open windows" - } + }, + "date-filter-all-time": "All Time", + "date-filter-today": "Today", + "date-filter-yesterday": "Yesterday", + "date-filter-previous-7-days": "Previous 7 Days" }, "lock-screen": { "authenticating": "Authenticating...", @@ -1862,6 +1866,7 @@ "click-to-stop-recording": "Screen recorder (stop recording)", "collapse": "Collapse sidebar", "copy-address": "Copy address", + "date-filter": "Date filter", "delete-notification": "Delete notification", "dismiss-notification": "Dismiss notification", "do-not-disturb-enabled": "Do Not Disturb", diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index e66a83930..e742d8480 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -583,6 +583,10 @@ "system": "Sistema", "webbrowser": "Navegador web" }, + "date-filter-all-time": "Todo el tiempo", + "date-filter-previous-7-days": "Últimos 7 días", + "date-filter-today": "Hoy", + "date-filter-yesterday": "Ayer", "providers": { "applications": "Aplicaciones", "calculator": "Calculadora", @@ -1856,6 +1860,7 @@ "click-to-stop-recording": "Grabadora de pantalla (detener grabación)", "collapse": "Colapsar barra lateral", "copy-address": "Copiar dirección", + "date-filter": "Filtro de fecha", "delete-notification": "Eliminar notificación", "dismiss-notification": "Descartar notificación", "do-not-disturb-enabled": "No molestar", diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index f9b4793bd..9e58b84c7 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -583,6 +583,10 @@ "system": "Système", "webbrowser": "Navigateur web" }, + "date-filter-all-time": "Tout le temps", + "date-filter-previous-7-days": "7 derniers jours", + "date-filter-today": "Aujourd'hui", + "date-filter-yesterday": "Hier", "providers": { "applications": "Applications", "calculator": "Calculatrice", @@ -1856,6 +1860,7 @@ "click-to-stop-recording": "Enregistreur d'écran (arrêter l'enregistrement)", "collapse": "Réduire la barre latérale", "copy-address": "Copier l'adresse", + "date-filter": "Filtre de date", "delete-notification": "Supprimer la notification", "dismiss-notification": "Ignorer la notification", "do-not-disturb-enabled": "Ne pas déranger", diff --git a/Assets/Translations/hu.json b/Assets/Translations/hu.json index 6a490c02a..36af3eac5 100644 --- a/Assets/Translations/hu.json +++ b/Assets/Translations/hu.json @@ -583,6 +583,10 @@ "system": "Rendszer", "webbrowser": "Webböngésző" }, + "date-filter-all-time": "Minden idő", + "date-filter-previous-7-days": "Elmúlt 7 nap", + "date-filter-today": "Ma", + "date-filter-yesterday": "Tegnap", "providers": { "applications": "Alkalmazások", "calculator": "Számológép", @@ -1856,6 +1860,7 @@ "click-to-stop-recording": "Képernyőfelvevő (felvétel leállítása)", "collapse": "Oldalsáv összecsukása", "copy-address": "Cím másolása", + "date-filter": "Dátum szűrő", "delete-notification": "Értesítés törlése", "dismiss-notification": "Értesítés elvetése", "do-not-disturb-enabled": "Ne zavarjanak", diff --git a/Assets/Translations/it.json b/Assets/Translations/it.json index dc01919cb..6892ce587 100644 --- a/Assets/Translations/it.json +++ b/Assets/Translations/it.json @@ -583,6 +583,10 @@ "system": "Sistema", "webbrowser": "Browser web" }, + "date-filter-all-time": "Tutto il tempo", + "date-filter-previous-7-days": "Ultimi 7 giorni", + "date-filter-today": "Oggi", + "date-filter-yesterday": "Ieri", "providers": { "applications": "Applicazioni", "calculator": "Calcolatrice", @@ -1856,6 +1860,7 @@ "click-to-stop-recording": "Registratore schermo (ferma registrazione)", "collapse": "Comprimi barra laterale", "copy-address": "Copia indirizzo", + "date-filter": "Filtro data", "delete-notification": "Elimina notifica", "dismiss-notification": "Ignora notifica", "do-not-disturb-enabled": "Non disturbare", diff --git a/Assets/Translations/ja.json b/Assets/Translations/ja.json index 7f6021f05..1f7086890 100644 --- a/Assets/Translations/ja.json +++ b/Assets/Translations/ja.json @@ -583,6 +583,10 @@ "system": "システム", "webbrowser": "ウェブブラウザ" }, + "date-filter-all-time": "すべての期間", + "date-filter-previous-7-days": "過去 7 日間", + "date-filter-today": "今日", + "date-filter-yesterday": "昨日", "providers": { "applications": "アプリケーション", "calculator": "電卓", @@ -1856,6 +1860,7 @@ "click-to-stop-recording": "画面録画(録画停止)", "collapse": "サイドバーを折りたたむ", "copy-address": "アドレスをコピー", + "date-filter": "日付フィルター", "delete-notification": "通知を削除", "dismiss-notification": "通知を閉じる", "do-not-disturb-enabled": "おやすみモード", diff --git a/Assets/Translations/ko-KR.json b/Assets/Translations/ko-KR.json index e15257716..731b104d1 100644 --- a/Assets/Translations/ko-KR.json +++ b/Assets/Translations/ko-KR.json @@ -583,6 +583,10 @@ "system": "시스템", "webbrowser": "웹 브라우저" }, + "date-filter-all-time": "모든 시간", + "date-filter-previous-7-days": "최근 7일", + "date-filter-today": "오늘", + "date-filter-yesterday": "어제", "providers": { "applications": "애플리케이션", "calculator": "계산기", @@ -1856,6 +1860,7 @@ "click-to-stop-recording": "화면 녹화 (녹화 중지)", "collapse": "사이드바 접기", "copy-address": "주소 복사", + "date-filter": "날짜 필터", "delete-notification": "알림 삭제", "dismiss-notification": "알림 닫기", "do-not-disturb-enabled": "방해 금지 모드", diff --git a/Assets/Translations/ku.json b/Assets/Translations/ku.json index b56c20f5e..0cb5fa464 100644 --- a/Assets/Translations/ku.json +++ b/Assets/Translations/ku.json @@ -544,6 +544,10 @@ "system": "Pergal", "webbrowser": "Geroka tevnê" }, + "date-filter-all-time": "Hemû Dem", + "date-filter-previous-7-days": "7 Rojên Borî", + "date-filter-today": "Îro", + "date-filter-yesterday": "Duh", "providers": { "applications": "Sepan", "calculator": "Jimarkar", @@ -1650,6 +1654,7 @@ "click-to-stop-recording": "Tomarkarê dîmenderê (tomarkirinê rawestîne)", "collapse": "Darika kêlelê veşêre", "copy-address": "Navnîşana kopî bike", + "date-filter": "Parzûna dîrokê", "delete-notification": "Agahdariyê jê bibe", "do-not-disturb-enabled": "Dengê dernexîne", "expand": "Darika kêlekê fereh bike", diff --git a/Assets/Translations/nl.json b/Assets/Translations/nl.json index 5867d8384..545fde545 100644 --- a/Assets/Translations/nl.json +++ b/Assets/Translations/nl.json @@ -583,6 +583,10 @@ "system": "Systeem", "webbrowser": "Webbrowser" }, + "date-filter-all-time": "Alle tijd", + "date-filter-previous-7-days": "Afgelopen 7 dagen", + "date-filter-today": "Vandaag", + "date-filter-yesterday": "Gisteren", "providers": { "applications": "Applicaties", "calculator": "Rekenmachine", @@ -1856,6 +1860,7 @@ "click-to-stop-recording": "Schermrecorder (opname stoppen)", "collapse": "Zijbalk inklappen", "copy-address": "Adres kopiëren", + "date-filter": "Datumfilter", "delete-notification": "Melding verwijderen", "dismiss-notification": "Melding sluiten", "do-not-disturb-enabled": "Niet storen", diff --git a/Assets/Translations/nn-HN.json b/Assets/Translations/nn-HN.json index de1f34d7c..e73a5e129 100644 --- a/Assets/Translations/nn-HN.json +++ b/Assets/Translations/nn-HN.json @@ -224,6 +224,12 @@ "brightness": "Ljosstyrke", "select-color-description": "Lita med hamlitene for tyngd." }, + "launcher": { + "date-filter-all-time": "Heile tida", + "date-filter-previous-7-days": "Siste 7 dagar", + "date-filter-today": "I dag", + "date-filter-yesterday": "I går" + }, "options": { "bar": { "density-compact": "Klembd" @@ -237,5 +243,8 @@ }, "system": { "welcome-back": "Velkomen attende," + }, + "tooltips": { + "date-filter": "Dato-filter" } } diff --git a/Assets/Translations/nn-NO.json b/Assets/Translations/nn-NO.json index 68d6a126b..153bfee75 100644 --- a/Assets/Translations/nn-NO.json +++ b/Assets/Translations/nn-NO.json @@ -575,6 +575,10 @@ "system": "System", "webbrowser": "Nettlesar" }, + "date-filter-all-time": "Heile tida", + "date-filter-previous-7-days": "Siste 7 dagar", + "date-filter-today": "I dag", + "date-filter-yesterday": "I går", "providers": { "applications": "Applikasjonar", "calculator": "Kalkulator", @@ -1782,6 +1786,7 @@ "click-to-stop-recording": "Skjermopptak (stogg opptak)", "collapse": "Gøym sidestolpen", "copy-address": "Kopier adresse", + "date-filter": "Dato-filter", "delete-notification": "Slett varsel", "dismiss-notification": "Avvis varsel", "do-not-disturb-enabled": "Ikkje forstyrr", diff --git a/Assets/Translations/pl.json b/Assets/Translations/pl.json index 6a29aec3f..f11d1f327 100644 --- a/Assets/Translations/pl.json +++ b/Assets/Translations/pl.json @@ -583,6 +583,10 @@ "system": "System", "webbrowser": "Przeglądarka www" }, + "date-filter-all-time": "Cały czas", + "date-filter-previous-7-days": "Ostatnie 7 dni", + "date-filter-today": "Dzisiaj", + "date-filter-yesterday": "Wczoraj", "providers": { "applications": "Aplikacje", "calculator": "Kalkulator", @@ -1856,6 +1860,7 @@ "click-to-stop-recording": "Nagrywanie ekranu (stop)", "collapse": "Zwiń pasek boczny", "copy-address": "Kopiuj adres", + "date-filter": "Filtr daty", "delete-notification": "Usuń powiadomienie", "dismiss-notification": "Odrzuć powiadomienie", "do-not-disturb-enabled": "Nie przeszkadzać", diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index 7cefafb7e..930b203a7 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -583,6 +583,10 @@ "system": "Sistema", "webbrowser": "Navegador web" }, + "date-filter-all-time": "Todo o tempo", + "date-filter-previous-7-days": "Últimos 7 dias", + "date-filter-today": "Hoje", + "date-filter-yesterday": "Ontem", "providers": { "applications": "Aplicativos", "calculator": "Calculadora", @@ -1856,6 +1860,7 @@ "click-to-stop-recording": "Gravador de tela (parar gravação)", "collapse": "Recolher barra lateral", "copy-address": "Copiar endereço", + "date-filter": "Filtro de data", "delete-notification": "Excluir notificação", "dismiss-notification": "Descartar notificação", "do-not-disturb-enabled": "Não perturbe", diff --git a/Assets/Translations/ru.json b/Assets/Translations/ru.json index 222fb63a4..f8389e9fc 100644 --- a/Assets/Translations/ru.json +++ b/Assets/Translations/ru.json @@ -583,6 +583,10 @@ "system": "Система", "webbrowser": "Веб-браузер" }, + "date-filter-all-time": "Все время", + "date-filter-previous-7-days": "Последние 7 дней", + "date-filter-today": "Сегодня", + "date-filter-yesterday": "Вчера", "providers": { "applications": "Приложения", "calculator": "Калькулятор", @@ -1856,6 +1860,7 @@ "click-to-stop-recording": "Запись экрана (остановить запись)", "collapse": "Свернуть боковую панель", "copy-address": "Копировать адрес", + "date-filter": "Фильтр по дате", "delete-notification": "Удалить уведомление", "dismiss-notification": "Отклонить уведомление", "do-not-disturb-enabled": "Не беспокоить", diff --git a/Assets/Translations/sv.json b/Assets/Translations/sv.json index 5df68e124..df64a0653 100644 --- a/Assets/Translations/sv.json +++ b/Assets/Translations/sv.json @@ -583,6 +583,10 @@ "system": "System", "webbrowser": "Webbläsare" }, + "date-filter-all-time": "Hela tiden", + "date-filter-previous-7-days": "Senaste 7 dagarna", + "date-filter-today": "Idag", + "date-filter-yesterday": "Igår", "providers": { "applications": "Applikationer", "calculator": "Kalkylator", @@ -1856,6 +1860,7 @@ "click-to-stop-recording": "Skärminspelare (stoppa inspelning)", "collapse": "Dölj sidofält", "copy-address": "Kopiera adress", + "date-filter": "Datumfilter", "delete-notification": "Ta bort avisering", "dismiss-notification": "Avfärda avisering", "do-not-disturb-enabled": "Stör inte", diff --git a/Assets/Translations/tr.json b/Assets/Translations/tr.json index 97508da72..7d401b378 100644 --- a/Assets/Translations/tr.json +++ b/Assets/Translations/tr.json @@ -583,6 +583,10 @@ "system": "Sistem", "webbrowser": "Tarayıcı" }, + "date-filter-all-time": "Tüm Zamanlar", + "date-filter-previous-7-days": "Son 7 Gün", + "date-filter-today": "Bugün", + "date-filter-yesterday": "Dün", "providers": { "applications": "Uygulamalar", "calculator": "Hesap makinesi", @@ -1856,6 +1860,7 @@ "click-to-stop-recording": "Ekran kaydedici (kaydı durdur)", "collapse": "Kenar çubuğunu daralt", "copy-address": "Adresi kopyala", + "date-filter": "Tarih filtresi", "delete-notification": "Bildiriyi sil", "dismiss-notification": "Bildirimi kapat", "do-not-disturb-enabled": "Rahatsız etme", diff --git a/Assets/Translations/uk-UA.json b/Assets/Translations/uk-UA.json index 0af5f5073..1433743f9 100644 --- a/Assets/Translations/uk-UA.json +++ b/Assets/Translations/uk-UA.json @@ -583,6 +583,10 @@ "system": "Система", "webbrowser": "Веб-браузер" }, + "date-filter-all-time": "Весь час", + "date-filter-previous-7-days": "Останні 7 днів", + "date-filter-today": "Сьогодні", + "date-filter-yesterday": "Вчора", "providers": { "applications": "Застосунки", "calculator": "Калькулятор", @@ -1856,6 +1860,7 @@ "click-to-stop-recording": "Запис екрана (зупинити запис)", "collapse": "Згорнути бічну панель", "copy-address": "Копіювати адресу", + "date-filter": "Фільтр за датою", "delete-notification": "Видалити сповіщення", "dismiss-notification": "Відхилити сповіщення", "do-not-disturb-enabled": "Не турбувати", diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index 7a795095f..5890027e1 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -583,6 +583,10 @@ "system": "系统", "webbrowser": "网页浏览器" }, + "date-filter-all-time": "所有时间", + "date-filter-previous-7-days": "过去 7 天", + "date-filter-today": "今天", + "date-filter-yesterday": "昨天", "providers": { "applications": "应用程序", "calculator": "计算器", @@ -1856,6 +1860,7 @@ "click-to-stop-recording": "屏幕录制器(停止录制)", "collapse": "折叠侧边栏", "copy-address": "复制地址", + "date-filter": "日期过滤", "delete-notification": "删除通知", "dismiss-notification": "关闭通知", "do-not-disturb-enabled": "勿扰模式", diff --git a/Assets/Translations/zh-TW.json b/Assets/Translations/zh-TW.json index ecf239801..060f445cb 100644 --- a/Assets/Translations/zh-TW.json +++ b/Assets/Translations/zh-TW.json @@ -583,6 +583,10 @@ "system": "系統", "webbrowser": "瀏覽器" }, + "date-filter-all-time": "所有時間", + "date-filter-previous-7-days": "過去 7 天", + "date-filter-today": "今天", + "date-filter-yesterday": "昨天", "providers": { "applications": "應用程式", "calculator": "計算機", @@ -1856,6 +1860,7 @@ "click-to-stop-recording": "螢幕錄影 (停止錄製)", "collapse": "收起側邊欄", "copy-address": "複製位址", + "date-filter": "日期過濾", "delete-notification": "刪除通知", "dismiss-notification": "關閉通知", "do-not-disturb-enabled": "勿擾模式", diff --git a/Modules/Panels/Launcher/Providers/ClipboardProvider.qml b/Modules/Panels/Launcher/Providers/ClipboardProvider.qml index 3d1c31ea9..8f8bdde43 100644 --- a/Modules/Panels/Launcher/Providers/ClipboardProvider.qml +++ b/Modules/Panels/Launcher/Providers/ClipboardProvider.qml @@ -43,24 +43,24 @@ Item { property string dateFilter: "all" property var availableDateFilters: [ { - "label": "All Time", + get label() { return I18n.tr("launcher.date-filter-all-time"); }, "action": "all", - "icon": iconMode === "tabler" ? "calendar" : "x-office-calendar" + get icon() { return iconMode === "tabler" ? "calendar" : "x-office-calendar"; } }, { - "label": "Today", + get label() { return I18n.tr("launcher.date-filter-today"); }, "action": "today", - "icon": iconMode === "tabler" ? "calendar-event" : "view-calendar-timeline" + get icon() { return iconMode === "tabler" ? "calendar-event" : "view-calendar-timeline"; } }, { - "label": "Yesterday", + get label() { return I18n.tr("launcher.date-filter-yesterday"); }, "action": "yesterday", - "icon": iconMode === "tabler" ? "calendar-time" : "view-calendar" + get icon() { return iconMode === "tabler" ? "calendar-time" : "view-calendar"; } }, { - "label": "Previous 7 Days", + get label() { return I18n.tr("launcher.date-filter-previous-7-days"); }, "action": "week", - "icon": iconMode === "tabler" ? "calendar-week" : "view-calendar-week" + get icon() { return iconMode === "tabler" ? "calendar-week" : "view-calendar-week"; } } ] @@ -304,13 +304,13 @@ Item { // Check date group logic if (headersEnabled && !searchTerm && root.selectedCategory === "All" && root.dateFilter === "all") { - let groupName = "Older"; + let groupName = I18n.tr("launcher.date-filter-all-time"); if (firstSeen >= todayStartTs) { - groupName = "Today"; + groupName = I18n.tr("launcher.date-filter-today"); } else if (firstSeen >= yesterdayStartTs) { - groupName = "Yesterday"; + groupName = I18n.tr("launcher.date-filter-yesterday"); } else if (firstSeen >= todayStartTs - (86400 * 7)) { - groupName = "Previous 7 Days"; + groupName = I18n.tr("launcher.date-filter-previous-7-days"); } if (groupName !== currentGroup) { From 67ffbb6f2739f2d48f327879ee1c59788b99e290 Mon Sep 17 00:00:00 2001 From: "Braian A. Diez" Date: Sat, 28 Feb 2026 13:11:56 -0300 Subject: [PATCH 3/5] fix: improve the code regex Signed-off-by: Braian A. Diez --- .../Launcher/Providers/ClipboardProvider.qml | 34 ++++++++++++++----- Services/Keyboard/ClipboardService.qml | 8 ++++- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/Modules/Panels/Launcher/Providers/ClipboardProvider.qml b/Modules/Panels/Launcher/Providers/ClipboardProvider.qml index 8f8bdde43..a942f8e31 100644 --- a/Modules/Panels/Launcher/Providers/ClipboardProvider.qml +++ b/Modules/Panels/Launcher/Providers/ClipboardProvider.qml @@ -39,28 +39,44 @@ Item { } // Date Filtering - property bool hasDateFilter: true + property bool hasDateFilter: Settings.data.appLauncher.enableClipboardDateHeaders property string dateFilter: "all" property var availableDateFilters: [ { - get label() { return I18n.tr("launcher.date-filter-all-time"); }, + get label() { + return I18n.tr("launcher.date-filter-all-time"); + }, "action": "all", - get icon() { return iconMode === "tabler" ? "calendar" : "x-office-calendar"; } + get icon() { + return iconMode === "tabler" ? "calendar" : "x-office-calendar"; + } }, { - get label() { return I18n.tr("launcher.date-filter-today"); }, + get label() { + return I18n.tr("launcher.date-filter-today"); + }, "action": "today", - get icon() { return iconMode === "tabler" ? "calendar-event" : "view-calendar-timeline"; } + get icon() { + return iconMode === "tabler" ? "calendar-event" : "view-calendar-timeline"; + } }, { - get label() { return I18n.tr("launcher.date-filter-yesterday"); }, + get label() { + return I18n.tr("launcher.date-filter-yesterday"); + }, "action": "yesterday", - get icon() { return iconMode === "tabler" ? "calendar-time" : "view-calendar"; } + get icon() { + return iconMode === "tabler" ? "calendar-time" : "view-calendar"; + } }, { - get label() { return I18n.tr("launcher.date-filter-previous-7-days"); }, + get label() { + return I18n.tr("launcher.date-filter-previous-7-days"); + }, "action": "week", - get icon() { return iconMode === "tabler" ? "calendar-week" : "view-calendar-week"; } + get icon() { + return iconMode === "tabler" ? "calendar-week" : "view-calendar-week"; + } } ] diff --git a/Services/Keyboard/ClipboardService.qml b/Services/Keyboard/ClipboardService.qml index f9fb2f772..1c66ec8b2 100644 --- a/Services/Keyboard/ClipboardService.qml +++ b/Services/Keyboard/ClipboardService.qml @@ -160,7 +160,13 @@ Singleton { contentType = "link"; } else if (/^(\/|~\/|file:\/\/)/i.test(t) && !t.startsWith('//') && !t.includes('\n')) { contentType = "file"; - } else if ((t.includes('{') && t.includes('}') && (t.includes(';') || t.includes('='))) || t.includes('') || t.includes('function') || t.includes('import ') || t.includes('export ')) { + } else if ( + (t.includes('{') && t.includes('}') && (t.includes(';') || t.includes('='))) || + t.includes('') || t.includes('=>') || t.includes('===') || t.includes('!==') || t.includes('::') || t.includes('->') || + /^(?:const|let|var|function|class|struct|interface|type|enum|import|export|func|fn|pub|def|using|namespace|property|public|private|protected)\b/i.test(t) || + /^(?:#include|#define|#\[|@|\/\/|\/\*|<\?| Date: Thu, 12 Mar 2026 21:10:31 -0300 Subject: [PATCH 4/5] chore(clipboard): cleanup request Signed-off-by: Braian A. Diez --- Assets/Translations/de.json | 5 - Assets/Translations/es.json | 5 - Assets/Translations/fr.json | 5 - Assets/Translations/hu.json | 5 - Assets/Translations/it.json | 5 - Assets/Translations/ja.json | 5 - Assets/Translations/ko-KR.json | 5 - Assets/Translations/ku.json | 5 - Assets/Translations/nl.json | 5 - Assets/Translations/nn-HN.json | 9 - Assets/Translations/nn-NO.json | 5 - Assets/Translations/pl.json | 5 - Assets/Translations/pt.json | 5 - Assets/Translations/ru.json | 5 - Assets/Translations/sv.json | 5 - Assets/Translations/tr.json | 5 - Assets/Translations/uk-UA.json | 5 - Assets/Translations/zh-CN.json | 5 - Assets/Translations/zh-TW.json | 5 - .../Launcher/Providers/ClipboardProvider.qml | 994 +++++++++--------- .../Tabs/Launcher/ClipboardSubTab.qml | 174 +-- Services/Keyboard/ClipboardService.qml | 951 +++++++++-------- 22 files changed, 1057 insertions(+), 1161 deletions(-) diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index 5e5d0f38c..71475b772 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -599,10 +599,6 @@ "system": "System", "webbrowser": "Webbrowser" }, - "date-filter-all-time": "Gesamte Zeit", - "date-filter-previous-7-days": "Letzte 7 Tage", - "date-filter-today": "Heute", - "date-filter-yesterday": "Gestern", "providers": { "applications": "Anwendungen", "calculator": "Rechner", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "Bildschirmrekorder (Aufnahme stoppen)", "collapse": "Seitenleiste einklappen", "copy-address": "Adresse kopieren", - "date-filter": "Datumsfilter", "delete-notification": "Benachrichtigung löschen", "dismiss-notification": "Benachrichtigung schließen", "do-not-disturb-enabled": "Nicht stören", diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index a99a5f4e9..ae374c781 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -599,10 +599,6 @@ "system": "Sistema", "webbrowser": "Navegador web" }, - "date-filter-all-time": "Todo el tiempo", - "date-filter-previous-7-days": "Últimos 7 días", - "date-filter-today": "Hoy", - "date-filter-yesterday": "Ayer", "providers": { "applications": "Aplicaciones", "calculator": "Calculadora", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "Grabadora de pantalla (detener grabación)", "collapse": "Colapsar barra lateral", "copy-address": "Copiar dirección", - "date-filter": "Filtro de fecha", "delete-notification": "Eliminar notificación", "dismiss-notification": "Descartar notificación", "do-not-disturb-enabled": "No molestar", diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index 3a221c91d..cc15f4f27 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -599,10 +599,6 @@ "system": "Système", "webbrowser": "Navigateur web" }, - "date-filter-all-time": "Tout le temps", - "date-filter-previous-7-days": "7 derniers jours", - "date-filter-today": "Aujourd'hui", - "date-filter-yesterday": "Hier", "providers": { "applications": "Applications", "calculator": "Calculatrice", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "Enregistreur d'écran (arrêter l'enregistrement)", "collapse": "Réduire la barre latérale", "copy-address": "Copier l'adresse", - "date-filter": "Filtre de date", "delete-notification": "Supprimer la notification", "dismiss-notification": "Ignorer la notification", "do-not-disturb-enabled": "Ne pas déranger", diff --git a/Assets/Translations/hu.json b/Assets/Translations/hu.json index aa24195fc..8136d287b 100644 --- a/Assets/Translations/hu.json +++ b/Assets/Translations/hu.json @@ -599,10 +599,6 @@ "system": "Rendszer", "webbrowser": "Webböngésző" }, - "date-filter-all-time": "Minden idő", - "date-filter-previous-7-days": "Elmúlt 7 nap", - "date-filter-today": "Ma", - "date-filter-yesterday": "Tegnap", "providers": { "applications": "Alkalmazások", "calculator": "Számológép", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "Képernyőfelvevő (felvétel leállítása)", "collapse": "Oldalsáv összecsukása", "copy-address": "Cím másolása", - "date-filter": "Dátum szűrő", "delete-notification": "Értesítés törlése", "dismiss-notification": "Értesítés elvetése", "do-not-disturb-enabled": "Ne zavarjanak", diff --git a/Assets/Translations/it.json b/Assets/Translations/it.json index 40fdd6882..fc99560c3 100644 --- a/Assets/Translations/it.json +++ b/Assets/Translations/it.json @@ -599,10 +599,6 @@ "system": "Sistema", "webbrowser": "Browser web" }, - "date-filter-all-time": "Tutto il tempo", - "date-filter-previous-7-days": "Ultimi 7 giorni", - "date-filter-today": "Oggi", - "date-filter-yesterday": "Ieri", "providers": { "applications": "Applicazioni", "calculator": "Calcolatrice", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "Registratore schermo (ferma registrazione)", "collapse": "Comprimi barra laterale", "copy-address": "Copia indirizzo", - "date-filter": "Filtro data", "delete-notification": "Elimina notifica", "dismiss-notification": "Ignora notifica", "do-not-disturb-enabled": "Non disturbare", diff --git a/Assets/Translations/ja.json b/Assets/Translations/ja.json index 5c860c0e3..63bf6758d 100644 --- a/Assets/Translations/ja.json +++ b/Assets/Translations/ja.json @@ -599,10 +599,6 @@ "system": "システム", "webbrowser": "ウェブブラウザ" }, - "date-filter-all-time": "すべての期間", - "date-filter-previous-7-days": "過去 7 日間", - "date-filter-today": "今日", - "date-filter-yesterday": "昨日", "providers": { "applications": "アプリケーション", "calculator": "電卓", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "画面録画(録画停止)", "collapse": "サイドバーを折りたたむ", "copy-address": "アドレスをコピー", - "date-filter": "日付フィルター", "delete-notification": "通知を削除", "dismiss-notification": "通知を閉じる", "do-not-disturb-enabled": "おやすみモード", diff --git a/Assets/Translations/ko-KR.json b/Assets/Translations/ko-KR.json index fce4865d6..7c57dbd75 100644 --- a/Assets/Translations/ko-KR.json +++ b/Assets/Translations/ko-KR.json @@ -599,10 +599,6 @@ "system": "시스템", "webbrowser": "웹 브라우저" }, - "date-filter-all-time": "모든 시간", - "date-filter-previous-7-days": "최근 7일", - "date-filter-today": "오늘", - "date-filter-yesterday": "어제", "providers": { "applications": "애플리케이션", "calculator": "계산기", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "화면 녹화 (녹화 중지)", "collapse": "사이드바 접기", "copy-address": "주소 복사", - "date-filter": "날짜 필터", "delete-notification": "알림 삭제", "dismiss-notification": "알림 닫기", "do-not-disturb-enabled": "방해 금지 모드", diff --git a/Assets/Translations/ku.json b/Assets/Translations/ku.json index 4591edd90..49332543c 100644 --- a/Assets/Translations/ku.json +++ b/Assets/Translations/ku.json @@ -545,10 +545,6 @@ "system": "Pergal", "webbrowser": "Geroka tevnê" }, - "date-filter-all-time": "Hemû Dem", - "date-filter-previous-7-days": "7 Rojên Borî", - "date-filter-today": "Îro", - "date-filter-yesterday": "Duh", "providers": { "applications": "Sepan", "calculator": "Jimarkar", @@ -1654,7 +1650,6 @@ "click-to-stop-recording": "Tomarkarê dîmenderê (tomarkirinê rawestîne)", "collapse": "Darika kêlelê veşêre", "copy-address": "Navnîşana kopî bike", - "date-filter": "Parzûna dîrokê", "delete-notification": "Agahdariyê jê bibe", "do-not-disturb-enabled": "Dengê dernexîne", "expand": "Darika kêlekê fereh bike", diff --git a/Assets/Translations/nl.json b/Assets/Translations/nl.json index ac9bd115e..a4d41d6cf 100644 --- a/Assets/Translations/nl.json +++ b/Assets/Translations/nl.json @@ -599,10 +599,6 @@ "system": "Systeem", "webbrowser": "Webbrowser" }, - "date-filter-all-time": "Alle tijd", - "date-filter-previous-7-days": "Afgelopen 7 dagen", - "date-filter-today": "Vandaag", - "date-filter-yesterday": "Gisteren", "providers": { "applications": "Applicaties", "calculator": "Rekenmachine", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "Schermrecorder (opname stoppen)", "collapse": "Zijbalk inklappen", "copy-address": "Adres kopiëren", - "date-filter": "Datumfilter", "delete-notification": "Melding verwijderen", "dismiss-notification": "Melding sluiten", "do-not-disturb-enabled": "Niet storen", diff --git a/Assets/Translations/nn-HN.json b/Assets/Translations/nn-HN.json index e73a5e129..de1f34d7c 100644 --- a/Assets/Translations/nn-HN.json +++ b/Assets/Translations/nn-HN.json @@ -224,12 +224,6 @@ "brightness": "Ljosstyrke", "select-color-description": "Lita med hamlitene for tyngd." }, - "launcher": { - "date-filter-all-time": "Heile tida", - "date-filter-previous-7-days": "Siste 7 dagar", - "date-filter-today": "I dag", - "date-filter-yesterday": "I går" - }, "options": { "bar": { "density-compact": "Klembd" @@ -243,8 +237,5 @@ }, "system": { "welcome-back": "Velkomen attende," - }, - "tooltips": { - "date-filter": "Dato-filter" } } diff --git a/Assets/Translations/nn-NO.json b/Assets/Translations/nn-NO.json index 39a0a688f..84f9fe1e9 100644 --- a/Assets/Translations/nn-NO.json +++ b/Assets/Translations/nn-NO.json @@ -576,10 +576,6 @@ "system": "System", "webbrowser": "Nettlesar" }, - "date-filter-all-time": "Heile tida", - "date-filter-previous-7-days": "Siste 7 dagar", - "date-filter-today": "I dag", - "date-filter-yesterday": "I går", "providers": { "applications": "Applikasjonar", "calculator": "Kalkulator", @@ -1792,7 +1788,6 @@ "click-to-stop-recording": "Skjermopptak (stogg opptak)", "collapse": "Gøym sidestolpen", "copy-address": "Kopier adresse", - "date-filter": "Dato-filter", "delete-notification": "Slett varsel", "dismiss-notification": "Avvis varsel", "do-not-disturb-enabled": "Ikkje forstyrr", diff --git a/Assets/Translations/pl.json b/Assets/Translations/pl.json index 0f31bb9e3..3f10b9cc7 100644 --- a/Assets/Translations/pl.json +++ b/Assets/Translations/pl.json @@ -599,10 +599,6 @@ "system": "System", "webbrowser": "Przeglądarka www" }, - "date-filter-all-time": "Cały czas", - "date-filter-previous-7-days": "Ostatnie 7 dni", - "date-filter-today": "Dzisiaj", - "date-filter-yesterday": "Wczoraj", "providers": { "applications": "Aplikacje", "calculator": "Kalkulator", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "Nagrywanie ekranu (stop)", "collapse": "Zwiń pasek boczny", "copy-address": "Kopiuj adres", - "date-filter": "Filtr daty", "delete-notification": "Usuń powiadomienie", "dismiss-notification": "Odrzuć powiadomienie", "do-not-disturb-enabled": "Nie przeszkadzać", diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index ba929625a..7cb2a338c 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -599,10 +599,6 @@ "system": "Sistema", "webbrowser": "Navegador web" }, - "date-filter-all-time": "Todo o tempo", - "date-filter-previous-7-days": "Últimos 7 dias", - "date-filter-today": "Hoje", - "date-filter-yesterday": "Ontem", "providers": { "applications": "Aplicativos", "calculator": "Calculadora", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "Gravador de tela (parar gravação)", "collapse": "Recolher barra lateral", "copy-address": "Copiar endereço", - "date-filter": "Filtro de data", "delete-notification": "Excluir notificação", "dismiss-notification": "Descartar notificação", "do-not-disturb-enabled": "Não perturbe", diff --git a/Assets/Translations/ru.json b/Assets/Translations/ru.json index 8e2faac06..96b9dcdf8 100644 --- a/Assets/Translations/ru.json +++ b/Assets/Translations/ru.json @@ -599,10 +599,6 @@ "system": "Система", "webbrowser": "Веб-браузер" }, - "date-filter-all-time": "Все время", - "date-filter-previous-7-days": "Последние 7 дней", - "date-filter-today": "Сегодня", - "date-filter-yesterday": "Вчера", "providers": { "applications": "Приложения", "calculator": "Калькулятор", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "Запись экрана (остановить запись)", "collapse": "Свернуть боковую панель", "copy-address": "Копировать адрес", - "date-filter": "Фильтр по дате", "delete-notification": "Удалить уведомление", "dismiss-notification": "Отклонить уведомление", "do-not-disturb-enabled": "Не беспокоить", diff --git a/Assets/Translations/sv.json b/Assets/Translations/sv.json index 537204da1..15481362b 100644 --- a/Assets/Translations/sv.json +++ b/Assets/Translations/sv.json @@ -599,10 +599,6 @@ "system": "System", "webbrowser": "Webbläsare" }, - "date-filter-all-time": "Hela tiden", - "date-filter-previous-7-days": "Senaste 7 dagarna", - "date-filter-today": "Idag", - "date-filter-yesterday": "Igår", "providers": { "applications": "Applikationer", "calculator": "Kalkylator", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "Skärminspelare (stoppa inspelning)", "collapse": "Dölj sidofält", "copy-address": "Kopiera adress", - "date-filter": "Datumfilter", "delete-notification": "Ta bort avisering", "dismiss-notification": "Avfärda avisering", "do-not-disturb-enabled": "Stör inte", diff --git a/Assets/Translations/tr.json b/Assets/Translations/tr.json index b97b65ee1..9f15abad2 100644 --- a/Assets/Translations/tr.json +++ b/Assets/Translations/tr.json @@ -599,10 +599,6 @@ "system": "Sistem", "webbrowser": "Tarayıcı" }, - "date-filter-all-time": "Tüm Zamanlar", - "date-filter-previous-7-days": "Son 7 Gün", - "date-filter-today": "Bugün", - "date-filter-yesterday": "Dün", "providers": { "applications": "Uygulamalar", "calculator": "Hesap makinesi", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "Ekran kaydedici (kaydı durdur)", "collapse": "Kenar çubuğunu daralt", "copy-address": "Adresi kopyala", - "date-filter": "Tarih filtresi", "delete-notification": "Bildiriyi sil", "dismiss-notification": "Bildirimi kapat", "do-not-disturb-enabled": "Rahatsız etme", diff --git a/Assets/Translations/uk-UA.json b/Assets/Translations/uk-UA.json index 98c34dbbb..1adb21bb7 100644 --- a/Assets/Translations/uk-UA.json +++ b/Assets/Translations/uk-UA.json @@ -599,10 +599,6 @@ "system": "Система", "webbrowser": "Веб-браузер" }, - "date-filter-all-time": "Весь час", - "date-filter-previous-7-days": "Останні 7 днів", - "date-filter-today": "Сьогодні", - "date-filter-yesterday": "Вчора", "providers": { "applications": "Застосунки", "calculator": "Калькулятор", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "Запис екрана (зупинити запис)", "collapse": "Згорнути бічну панель", "copy-address": "Копіювати адресу", - "date-filter": "Фільтр за датою", "delete-notification": "Видалити сповіщення", "dismiss-notification": "Відхилити сповіщення", "do-not-disturb-enabled": "Не турбувати", diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index e1e8854bb..9e181b10c 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -599,10 +599,6 @@ "system": "系统", "webbrowser": "网页浏览器" }, - "date-filter-all-time": "所有时间", - "date-filter-previous-7-days": "过去 7 天", - "date-filter-today": "今天", - "date-filter-yesterday": "昨天", "providers": { "applications": "应用程序", "calculator": "计算器", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "屏幕录制器(停止录制)", "collapse": "折叠侧边栏", "copy-address": "复制地址", - "date-filter": "日期过滤", "delete-notification": "删除通知", "dismiss-notification": "关闭通知", "do-not-disturb-enabled": "勿扰模式", diff --git a/Assets/Translations/zh-TW.json b/Assets/Translations/zh-TW.json index d37dff40a..f948273a5 100644 --- a/Assets/Translations/zh-TW.json +++ b/Assets/Translations/zh-TW.json @@ -599,10 +599,6 @@ "system": "系統", "webbrowser": "瀏覽器" }, - "date-filter-all-time": "所有時間", - "date-filter-previous-7-days": "過去 7 天", - "date-filter-today": "今天", - "date-filter-yesterday": "昨天", "providers": { "applications": "應用程式", "calculator": "計算機", @@ -1949,7 +1945,6 @@ "click-to-stop-recording": "螢幕錄影 (停止錄製)", "collapse": "收起側邊欄", "copy-address": "複製位址", - "date-filter": "日期過濾", "delete-notification": "刪除通知", "dismiss-notification": "關閉通知", "do-not-disturb-enabled": "勿擾模式", diff --git a/Modules/Panels/Launcher/Providers/ClipboardProvider.qml b/Modules/Panels/Launcher/Providers/ClipboardProvider.qml index a942f8e31..5eee31fdc 100644 --- a/Modules/Panels/Launcher/Providers/ClipboardProvider.qml +++ b/Modules/Panels/Launcher/Providers/ClipboardProvider.qml @@ -5,540 +5,540 @@ import qs.Services.Keyboard import qs.Services.Noctalia Item { - id: root + id: root - // Provider metadata - property string name: I18n.tr("launcher.providers.clipboard") - property var launcher: null - property string iconMode: Settings.data.appLauncher.iconMode - property string supportedLayouts: "list" // List view for clipboard content - property bool wrapNavigation: false // Don't wrap at end of list + // Provider metadata + property string name: I18n.tr("launcher.providers.clipboard") + property var launcher: null + property string iconMode: Settings.data.appLauncher.iconMode + property string supportedLayouts: "list" // List view for clipboard content + property bool wrapNavigation: false // Don't wrap at end of list - // Provider capabilities - property bool handleSearch: false // Don't handle regular search + // Provider capabilities + property bool handleSearch: false // Don't handle regular search - // Preview support - property bool hasPreview: Settings.data.appLauncher.enableClipPreview - property string previewComponentPath: "./ClipboardPreview.qml" + // Preview support + property bool hasPreview: Settings.data.appLauncher.enableClipPreview + property string previewComponentPath: "./ClipboardPreview.qml" - // Image handling - expose revision for reactive updates in delegates - readonly property int imageRevision: ClipboardService.revision + // Image handling - expose revision for reactive updates in delegates + readonly property int imageRevision: ClipboardService.revision - // Categories - property var availableCategories: Settings.data.appLauncher.enableClipboardChips ? ["All", "Images", "Links", "Files", "Code", "Colors"] : [] - property bool showsCategories: Settings.data.appLauncher.enableClipboardChips - property string selectedCategory: "All" + // Categories + property var availableCategories: Settings.data.appLauncher.enableClipboardChips ? ["All", "Images", "Links", "Files", "Code", "Colors"] : [] + property bool showsCategories: Settings.data.appLauncher.enableClipboardChips + property string selectedCategory: "All" - function selectCategory(cat) { - if (selectedCategory !== cat) { - selectedCategory = cat; - if (launcher) { - launcher.updateResults(); - } - } + function selectCategory(cat) { + if (selectedCategory !== cat) { + selectedCategory = cat; + if (launcher) { + launcher.updateResults(); + } } + } - // Date Filtering - property bool hasDateFilter: Settings.data.appLauncher.enableClipboardDateHeaders - property string dateFilter: "all" - property var availableDateFilters: [ - { - get label() { - return I18n.tr("launcher.date-filter-all-time"); - }, - "action": "all", - get icon() { - return iconMode === "tabler" ? "calendar" : "x-office-calendar"; - } - }, - { - get label() { - return I18n.tr("launcher.date-filter-today"); - }, - "action": "today", - get icon() { - return iconMode === "tabler" ? "calendar-event" : "view-calendar-timeline"; - } - }, - { - get label() { - return I18n.tr("launcher.date-filter-yesterday"); - }, - "action": "yesterday", - get icon() { - return iconMode === "tabler" ? "calendar-time" : "view-calendar"; - } - }, - { - get label() { - return I18n.tr("launcher.date-filter-previous-7-days"); - }, - "action": "week", - get icon() { - return iconMode === "tabler" ? "calendar-week" : "view-calendar-week"; - } - } - ] - - function selectDateFilter(filter) { - if (dateFilter !== filter) { - dateFilter = filter; - if (launcher) { - launcher.updateResults(); - } - } + // Date Filtering + property bool hasDateFilter: Settings.data.appLauncher.enableClipboardDateHeaders + property string dateFilter: "all" + property var availableDateFilters: [ + { + get label() { + return I18n.tr("launcher.date-filter-all-time"); + }, + "action": "all", + get icon() { + return iconMode === "tabler" ? "calendar" : "x-office-calendar"; + } + }, + { + get label() { + return I18n.tr("launcher.date-filter-today"); + }, + "action": "today", + get icon() { + return iconMode === "tabler" ? "calendar-event" : "view-calendar-timeline"; + } + }, + { + get label() { + return I18n.tr("launcher.date-filter-yesterday"); + }, + "action": "yesterday", + get icon() { + return iconMode === "tabler" ? "calendar-time" : "view-calendar"; + } + }, + { + get label() { + return I18n.tr("launcher.date-filter-previous-7-days"); + }, + "action": "week", + get icon() { + return iconMode === "tabler" ? "calendar-week" : "view-calendar-week"; + } } + ] - property var categoryIcons: { - "All": iconMode === "tabler" ? "border-all" : "view-grid", - "Images": iconMode === "tabler" ? "photo" : "image", - "Links": iconMode === "tabler" ? "link" : "insert-link", - "Files": iconMode === "tabler" ? "file" : "text-x-generic", - "Code": iconMode === "tabler" ? "code" : "text-x-script", - "Colors": iconMode === "tabler" ? "palette" : "color-picker" + function selectDateFilter(filter) { + if (dateFilter !== filter) { + dateFilter = filter; + if (launcher) { + launcher.updateResults(); + } } + } - // Internal state - property bool isWaitingForData: false - property bool gotResults: false - property string lastSearchText: "" + property var categoryIcons: { + "All": iconMode === "tabler" ? "border-all" : "view-grid", + "Images": iconMode === "tabler" ? "photo" : "image", + "Links": iconMode === "tabler" ? "link" : "insert-link", + "Files": iconMode === "tabler" ? "file" : "text-x-generic", + "Code": iconMode === "tabler" ? "code" : "text-x-script", + "Colors": iconMode === "tabler" ? "palette" : "color-picker" + } - // Listen for clipboard data updates - Connections { - target: ClipboardService - function onListCompleted() { - if (gotResults && (lastSearchText === searchText)) { - // Do not update results after the first fetch. - // This will avoid the list resetting every 2seconds when the service updates. - return; - } - // Refresh results if we're waiting for data or if clipboard plugin is active - if (isWaitingForData || (launcher && launcher.searchText.startsWith(">clip"))) { - isWaitingForData = false; - gotResults = true; - if (launcher) { - launcher.updateResults(); - } - } - } - function onActiveChanged() { - // When active state changes (e.g. dependency check completes), refresh results - if (ClipboardService.active && launcher && launcher.searchText.startsWith(">clip")) { - isWaitingForData = true; - gotResults = false; - ClipboardService.list(100); - } + // Internal state + property bool isWaitingForData: false + property bool gotResults: false + property string lastSearchText: "" + + // Listen for clipboard data updates + Connections { + target: ClipboardService + function onListCompleted() { + if (gotResults && (lastSearchText === searchText)) { + // Do not update results after the first fetch. + // This will avoid the list resetting every 2seconds when the service updates. + return; + } + // Refresh results if we're waiting for data or if clipboard plugin is active + if (isWaitingForData || (launcher && launcher.searchText.startsWith(">clip"))) { + isWaitingForData = false; + gotResults = true; + if (launcher) { + launcher.updateResults(); } + } } - - // Initialize provider - function init() { - Logger.d("ClipboardProvider", "Initialized"); - // Pre-load clipboard data if service is active - if (ClipboardService.active) { - ClipboardService.list(100); - } - } - - // Called when launcher opens - function onOpened() { + function onActiveChanged() { + // When active state changes (e.g. dependency check completes), refresh results + if (ClipboardService.active && launcher && launcher.searchText.startsWith(">clip")) { isWaitingForData = true; gotResults = false; - lastSearchText = ""; - - // Refresh clipboard history when launcher opens - if (ClipboardService.active) { - ClipboardService.list(100); - } + ClipboardService.list(100); + } } + } - // Check if this provider handles the command - function handleCommand(searchText) { - return searchText.startsWith(">clip"); + // Initialize provider + function init() { + Logger.d("ClipboardProvider", "Initialized"); + // Pre-load clipboard data if service is active + if (ClipboardService.active) { + ClipboardService.list(100); } + } - // Return available commands when user types ">" - function commands() { - return [ - { - "name": ">clip", - "description": I18n.tr("launcher.providers.clipboard-search-description"), - "icon": iconMode === "tabler" ? "clipboard" : "diodon", - "isTablerIcon": true, - "isImage": false, - "onActivate": function () { - launcher.setSearchText(">clip "); - } - }, - { - "name": ">clip clear", - "description": I18n.tr("launcher.providers.clipboard-clear-description"), - "icon": iconMode === "tabler" ? "trash" : "user-trash", - "isTablerIcon": true, - "isImage": false, - "onActivate": function () { - ClipboardService.wipeAll(); - launcher.close(); - } - } - ]; + // Called when launcher opens + function onOpened() { + isWaitingForData = true; + gotResults = false; + lastSearchText = ""; + + // Refresh clipboard history when launcher opens + if (ClipboardService.active) { + ClipboardService.list(100); } + } - // Get search results - function getResults(searchText) { - if (!searchText.startsWith(">clip")) { - return []; - } + // Check if this provider handles the command + function handleCommand(searchText) { + return searchText.startsWith(">clip"); + } - lastSearchText = searchText; - const results = []; - const query = searchText.slice(5).trim(); - - // Check if clipboard service is not active - if (!ClipboardService.active) { - // If dependency check hasn't completed yet, show loading instead of disabled - if (!ClipboardService.dependencyChecked) { - return [ - { - "name": I18n.tr("launcher.providers.clipboard-loading"), - "description": I18n.tr("launcher.providers.emoji-loading-description"), - "icon": iconMode === "tabler" ? "refresh" : "view-refresh", - "isTablerIcon": true, - "isImage": false, - "onActivate": function () {} - } - ]; - } - return [ - { - "name": I18n.tr("launcher.providers.clipboard-history-disabled"), - "description": I18n.tr("launcher.providers.clipboard-history-disabled-description"), - "icon": iconMode === "tabler" ? "refresh" : "view-refresh", - "isTablerIcon": true, - "isImage": false, - "onActivate": function () {} - } - ]; - } - - // Special command: clear - if (query === "clear") { - return [ - { - "name": I18n.tr("launcher.providers.clipboard-clear-history"), - "description": I18n.tr("launcher.providers.clipboard-clear-description-full"), - "icon": iconMode === "tabler" ? "trash" : "user-trash", - "isTablerIcon": true, - "isImage": false, - "onActivate": function () { - ClipboardService.wipeAll(); - launcher.close(); - } - } - ]; - } - - // Show loading state if data is being loaded - if (ClipboardService.loading || isWaitingForData) { - return [ - { - "name": I18n.tr("launcher.providers.clipboard-loading"), - "description": I18n.tr("launcher.providers.emoji-loading-description"), - "icon": iconMode === "tabler" ? "refresh" : "view-refresh", - "isTablerIcon": true, - "isImage": false, - "onActivate": function () {} - } - ]; - } - - // Get clipboard items - const items = ClipboardService.items || []; - - // If no items and we haven't tried loading yet, trigger a load - if (items.count === 0 && !ClipboardService.loading) { - isWaitingForData = true; - ClipboardService.list(100); - return [ - { - "name": I18n.tr("launcher.providers.clipboard-loading"), - "description": I18n.tr("launcher.providers.emoji-loading-description"), - "icon": iconMode === "tabler" ? "refresh" : "view-refresh", - "isTablerIcon": true, - "isImage": false, - "onActivate": function () {} - } - ]; - } - - // Search clipboard items - const searchTerm = query.toLowerCase(); - - // Date grouping trackers - const headersEnabled = Settings.data.appLauncher.enableClipboardDateHeaders; - const now = Date.now() / 1000; - const todayStart = new Date(); - todayStart.setHours(0, 0, 0, 0); - const todayStartTs = todayStart.getTime() / 1000; - const yesterdayStartTs = todayStartTs - 86400; - - let currentGroup = ""; - - // Filter and format results - items.forEach(function (item) { - // Category filter - if (Settings.data.appLauncher.enableClipboardChips && root.selectedCategory !== "All") { - const catMap = { - "Images": "image", - "Links": "link", - "Files": "file", - "Code": "code", - "Colors": "color" - }; - if (item.contentType !== catMap[root.selectedCategory]) { - return; - } - } - - const preview = (item.preview || "").toLowerCase(); - - // Skip if search term doesn't match - if (searchTerm && preview.indexOf(searchTerm) === -1) { - return; - } - - // Date Filter - const firstSeen = ClipboardService.firstSeenById[item.id] || now; - if (root.dateFilter !== "all") { - if (root.dateFilter === "today" && firstSeen < todayStartTs) - return; - if (root.dateFilter === "yesterday" && (firstSeen >= todayStartTs || firstSeen < yesterdayStartTs)) - return; - if (root.dateFilter === "week" && (firstSeen >= yesterdayStartTs || firstSeen < (todayStartTs - (86400 * 7)))) - return; - } - - // Check date group logic - if (headersEnabled && !searchTerm && root.selectedCategory === "All" && root.dateFilter === "all") { - let groupName = I18n.tr("launcher.date-filter-all-time"); - if (firstSeen >= todayStartTs) { - groupName = I18n.tr("launcher.date-filter-today"); - } else if (firstSeen >= yesterdayStartTs) { - groupName = I18n.tr("launcher.date-filter-yesterday"); - } else if (firstSeen >= todayStartTs - (86400 * 7)) { - groupName = I18n.tr("launcher.date-filter-previous-7-days"); - } - - if (groupName !== currentGroup) { - currentGroup = groupName; - results.push({ - "name": currentGroup, - "description": "", - "icon": iconMode === "tabler" ? "calendar" : "x-office-calendar", - "isTablerIcon": true, - "isImage": false, - "hideIcon": true, - "isHeader": true, - "clipboardId": "", - "onActivate": function () {} - }); - } - } - - // Format the result based on type - let entry; - if (item.isImage) { - entry = formatImageEntry(item); - } else { - entry = formatTextEntry(item); - } - - // Add activation handler - entry.onActivate = function () { - if (Settings.data.appLauncher.autoPasteClipboard) { - launcher.closeImmediately(); - Qt.callLater(() => { - ClipboardService.pasteFromClipboard(item.id, item.mime); - }); - } else { - ClipboardService.copyToClipboard(item.id); - launcher.close(); - } - }; - - results.push(entry); - }); - - // Show empty state if no results - if (results.length === 0) { - results.push({ - "name": searchTerm ? "No matching clipboard items" : "Clipboard is empty", - "description": searchTerm ? `No items containing "${query}"` : "Copy something to see it here", - "icon": iconMode === "tabler" ? "clipboard" : "text-x-generic", - "isTablerIcon": true, - "isImage": false, - "onActivate": function () {// Do nothing - } - }); - } - - //Logger.i("ClipboardPlugin", `Returning ${results.length} results for query: "${query}"`) - return results; - } - - function formatImageEntry(item) { - const meta = ClipboardService.parseImageMeta(item.preview); - - return { - "name": meta ? `Image ${meta.w}×${meta.h}` : "Image", - "description": meta ? `${meta.fmt} • ${meta.size}` : item.mime || "Image data", - "icon": iconMode === "tabler" ? "photo" : "image", - "isTablerIcon": true, - "isImage": true, - "imageWidth": meta ? meta.w : 0, - "imageHeight": meta ? meta.h : 0, - "clipboardId": item.id, - "mime": item.mime, - "preview": item.preview, - "provider": root - }; - } - - function formatTextEntry(item) { - const preview = (item.preview || "").trim(); - const lines = preview.split('\n').filter(l => l.trim()); - - let title = lines[0] || "Empty text"; - if (title.length > 60) { - title = title.substring(0, 57) + "..."; - } - - let description = ""; - if (lines.length > 1) { - description = lines[1]; - if (description.length > 80) { - description = description.substring(0, 77) + "..."; - } - } else { - // Preview is truncated at ~100 chars, so we can't show exact count - if (preview.length >= 100) { - description = I18n.tr("toast.clipboard.long-text"); - } else { - const chars = preview.length; - const words = preview.split(/\s+/).length; - description = `${chars} characters, ${words} word${words !== 1 ? 's' : ''}`; - } - } - - let defaultIcon = iconMode === "tabler" ? "clipboard" : "text-x-generic"; - let colorHex = ""; - if (Settings.data.appLauncher.enableClipboardSmartIcons) { - if (item.contentType === "link") - defaultIcon = iconMode === "tabler" ? "link" : "insert-link"; - else if (item.contentType === "file") - defaultIcon = iconMode === "tabler" ? "file" : "text-x-generic"; - else if (item.contentType === "code") - defaultIcon = iconMode === "tabler" ? "code" : "text-x-script"; - else if (item.contentType === "color") { - defaultIcon = iconMode === "tabler" ? "palette" : "color-picker"; - colorHex = preview; - } - } - - return { - "name": title, - "description": description, - "icon": defaultIcon, + // Return available commands when user types ">" + function commands() { + return [ + { + "name": ">clip", + "description": I18n.tr("launcher.providers.clipboard-search-description"), + "icon": iconMode === "tabler" ? "clipboard" : "diodon", "isTablerIcon": true, "isImage": false, - "clipboardId": item.id, - "preview": preview, - "contentType": item.contentType, - "colorHex": colorHex, - "provider": root + "onActivate": function () { + launcher.setSearchText(">clip "); + } + }, + { + "name": ">clip clear", + "description": I18n.tr("launcher.providers.clipboard-clear-description"), + "icon": iconMode === "tabler" ? "trash" : "user-trash", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () { + ClipboardService.wipeAll(); + launcher.close(); + } + } + ]; + } + + // Get search results + function getResults(searchText) { + if (!searchText.startsWith(">clip")) { + return []; + } + + lastSearchText = searchText; + const results = []; + const query = searchText.slice(5).trim(); + + // Check if clipboard service is not active + if (!ClipboardService.active) { + // If dependency check hasn't completed yet, show loading instead of disabled + if (!ClipboardService.dependencyChecked) { + return [ + { + "name": I18n.tr("launcher.providers.clipboard-loading"), + "description": I18n.tr("launcher.providers.emoji-loading-description"), + "icon": iconMode === "tabler" ? "refresh" : "view-refresh", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () {} + } + ]; + } + return [ + { + "name": I18n.tr("launcher.providers.clipboard-history-disabled"), + "description": I18n.tr("launcher.providers.clipboard-history-disabled-description"), + "icon": iconMode === "tabler" ? "refresh" : "view-refresh", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () {} + } + ]; + } + + // Special command: clear + if (query === "clear") { + return [ + { + "name": I18n.tr("launcher.providers.clipboard-clear-history"), + "description": I18n.tr("launcher.providers.clipboard-clear-description-full"), + "icon": iconMode === "tabler" ? "trash" : "user-trash", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () { + ClipboardService.wipeAll(); + launcher.close(); + } + } + ]; + } + + // Show loading state if data is being loaded + if (ClipboardService.loading || isWaitingForData) { + return [ + { + "name": I18n.tr("launcher.providers.clipboard-loading"), + "description": I18n.tr("launcher.providers.emoji-loading-description"), + "icon": iconMode === "tabler" ? "refresh" : "view-refresh", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () {} + } + ]; + } + + // Get clipboard items + const items = ClipboardService.items || []; + + // If no items and we haven't tried loading yet, trigger a load + if (items.count === 0 && !ClipboardService.loading) { + isWaitingForData = true; + ClipboardService.list(100); + return [ + { + "name": I18n.tr("launcher.providers.clipboard-loading"), + "description": I18n.tr("launcher.providers.emoji-loading-description"), + "icon": iconMode === "tabler" ? "refresh" : "view-refresh", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () {} + } + ]; + } + + // Search clipboard items + const searchTerm = query.toLowerCase(); + + // Date grouping trackers + const headersEnabled = Settings.data.appLauncher.enableClipboardDateHeaders; + const now = Date.now() / 1000; + const todayStart = new Date(); + todayStart.setHours(0, 0, 0, 0); + const todayStartTs = todayStart.getTime() / 1000; + const yesterdayStartTs = todayStartTs - 86400; + + let currentGroup = ""; + + // Filter and format results + items.forEach(function (item) { + // Category filter + if (Settings.data.appLauncher.enableClipboardChips && root.selectedCategory !== "All") { + const catMap = { + "Images": "image", + "Links": "link", + "Files": "file", + "Code": "code", + "Colors": "color" }; - } + if (item.contentType !== catMap[root.selectedCategory]) { + return; + } + } - function getImageForItem(clipboardId) { - return ClipboardService.getImageData ? ClipboardService.getImageData(clipboardId) : null; - } + const preview = (item.preview || "").toLowerCase(); - // ------------------------- - // Item actions for launcher delegate - function getItemActions(item) { - if (!item || !item.clipboardId) - return []; + // Skip if search term doesn't match + if (searchTerm && preview.indexOf(searchTerm) === -1) { + return; + } - var actions = []; + // Date Filter + const firstSeen = ClipboardService.firstSeenById[item.id] || now; + if (root.dateFilter !== "all") { + if (root.dateFilter === "today" && firstSeen < todayStartTs) + return; + if (root.dateFilter === "yesterday" && (firstSeen >= todayStartTs || firstSeen < yesterdayStartTs)) + return; + if (root.dateFilter === "week" && (firstSeen >= yesterdayStartTs || firstSeen < (todayStartTs - (86400 * 7)))) + return; + } - // Annotation tool for images - if (item.isImage && Settings.data.appLauncher.screenshotAnnotationTool !== "") { - actions.push({ - "icon": "pencil", - "tooltip": I18n.tr("tooltips.open-annotation-tool"), - "action": function () { - var tool = Settings.data.appLauncher.screenshotAnnotationTool; - Quickshell.execDetached(["sh", "-c", "cliphist decode " + item.clipboardId + " | " + tool]); - if (launcher) - launcher.close(); - } - }); + // Check date group logic + if (headersEnabled && !searchTerm && root.selectedCategory === "All" && root.dateFilter === "all") { + let groupName = I18n.tr("launcher.date-filter-all-time"); + if (firstSeen >= todayStartTs) { + groupName = I18n.tr("launcher.date-filter-today"); + } else if (firstSeen >= yesterdayStartTs) { + groupName = I18n.tr("launcher.date-filter-yesterday"); + } else if (firstSeen >= todayStartTs - (86400 * 7)) { + groupName = I18n.tr("launcher.date-filter-previous-7-days"); } - // Delete action - actions.push({ - "icon": "trash", - "tooltip": I18n.tr("launcher.providers.clipboard-delete"), - "action": function () { - deleteItem(item); - } - }); - - return actions; - } - - function canDeleteItem(item) { - return item && !!item.clipboardId; - } - - function deleteItem(item) { - if (!item || !item.clipboardId) - return; - - // Set provider state before deletion so refresh works - gotResults = false; - isWaitingForData = true; - lastSearchText = launcher ? launcher.searchText : ""; - - // Delete the item - ClipboardService.deleteById(String(item.clipboardId)); - } - - // Prepare item for display (handles image decoding) - function prepareItem(item) { - if (item && item.isImage && item.clipboardId) { - if (!ClipboardService.getImageData(item.clipboardId)) { - ClipboardService.decodeToDataUrl(item.clipboardId, item.mime, null); - } + if (groupName !== currentGroup) { + currentGroup = groupName; + results.push({ + "name": currentGroup, + "description": "", + "icon": iconMode === "tabler" ? "calendar" : "x-office-calendar", + "isTablerIcon": true, + "isImage": false, + "hideIcon": true, + "isHeader": true, + "clipboardId": "", + "onActivate": function () {} + }); } + } + + // Format the result based on type + let entry; + if (item.isImage) { + entry = formatImageEntry(item); + } else { + entry = formatTextEntry(item); + } + + // Add activation handler + entry.onActivate = function () { + if (Settings.data.appLauncher.autoPasteClipboard) { + launcher.closeImmediately(); + Qt.callLater(() => { + ClipboardService.pasteFromClipboard(item.id, item.mime); + }); + } else { + ClipboardService.copyToClipboard(item.id); + launcher.close(); + } + }; + + results.push(entry); + }); + + // Show empty state if no results + if (results.length === 0) { + results.push({ + "name": searchTerm ? "No matching clipboard items" : "Clipboard is empty", + "description": searchTerm ? `No items containing "${query}"` : "Copy something to see it here", + "icon": iconMode === "tabler" ? "clipboard" : "text-x-generic", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () {// Do nothing + } + }); } - // Get image URL for item (used by delegates) - function getImageUrl(item) { - if (!item || !item.clipboardId) - return ""; - return ClipboardService.getImageData(item.clipboardId) || ""; + //Logger.i("ClipboardPlugin", `Returning ${results.length} results for query: "${query}"`) + return results; + } + + function formatImageEntry(item) { + const meta = ClipboardService.parseImageMeta(item.preview); + + return { + "name": meta ? `Image ${meta.w}×${meta.h}` : "Image", + "description": meta ? `${meta.fmt} • ${meta.size}` : item.mime || "Image data", + "icon": iconMode === "tabler" ? "photo" : "image", + "isTablerIcon": true, + "isImage": true, + "imageWidth": meta ? meta.w : 0, + "imageHeight": meta ? meta.h : 0, + "clipboardId": item.id, + "mime": item.mime, + "preview": item.preview, + "provider": root + }; + } + + function formatTextEntry(item) { + const preview = (item.preview || "").trim(); + const lines = preview.split('\n').filter(l => l.trim()); + + let title = lines[0] || "Empty text"; + if (title.length > 60) { + title = title.substring(0, 57) + "..."; } - // Get preview data for the preview panel - function getPreviewData(item) { - if (!item || item.isHeader) - return null; - return { - "clipboardId": item.clipboardId, - "isImage": item.isImage, - "mime": item.mime, - "preview": item.preview - }; + let description = ""; + if (lines.length > 1) { + description = lines[1]; + if (description.length > 80) { + description = description.substring(0, 77) + "..."; + } + } else { + // Preview is truncated at ~100 chars, so we can't show exact count + if (preview.length >= 100) { + description = I18n.tr("toast.clipboard.long-text"); + } else { + const chars = preview.length; + const words = preview.split(/\s+/).length; + description = `${chars} characters, ${words} word${words !== 1 ? 's' : ''}`; + } } + + let defaultIcon = iconMode === "tabler" ? "clipboard" : "text-x-generic"; + let colorHex = ""; + if (Settings.data.appLauncher.enableClipboardSmartIcons) { + if (item.contentType === "link") + defaultIcon = iconMode === "tabler" ? "link" : "insert-link"; + else if (item.contentType === "file") + defaultIcon = iconMode === "tabler" ? "file" : "text-x-generic"; + else if (item.contentType === "code") + defaultIcon = iconMode === "tabler" ? "code" : "text-x-script"; + else if (item.contentType === "color") { + defaultIcon = iconMode === "tabler" ? "palette" : "color-picker"; + colorHex = preview; + } + } + + return { + "name": title, + "description": description, + "icon": defaultIcon, + "isTablerIcon": true, + "isImage": false, + "clipboardId": item.id, + "preview": preview, + "contentType": item.contentType, + "colorHex": colorHex, + "provider": root + }; + } + + function getImageForItem(clipboardId) { + return ClipboardService.getImageData ? ClipboardService.getImageData(clipboardId) : null; + } + + // ------------------------- + // Item actions for launcher delegate + function getItemActions(item) { + if (!item || !item.clipboardId) + return []; + + var actions = []; + + // Annotation tool for images + if (item.isImage && Settings.data.appLauncher.screenshotAnnotationTool !== "") { + actions.push({ + "icon": "pencil", + "tooltip": I18n.tr("tooltips.open-annotation-tool"), + "action": function () { + var tool = Settings.data.appLauncher.screenshotAnnotationTool; + Quickshell.execDetached(["sh", "-c", "cliphist decode " + item.clipboardId + " | " + tool]); + if (launcher) + launcher.close(); + } + }); + } + + // Delete action + actions.push({ + "icon": "trash", + "tooltip": I18n.tr("launcher.providers.clipboard-delete"), + "action": function () { + deleteItem(item); + } + }); + + return actions; + } + + function canDeleteItem(item) { + return item && !!item.clipboardId; + } + + function deleteItem(item) { + if (!item || !item.clipboardId) + return; + + // Set provider state before deletion so refresh works + gotResults = false; + isWaitingForData = true; + lastSearchText = launcher ? launcher.searchText : ""; + + // Delete the item + ClipboardService.deleteById(String(item.clipboardId)); + } + + // Prepare item for display (handles image decoding) + function prepareItem(item) { + if (item && item.isImage && item.clipboardId) { + if (!ClipboardService.getImageData(item.clipboardId)) { + ClipboardService.decodeToDataUrl(item.clipboardId, item.mime, null); + } + } + } + + // Get image URL for item (used by delegates) + function getImageUrl(item) { + if (!item || !item.clipboardId) + return ""; + return ClipboardService.getImageData(item.clipboardId) || ""; + } + + // Get preview data for the preview panel + function getPreviewData(item) { + if (!item || item.isHeader) + return null; + return { + "clipboardId": item.clipboardId, + "isImage": item.isImage, + "mime": item.mime, + "preview": item.preview + }; + } } diff --git a/Modules/Panels/Settings/Tabs/Launcher/ClipboardSubTab.qml b/Modules/Panels/Settings/Tabs/Launcher/ClipboardSubTab.qml index fd7899a46..7ddb19d2e 100644 --- a/Modules/Panels/Settings/Tabs/Launcher/ClipboardSubTab.qml +++ b/Modules/Panels/Settings/Tabs/Launcher/ClipboardSubTab.qml @@ -6,94 +6,94 @@ import qs.Services.System import qs.Widgets ColumnLayout { - id: root - spacing: Style.marginL + id: root + spacing: Style.marginL + Layout.fillWidth: true + + NToggle { + label: I18n.tr("panels.launcher.settings-clipboard-history-label") + description: I18n.tr("panels.launcher.settings-clipboard-history-description") + checked: Settings.data.appLauncher.enableClipboardHistory + onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked + defaultValue: Settings.getDefaultValue("appLauncher.enableClipboardHistory") + } + + NToggle { + label: I18n.tr("panels.launcher.settings-clip-preview-label") + description: I18n.tr("panels.launcher.settings-clip-preview-description") + checked: Settings.data.appLauncher.enableClipPreview + onToggled: checked => Settings.data.appLauncher.enableClipPreview = checked + defaultValue: Settings.getDefaultValue("appLauncher.enableClipPreview") + enabled: Settings.data.appLauncher.enableClipboardHistory + } + + NToggle { + label: I18n.tr("panels.launcher.settings-clip-wrap-text-label") + description: I18n.tr("panels.launcher.settings-clip-wrap-text-description") + checked: Settings.data.appLauncher.clipboardWrapText + onToggled: checked => Settings.data.appLauncher.clipboardWrapText = checked + defaultValue: Settings.getDefaultValue("appLauncher.clipboardWrapText") + enabled: Settings.data.appLauncher.enableClipboardHistory + } + + NToggle { + label: I18n.tr("panels.launcher.settings-auto-paste-label") + description: I18n.tr("panels.launcher.settings-auto-paste-description") + checked: Settings.data.appLauncher.autoPasteClipboard + onToggled: checked => Settings.data.appLauncher.autoPasteClipboard = checked + defaultValue: Settings.getDefaultValue("appLauncher.autoPasteClipboard") + enabled: Settings.data.appLauncher.enableClipboardHistory && ProgramCheckerService.wtypeAvailable + } + + NToggle { + label: I18n.tr("panels.launcher.settings-clip-smart-icons-label") + description: I18n.tr("panels.launcher.settings-clip-smart-icons-description") + checked: Settings.data.appLauncher.enableClipboardSmartIcons + onToggled: checked => Settings.data.appLauncher.enableClipboardSmartIcons = checked + defaultValue: Settings.getDefaultValue("appLauncher.enableClipboardSmartIcons") + enabled: Settings.data.appLauncher.enableClipboardHistory + } + + NToggle { + label: I18n.tr("panels.launcher.settings-clip-chips-label") + description: I18n.tr("panels.launcher.settings-clip-chips-description") + checked: Settings.data.appLauncher.enableClipboardChips + onToggled: checked => Settings.data.appLauncher.enableClipboardChips = checked + defaultValue: Settings.getDefaultValue("appLauncher.enableClipboardChips") + enabled: Settings.data.appLauncher.enableClipboardHistory + } + + NToggle { + label: I18n.tr("panels.launcher.settings-clip-date-headers-label") + description: I18n.tr("panels.launcher.settings-clip-date-headers-description") + checked: Settings.data.appLauncher.enableClipboardDateHeaders + onToggled: checked => Settings.data.appLauncher.enableClipboardDateHeaders = checked + defaultValue: Settings.getDefaultValue("appLauncher.enableClipboardDateHeaders") + enabled: Settings.data.appLauncher.enableClipboardHistory + } + + NDivider { Layout.fillWidth: true + visible: Settings.data.appLauncher.enableClipboardHistory + } - NToggle { - label: I18n.tr("panels.launcher.settings-clipboard-history-label") - description: I18n.tr("panels.launcher.settings-clipboard-history-description") - checked: Settings.data.appLauncher.enableClipboardHistory - onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked - defaultValue: Settings.getDefaultValue("appLauncher.enableClipboardHistory") - } + NTextInput { + label: I18n.tr("panels.launcher.settings-clipboard-watch-text-label") + description: I18n.tr("panels.launcher.settings-clipboard-watch-text-description") + Layout.fillWidth: true + text: Settings.data.appLauncher.clipboardWatchTextCommand + onEditingFinished: Settings.data.appLauncher.clipboardWatchTextCommand = text + enabled: Settings.data.appLauncher.enableClipboardHistory + visible: Settings.data.appLauncher.enableClipboardHistory + } - NToggle { - label: I18n.tr("panels.launcher.settings-clip-preview-label") - description: I18n.tr("panels.launcher.settings-clip-preview-description") - checked: Settings.data.appLauncher.enableClipPreview - onToggled: checked => Settings.data.appLauncher.enableClipPreview = checked - defaultValue: Settings.getDefaultValue("appLauncher.enableClipPreview") - enabled: Settings.data.appLauncher.enableClipboardHistory - } - - NToggle { - label: I18n.tr("panels.launcher.settings-clip-wrap-text-label") - description: I18n.tr("panels.launcher.settings-clip-wrap-text-description") - checked: Settings.data.appLauncher.clipboardWrapText - onToggled: checked => Settings.data.appLauncher.clipboardWrapText = checked - defaultValue: Settings.getDefaultValue("appLauncher.clipboardWrapText") - enabled: Settings.data.appLauncher.enableClipboardHistory - } - - NToggle { - label: I18n.tr("panels.launcher.settings-auto-paste-label") - description: I18n.tr("panels.launcher.settings-auto-paste-description") - checked: Settings.data.appLauncher.autoPasteClipboard - onToggled: checked => Settings.data.appLauncher.autoPasteClipboard = checked - defaultValue: Settings.getDefaultValue("appLauncher.autoPasteClipboard") - enabled: Settings.data.appLauncher.enableClipboardHistory && ProgramCheckerService.wtypeAvailable - } - - NToggle { - label: I18n.tr("panels.launcher.settings-clip-smart-icons-label") - description: I18n.tr("panels.launcher.settings-clip-smart-icons-description") - checked: Settings.data.appLauncher.enableClipboardSmartIcons - onToggled: checked => Settings.data.appLauncher.enableClipboardSmartIcons = checked - defaultValue: Settings.getDefaultValue("appLauncher.enableClipboardSmartIcons") - enabled: Settings.data.appLauncher.enableClipboardHistory - } - - NToggle { - label: I18n.tr("panels.launcher.settings-clip-chips-label") - description: I18n.tr("panels.launcher.settings-clip-chips-description") - checked: Settings.data.appLauncher.enableClipboardChips - onToggled: checked => Settings.data.appLauncher.enableClipboardChips = checked - defaultValue: Settings.getDefaultValue("appLauncher.enableClipboardChips") - enabled: Settings.data.appLauncher.enableClipboardHistory - } - - NToggle { - label: I18n.tr("panels.launcher.settings-clip-date-headers-label") - description: I18n.tr("panels.launcher.settings-clip-date-headers-description") - checked: Settings.data.appLauncher.enableClipboardDateHeaders - onToggled: checked => Settings.data.appLauncher.enableClipboardDateHeaders = checked - defaultValue: Settings.getDefaultValue("appLauncher.enableClipboardDateHeaders") - enabled: Settings.data.appLauncher.enableClipboardHistory - } - - NDivider { - Layout.fillWidth: true - visible: Settings.data.appLauncher.enableClipboardHistory - } - - NTextInput { - label: I18n.tr("panels.launcher.settings-clipboard-watch-text-label") - description: I18n.tr("panels.launcher.settings-clipboard-watch-text-description") - Layout.fillWidth: true - text: Settings.data.appLauncher.clipboardWatchTextCommand - onEditingFinished: Settings.data.appLauncher.clipboardWatchTextCommand = text - enabled: Settings.data.appLauncher.enableClipboardHistory - visible: Settings.data.appLauncher.enableClipboardHistory - } - - NTextInput { - label: I18n.tr("panels.launcher.settings-clipboard-watch-image-label") - description: I18n.tr("panels.launcher.settings-clipboard-watch-image-description") - Layout.fillWidth: true - text: Settings.data.appLauncher.clipboardWatchImageCommand - onEditingFinished: Settings.data.appLauncher.clipboardWatchImageCommand = text - enabled: Settings.data.appLauncher.enableClipboardHistory - visible: Settings.data.appLauncher.enableClipboardHistory - } + NTextInput { + label: I18n.tr("panels.launcher.settings-clipboard-watch-image-label") + description: I18n.tr("panels.launcher.settings-clipboard-watch-image-description") + Layout.fillWidth: true + text: Settings.data.appLauncher.clipboardWatchImageCommand + onEditingFinished: Settings.data.appLauncher.clipboardWatchImageCommand = text + enabled: Settings.data.appLauncher.enableClipboardHistory + visible: Settings.data.appLauncher.enableClipboardHistory + } } diff --git a/Services/Keyboard/ClipboardService.qml b/Services/Keyboard/ClipboardService.qml index 1c66ec8b2..3c04bd29b 100644 --- a/Services/Keyboard/ClipboardService.qml +++ b/Services/Keyboard/ClipboardService.qml @@ -8,534 +8,529 @@ import qs.Services.UI // Clipboard history service using cliphist + local content cache Singleton { - id: root + id: root - // Public API - property bool active: Settings.data.appLauncher.enableClipboardHistory && cliphistAvailable - property bool loading: false - property var items: [] // [{id, preview, mime, isImage}] + // Public API + property bool active: Settings.data.appLauncher.enableClipboardHistory && cliphistAvailable + property bool loading: false + property var items: [] // [{id, preview, mime, isImage}] - // Check if cliphist is available on the system - property bool cliphistAvailable: false - property bool dependencyChecked: false + // Check if cliphist is available on the system + property bool cliphistAvailable: false + property bool dependencyChecked: false - // Optional automatic watchers to feed cliphist DB - property bool autoWatch: true - property bool watchersStarted: false + // Optional automatic watchers to feed cliphist DB + property bool autoWatch: true + property bool watchersStarted: false - // Expose decoded thumbnails by id and a revision to notify bindings - property var imageDataById: ({}) - property var _imageDataInsertOrder: [] // insertion-order IDs for LRU eviction - readonly property int _imageDataMaxEntries: 20 // max decoded images held in RAM at once - property int revision: 0 + // Expose decoded thumbnails by id and a revision to notify bindings + property var imageDataById: ({}) + property var _imageDataInsertOrder: [] // insertion-order IDs for LRU eviction + readonly property int _imageDataMaxEntries: 20 // max decoded images held in RAM at once + property int revision: 0 - // Local content cache - stores full text content by ID - // This avoids relying on cliphist decode which can be unreliable - property var contentCache: ({}) + // Local content cache - stores full text content by ID + // This avoids relying on cliphist decode which can be unreliable + property var contentCache: ({}) - // Track the most recent clipboard content for instant access - property string _latestTextContent: "" - property string _latestTextId: "" + // Track the most recent clipboard content for instant access + property string _latestTextContent: "" + property string _latestTextId: "" - // Approximate first-seen timestamps for entries this session (seconds) - property var firstSeenById: ({}) + // Approximate first-seen timestamps for entries this session (seconds) + property var firstSeenById: ({}) - // Internal: store callback for decode - property var _decodeCallback: null - property int _decodeRequestId: 0 + // Internal: store callback for decode + property var _decodeCallback: null + property int _decodeRequestId: 0 - // Queue for base64 decodes - property var _b64Queue: [] - property var _b64CurrentCb: null - property string _b64CurrentMime: "" - property string _b64CurrentId: "" + // Queue for base64 decodes + property var _b64Queue: [] + property var _b64CurrentCb: null + property string _b64CurrentMime: "" + property string _b64CurrentId: "" - signal listCompleted + signal listCompleted - // Check if cliphist is available - Component.onCompleted: { - checkCliphistAvailability(); - } + // Check if cliphist is available + Component.onCompleted: { + checkCliphistAvailability(); + } - // Check dependency availability - function checkCliphistAvailability() { - if (dependencyChecked) - return; - dependencyCheckProcess.command = ["sh", "-c", "command -v cliphist"]; - dependencyCheckProcess.running = true; - } + // Check dependency availability + function checkCliphistAvailability() { + if (dependencyChecked) + return; + dependencyCheckProcess.command = ["sh", "-c", "command -v cliphist"]; + dependencyCheckProcess.running = true; + } - // Process to check if cliphist is available - Process { - id: dependencyCheckProcess - stdout: StdioCollector {} - onExited: (exitCode, exitStatus) => { - root.dependencyChecked = true; - if (exitCode === 0) { - root.cliphistAvailable = true; - // Start watchers if feature is enabled - if (root.active) { - startWatchers(); - } - } else { - root.cliphistAvailable = false; - // Show toast notification if feature is enabled but cliphist is missing - if (Settings.data.appLauncher.enableClipboardHistory) { - ToastService.showWarning(I18n.tr("toast.clipboard.unavailable"), I18n.tr("toast.clipboard.unavailable-desc"), 6000); - } - } - } - } - - // Start/stop watchers when enabled changes - onActiveChanged: { + // Process to check if cliphist is available + Process { + id: dependencyCheckProcess + stdout: StdioCollector {} + onExited: (exitCode, exitStatus) => { + root.dependencyChecked = true; + if (exitCode === 0) { + root.cliphistAvailable = true; + // Start watchers if feature is enabled if (root.active) { - startWatchers(); - } else { - stopWatchers(); - loading = false; - items = []; + startWatchers(); } + } else { + root.cliphistAvailable = false; + // Show toast notification if feature is enabled but cliphist is missing + if (Settings.data.appLauncher.enableClipboardHistory) { + ToastService.showWarning(I18n.tr("toast.clipboard.unavailable"), I18n.tr("toast.clipboard.unavailable-desc"), 6000); + } + } } + } - // Fallback: periodically refresh list so UI updates even if not in clip mode - Timer { - interval: 5000 - repeat: true - running: root.active - onTriggered: list() + // Start/stop watchers when enabled changes + onActiveChanged: { + if (root.active) { + startWatchers(); + } else { + stopWatchers(); + loading = false; + items = []; } + } - // Internal process objects - Process { - id: listProc - stdout: StdioCollector {} - onExited: (exitCode, exitStatus) => { - const out = String(stdout.text); - const lines = out.split('\n').filter(l => l.length > 0); - // cliphist list default format: " " or "\t" - const parsed = lines.map((l, i) => { - let id = ""; - let preview = ""; - const m = l.match(/^(\d+)\s+(.+)$/); - if (m) { - id = m[1]; - preview = m[2]; - } else { - const tab = l.indexOf('\t'); - id = tab > -1 ? l.slice(0, tab) : l; - preview = tab > -1 ? l.slice(tab + 1) : ""; - } - const lower = preview.toLowerCase(); - const isImage = lower.startsWith("[image]") || lower.includes(" binary data "); - // Best-effort mime guess from preview - var mime = "text/plain"; - if (isImage) { - if (lower.includes(" png")) - mime = "image/png"; - else if (lower.includes(" jpg") || lower.includes(" jpeg")) - mime = "image/jpeg"; - else if (lower.includes(" webp")) - mime = "image/webp"; - else if (lower.includes(" gif")) - mime = "image/gif"; - else - mime = "image/*"; - } - // Record first seen time for new ids (approximate copy time) - if (!root.firstSeenById[id]) { - const assumedAge = i * 15 * 60; - root.firstSeenById[id] = Time.timestamp - assumedAge; - } - // Smart type detection - var contentType = "text"; - if (isImage) { - contentType = "image"; - } else { - const t = preview.trim(); - const tLower = t.toLowerCase(); - if (/^#([a-f0-9]{3}|[a-f0-9]{6}|[a-f0-9]{8})$/.test(tLower)) { - contentType = "color"; - } else if (/^https?:\/\//i.test(t)) { - contentType = "link"; - } else if (/^(\/|~\/|file:\/\/)/i.test(t) && !t.startsWith('//') && !t.includes('\n')) { - contentType = "file"; - } else if ( - (t.includes('{') && t.includes('}') && (t.includes(';') || t.includes('='))) || - t.includes('') || t.includes('=>') || t.includes('===') || t.includes('!==') || t.includes('::') || t.includes('->') || - /^(?:const|let|var|function|class|struct|interface|type|enum|import|export|func|fn|pub|def|using|namespace|property|public|private|protected)\b/i.test(t) || - /^(?:#include|#define|#\[|@|\/\/|\/\*|<\?| { + const out = String(stdout.text); + const lines = out.split('\n').filter(l => l.length > 0); + // cliphist list default format: " " or "\t" + const parsed = lines.map((l, i) => { + let id = ""; + let preview = ""; + const m = l.match(/^(\d+)\s+(.+)$/); + if (m) { + id = m[1]; + preview = m[2]; + } else { + const tab = l.indexOf('\t'); + id = tab > -1 ? l.slice(0, tab) : l; + preview = tab > -1 ? l.slice(tab + 1) : ""; + } + const lower = preview.toLowerCase(); + const isImage = lower.startsWith("[image]") || lower.includes(" binary data "); + // Best-effort mime guess from preview + var mime = "text/plain"; + if (isImage) { + if (lower.includes(" png")) + mime = "image/png"; + else if (lower.includes(" jpg") || lower.includes(" jpeg")) + mime = "image/jpeg"; + else if (lower.includes(" webp")) + mime = "image/webp"; + else if (lower.includes(" gif")) + mime = "image/gif"; + else + mime = "image/*"; + } + // Record first seen time for new ids (approximate copy time) + if (!root.firstSeenById[id]) { + const assumedAge = i * 15 * 60; + root.firstSeenById[id] = Time.timestamp - assumedAge; + } + // Smart type detection + var contentType = "text"; + if (isImage) { + contentType = "image"; + } else { + const t = preview.trim(); + const tLower = t.toLowerCase(); + if (/^#([a-f0-9]{3}|[a-f0-9]{6}|[a-f0-9]{8})$/.test(tLower)) { + contentType = "color"; + } else if (/^https?:\/\//i.test(t)) { + contentType = "link"; + } else if (/^(\/|~\/|file:\/\/)/i.test(t) && !t.startsWith('//') && !t.includes('\n')) { + contentType = "file"; + } else if ((t.includes('{') && t.includes('}') && (t.includes(';') || t.includes('='))) || t.includes('') || t.includes('=>') || t.includes('===') || t.includes('!==') || t.includes('::') || t.includes('->') || + /^(?:const|let|var|function|class|struct|interface|type|enum|import|export|func|fn|pub|def|using|namespace|property|public|private|protected)\b/i.test(t) || /^(?:#include|#define|#\[|@|\/\/|\/\*|<\?| { - if (item.isImage) - return true; - const p = item.preview; - // Skip UTF-16 encoded text (has null bytes between chars), chromium browser artifact - const nullCount = (p.match(/\x00/g) || []).length; - if (nullCount > p.length * 0.2) - return false; - // Skip browser-generated HTML wrapper, firefox - if (p.toLowerCase().startsWith(" { + if (item.isImage) + return true; + const p = item.preview; + // Skip UTF-16 encoded text (has null bytes between chars), chromium browser artifact + const nullCount = (p.match(/\x00/g) || []).length; + if (nullCount > p.length * 0.2) + return false; + // Skip browser-generated HTML wrapper, firefox + if (p.toLowerCase().startsWith(" root._imageDataMaxEntries) { + const evicted = root._imageDataInsertOrder.shift(); + delete root.imageDataById[evicted]; + } + root.revision += 1; + } + root._b64CurrentCb = null; + root._b64CurrentMime = ""; + root._b64CurrentId = ""; + Qt.callLater(root._startNextB64); + } + } + + // Text watcher - stores to cliphist and triggers content capture + Process { + id: watchText + stdout: StdioCollector {} + onExited: (exitCode, exitStatus) => { + if (root.autoWatch && root.watchersStarted && Settings.data.appLauncher.clipboardWatchTextCommand.trim() !== "") { + watchTextRestartTimer.restart(); + } + } + } + + Timer { + id: watchTextRestartTimer + interval: 1000 + repeat: false + onTriggered: { + if (root.autoWatch && root.watchersStarted) + watchText.running = true; + } + } + + // Image watcher + Process { + id: watchImage + stdout: StdioCollector {} + onExited: (exitCode, exitStatus) => { + if (root.autoWatch && root.watchersStarted && Settings.data.appLauncher.clipboardWatchImageCommand.trim() !== "") { + watchImageRestartTimer.restart(); + } + } + } + + Timer { + id: watchImageRestartTimer + interval: 1000 + repeat: false + onTriggered: { + if (root.autoWatch && root.watchersStarted) + watchImage.running = true; + } + } + + // Capture current clipboard text when needed + Process { + id: captureTextProc + stdout: StdioCollector {} + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + const content = String(stdout.text); + if (content.length > 0) { + root._latestTextContent = content; + // Associate with newest item if we have one + if (root.items.length > 0 && !root.items[0].isImage) { + const newestId = root.items[0].id; + if (!root.contentCache[newestId]) { + root.contentCache[newestId] = content; + root.revision++; } - - root.listCompleted(); + } } + } } + } - Process { - id: decodeProc - property int requestId: 0 - stdout: StdioCollector {} - onExited: (exitCode, exitStatus) => { - if (requestId === root._decodeRequestId && root._decodeCallback) { - const out = String(stdout.text); - try { - root._decodeCallback(out); - } finally { - root._decodeCallback = null; - } - } - } - } + function startWatchers() { + if (!root.active || !autoWatch || watchersStarted || !root.cliphistAvailable) + return; + watchersStarted = true; - Process { - id: copyProc - stdout: StdioCollector {} - } - - Process { - id: pasteProc - stdout: StdioCollector {} - } - - Process { - id: deleteProc - stdout: StdioCollector {} - onExited: (exitCode, exitStatus) => { - revision++; - Qt.callLater(() => list()); - } - } - - // Base64 decode pipeline (queued) - Process { - id: decodeB64Proc - stdout: StdioCollector {} - onExited: (exitCode, exitStatus) => { - const b64 = String(stdout.text).trim(); - if (root._b64CurrentCb) { - const url = `data:${root._b64CurrentMime};base64,${b64}`; - try { - root._b64CurrentCb(url); - } catch (e) {} - } - if (root._b64CurrentId !== "") { - const entryId = root._b64CurrentId; - root.imageDataById[entryId] = `data:${root._b64CurrentMime};base64,${b64}`; - // Track insertion order and evict oldest entries beyond the cap - root._imageDataInsertOrder.push(entryId); - while (root._imageDataInsertOrder.length > root._imageDataMaxEntries) { - const evicted = root._imageDataInsertOrder.shift(); - delete root.imageDataById[evicted]; - } - root.revision += 1; - } - root._b64CurrentCb = null; - root._b64CurrentMime = ""; - root._b64CurrentId = ""; - Qt.callLater(root._startNextB64); - } - } - - // Text watcher - stores to cliphist and triggers content capture - Process { - id: watchText - stdout: StdioCollector {} - onExited: (exitCode, exitStatus) => { - if (root.autoWatch && root.watchersStarted && Settings.data.appLauncher.clipboardWatchTextCommand.trim() !== "") { - watchTextRestartTimer.restart(); - } - } - } - - Timer { - id: watchTextRestartTimer - interval: 1000 - repeat: false - onTriggered: { - if (root.autoWatch && root.watchersStarted) - watchText.running = true; - } - } + // Text watcher + watchText.command = ["sh", "-c", Settings.data.appLauncher.clipboardWatchTextCommand]; + watchText.running = true; // Image watcher - Process { - id: watchImage - stdout: StdioCollector {} - onExited: (exitCode, exitStatus) => { - if (root.autoWatch && root.watchersStarted && Settings.data.appLauncher.clipboardWatchImageCommand.trim() !== "") { - watchImageRestartTimer.restart(); - } - } + watchImage.command = ["sh", "-c", Settings.data.appLauncher.clipboardWatchImageCommand]; + watchImage.running = true; + } + + function stopWatchers() { + if (!watchersStarted) + return; + watchText.running = false; + watchImage.running = false; + watchersStarted = false; + } + + // Capture current clipboard text and cache it + function captureCurrentClipboard() { + if (captureTextProc.running) + return; + captureTextProc.command = ["wl-paste", "--no-newline"]; + captureTextProc.running = true; + } + + function list(maxPreviewWidth) { + if (!root.active || !root.cliphistAvailable) { + return; + } + if (listProc.running) + return; + loading = true; + const width = maxPreviewWidth || 100; + listProc.command = ["cliphist", "list", "-preview-width", String(width)]; + listProc.running = true; + } + + // Get content for an ID - uses cache first, falls back to cliphist decode + function getContent(id) { + if (root.contentCache[id]) { + return root.contentCache[id]; + } + return null; + } + + // Async decode - checks cache first, then falls back to cliphist + function decode(id, cb) { + if (!root.cliphistAvailable) { + if (cb) + cb(""); + return; } - Timer { - id: watchImageRestartTimer - interval: 1000 - repeat: false - onTriggered: { - if (root.autoWatch && root.watchersStarted) - watchImage.running = true; - } + // Check cache first + const cached = root.contentCache[id]; + if (cached) { + if (cb) + cb(cached); + return; } - // Capture current clipboard text when needed - Process { - id: captureTextProc - stdout: StdioCollector {} - onExited: (exitCode, exitStatus) => { - if (exitCode === 0) { - const content = String(stdout.text); - if (content.length > 0) { - root._latestTextContent = content; - // Associate with newest item if we have one - if (root.items.length > 0 && !root.items[0].isImage) { - const newestId = root.items[0].id; - if (!root.contentCache[newestId]) { - root.contentCache[newestId] = content; - root.revision++; - } - } - } - } - } + // Fall back to cliphist decode + if (decodeProc.running) { + decodeProc.running = false; } + root._decodeRequestId++; + decodeProc.requestId = root._decodeRequestId; + root._decodeCallback = function (content) { + // Cache the result if successful + if (content && content.trim()) { + root.contentCache[id] = content; + } + if (cb) + cb(content); + }; + const idStr = String(id); + decodeProc.command = ["cliphist", "decode", idStr]; + decodeProc.running = true; + } - function startWatchers() { - if (!root.active || !autoWatch || watchersStarted || !root.cliphistAvailable) - return; - watchersStarted = true; - - // Text watcher - watchText.command = ["sh", "-c", Settings.data.appLauncher.clipboardWatchTextCommand]; - watchText.running = true; - - // Image watcher - watchImage.command = ["sh", "-c", Settings.data.appLauncher.clipboardWatchImageCommand]; - watchImage.running = true; + function decodeToDataUrl(id, mime, cb) { + if (!root.cliphistAvailable) { + if (cb) + cb(""); + return; } - - function stopWatchers() { - if (!watchersStarted) - return; - watchText.running = false; - watchImage.running = false; - watchersStarted = false; + // If cached, return immediately + if (root.imageDataById[id]) { + if (cb) + cb(root.imageDataById[id]); + return; } - - // Capture current clipboard text and cache it - function captureCurrentClipboard() { - if (captureTextProc.running) - return; - captureTextProc.command = ["wl-paste", "--no-newline"]; - captureTextProc.running = true; + // Queue request; ensures single process handles sequentially + root._b64Queue.push({ + "id": id, + "mime": mime || "image/*", + "cb": cb + }); + if (!decodeB64Proc.running && root._b64CurrentCb === null) { + _startNextB64(); } + } - function list(maxPreviewWidth) { - if (!root.active || !root.cliphistAvailable) { - return; - } - if (listProc.running) - return; - loading = true; - const width = maxPreviewWidth || 100; - listProc.command = ["cliphist", "list", "-preview-width", String(width)]; - listProc.running = true; + function getImageData(id) { + if (id === undefined) { + return null; } + return root.imageDataById[id]; + } - // Get content for an ID - uses cache first, falls back to cliphist decode - function getContent(id) { - if (root.contentCache[id]) { - return root.contentCache[id]; - } - return null; + function _startNextB64() { + if (root._b64Queue.length === 0 || !root.cliphistAvailable) + return; + const job = root._b64Queue.shift(); + root._b64CurrentCb = job.cb; + root._b64CurrentMime = job.mime; + root._b64CurrentId = job.id; + decodeB64Proc.command = ["sh", "-c", `cliphist decode ${job.id} | base64 -w 0`]; + decodeB64Proc.running = true; + } + + function copyToClipboard(id) { + if (!root.cliphistAvailable) { + return; } + copyProc.command = ["sh", "-c", `cliphist decode ${id} | wl-copy`]; + copyProc.running = true; + } - // Async decode - checks cache first, then falls back to cliphist - function decode(id, cb) { - if (!root.cliphistAvailable) { - if (cb) - cb(""); - return; - } - - // Check cache first - const cached = root.contentCache[id]; - if (cached) { - if (cb) - cb(cached); - return; - } - - // Fall back to cliphist decode - if (decodeProc.running) { - decodeProc.running = false; - } - root._decodeRequestId++; - decodeProc.requestId = root._decodeRequestId; - root._decodeCallback = function (content) { - // Cache the result if successful - if (content && content.trim()) { - root.contentCache[id] = content; - } - if (cb) - cb(content); - }; - const idStr = String(id); - decodeProc.command = ["cliphist", "decode", idStr]; - decodeProc.running = true; + function pasteFromClipboard(id, mime) { + if (!root.cliphistAvailable) { + return; } + const isImage = mime && mime.startsWith("image/"); + const typeArg = isImage ? ` --type ${mime}` : ""; + const pasteKeys = isImage ? "wtype -M ctrl -k v" : "wtype -M ctrl -M shift v"; + const cmd = `cliphist decode ${id} | wl-copy${typeArg} && ${pasteKeys}`; + pasteProc.command = ["sh", "-c", cmd]; + pasteProc.running = true; + } - function decodeToDataUrl(id, mime, cb) { - if (!root.cliphistAvailable) { - if (cb) - cb(""); - return; - } - // If cached, return immediately - if (root.imageDataById[id]) { - if (cb) - cb(root.imageDataById[id]); - return; - } - // Queue request; ensures single process handles sequentially - root._b64Queue.push({ - "id": id, - "mime": mime || "image/*", - "cb": cb - }); - if (!decodeB64Proc.running && root._b64CurrentCb === null) { - _startNextB64(); - } + function pasteText(text) { + if (!text) + return; + const escaped = text.replace(/'/g, "'\\''"); + const cmd = `printf '%s' '${escaped}' | wl-copy && wtype -M ctrl -M shift v`; + pasteProc.command = ["sh", "-c", cmd]; + pasteProc.running = true; + } + + function deleteById(id) { + if (!root.cliphistAvailable) { + return; } - - function getImageData(id) { - if (id === undefined) { - return null; - } - return root.imageDataById[id]; + if (deleteProc.running) { + return; } + const idStr = String(id).trim(); + // Remove from caches + delete root.contentCache[idStr]; + delete root.imageDataById[idStr]; + const orderIdx = root._imageDataInsertOrder.indexOf(idStr); + if (orderIdx !== -1) + root._imageDataInsertOrder.splice(orderIdx, 1); + deleteProc.command = ["sh", "-c", `echo ${idStr} | cliphist delete`]; + deleteProc.running = true; + } - function _startNextB64() { - if (root._b64Queue.length === 0 || !root.cliphistAvailable) - return; - const job = root._b64Queue.shift(); - root._b64CurrentCb = job.cb; - root._b64CurrentMime = job.mime; - root._b64CurrentId = job.id; - decodeB64Proc.command = ["sh", "-c", `cliphist decode ${job.id} | base64 -w 0`]; - decodeB64Proc.running = true; + function wipeAll() { + if (!root.cliphistAvailable) { + return; } + // Clear caches + root.contentCache = {}; + root.imageDataById = {}; + root._imageDataInsertOrder = []; + root._latestTextContent = ""; + root._latestTextId = ""; - function copyToClipboard(id) { - if (!root.cliphistAvailable) { - return; - } - copyProc.command = ["sh", "-c", `cliphist decode ${id} | wl-copy`]; - copyProc.running = true; - } + Quickshell.execDetached(["cliphist", "wipe"]); + revision++; + Qt.callLater(() => list()); + } - function pasteFromClipboard(id, mime) { - if (!root.cliphistAvailable) { - return; - } - const isImage = mime && mime.startsWith("image/"); - const typeArg = isImage ? ` --type ${mime}` : ""; - const pasteKeys = isImage ? "wtype -M ctrl -k v" : "wtype -M ctrl -M shift v"; - const cmd = `cliphist decode ${id} | wl-copy${typeArg} && ${pasteKeys}`; - pasteProc.command = ["sh", "-c", cmd]; - pasteProc.running = true; - } - - function pasteText(text) { - if (!text) - return; - const escaped = text.replace(/'/g, "'\\''"); - const cmd = `printf '%s' '${escaped}' | wl-copy && wtype -M ctrl -M shift v`; - pasteProc.command = ["sh", "-c", cmd]; - pasteProc.running = true; - } - - function deleteById(id) { - if (!root.cliphistAvailable) { - return; - } - if (deleteProc.running) { - return; - } - const idStr = String(id).trim(); - // Remove from caches - delete root.contentCache[idStr]; - delete root.imageDataById[idStr]; - const orderIdx = root._imageDataInsertOrder.indexOf(idStr); - if (orderIdx !== -1) - root._imageDataInsertOrder.splice(orderIdx, 1); - deleteProc.command = ["sh", "-c", `echo ${idStr} | cliphist delete`]; - deleteProc.running = true; - } - - function wipeAll() { - if (!root.cliphistAvailable) { - return; - } - // Clear caches - root.contentCache = {}; - root.imageDataById = {}; - root._imageDataInsertOrder = []; - root._latestTextContent = ""; - root._latestTextId = ""; - - Quickshell.execDetached(["cliphist", "wipe"]); - revision++; - Qt.callLater(() => list()); - } - - // Parse image metadata from cliphist preview string - function parseImageMeta(preview) { - const re = /\[\[\s*binary data\s+([\d\.]+\s*(?:KiB|MiB|GiB|B))\s+(\w+)\s+(\d+)x(\d+)\s*\]\]/i; - const match = (preview || "").match(re); - if (!match) - return null; - return { - "size": match[1], - "fmt": (match[2] || "").toUpperCase(), - "w": Number(match[3]), - "h": Number(match[4]) - }; - } + // Parse image metadata from cliphist preview string + function parseImageMeta(preview) { + const re = /\[\[\s*binary data\s+([\d\.]+\s*(?:KiB|MiB|GiB|B))\s+(\w+)\s+(\d+)x(\d+)\s*\]\]/i; + const match = (preview || "").match(re); + if (!match) + return null; + return { + "size": match[1], + "fmt": (match[2] || "").toUpperCase(), + "w": Number(match[3]), + "h": Number(match[4]) + }; + } } From d92aa0a7277f4886c8c937dbc71317d13d64017f Mon Sep 17 00:00:00 2001 From: "Braian A. Diez" Date: Thu, 12 Mar 2026 22:37:58 -0300 Subject: [PATCH 5/5] fix(clipboard): code cleanup Signed-off-by: Braian A. Diez --- Modules/Panels/Launcher/Launcher.qml | 3 +- .../Panels/Launcher/LauncherOverlayWindow.qml | 3 +- .../Launcher/Providers/ClipboardProvider.qml | 47 +++++++------------ 3 files changed, 18 insertions(+), 35 deletions(-) diff --git a/Modules/Panels/Launcher/Launcher.qml b/Modules/Panels/Launcher/Launcher.qml index 4518a6bc0..f71dd9da1 100644 --- a/Modules/Panels/Launcher/Launcher.qml +++ b/Modules/Panels/Launcher/Launcher.qml @@ -40,8 +40,7 @@ SmartPanel { return false; if (!Settings.data.appLauncher.enableClipPreview) return false; - var item = results[selectedIndex]; - return selectedIndex >= 0 && results && !!item && !item.isHeader; + return selectedIndex >= 0 && results && !!results[selectedIndex] && !results[selectedIndex].isHeader; } readonly property int previewPanelWidth: Math.round(400 * Style.uiScaleRatio) diff --git a/Modules/Panels/Launcher/LauncherOverlayWindow.qml b/Modules/Panels/Launcher/LauncherOverlayWindow.qml index 84dc88b3a..cab52c4d2 100644 --- a/Modules/Panels/Launcher/LauncherOverlayWindow.qml +++ b/Modules/Panels/Launcher/LauncherOverlayWindow.qml @@ -93,8 +93,7 @@ Variants { return false; if (!Settings.data.appLauncher.enableClipPreview) return false; - var item = launcherCore.results[launcherCore.selectedIndex]; - return launcherCore.selectedIndex >= 0 && launcherCore.results && !!item && !item.isHeader; + return launcherCore.selectedIndex >= 0 && launcherCore.results && !!launcherCore.results[launcherCore.selectedIndex] && !launcherCore.results[launcherCore.selectedIndex].isHeader; } // Dimmer background (click to close) diff --git a/Modules/Panels/Launcher/Providers/ClipboardProvider.qml b/Modules/Panels/Launcher/Providers/ClipboardProvider.qml index 5eee31fdc..afa090d60 100644 --- a/Modules/Panels/Launcher/Providers/ClipboardProvider.qml +++ b/Modules/Panels/Launcher/Providers/ClipboardProvider.qml @@ -43,40 +43,24 @@ Item { property string dateFilter: "all" property var availableDateFilters: [ { - get label() { - return I18n.tr("launcher.date-filter-all-time"); - }, + "label": I18n.tr("launcher.date-filter-all-time"), "action": "all", - get icon() { - return iconMode === "tabler" ? "calendar" : "x-office-calendar"; - } + "icon": iconMode === "tabler" ? "calendar" : "x-office-calendar" }, { - get label() { - return I18n.tr("launcher.date-filter-today"); - }, + "label": I18n.tr("launcher.date-filter-today"), "action": "today", - get icon() { - return iconMode === "tabler" ? "calendar-event" : "view-calendar-timeline"; - } + "icon": iconMode === "tabler" ? "calendar-event" : "view-calendar-timeline" }, { - get label() { - return I18n.tr("launcher.date-filter-yesterday"); - }, + "label": I18n.tr("launcher.date-filter-yesterday"), "action": "yesterday", - get icon() { - return iconMode === "tabler" ? "calendar-time" : "view-calendar"; - } + "icon": iconMode === "tabler" ? "calendar-time" : "view-calendar" }, { - get label() { - return I18n.tr("launcher.date-filter-previous-7-days"); - }, + "label": I18n.tr("launcher.date-filter-previous-7-days"), "action": "week", - get icon() { - return iconMode === "tabler" ? "calendar-week" : "view-calendar-week"; - } + "icon": iconMode === "tabler" ? "calendar-week" : "view-calendar-week" } ] @@ -284,17 +268,18 @@ Item { let currentGroup = ""; + const catMap = { + "Images": "image", + "Links": "link", + "Files": "file", + "Code": "code", + "Colors": "color" + }; + // Filter and format results items.forEach(function (item) { // Category filter if (Settings.data.appLauncher.enableClipboardChips && root.selectedCategory !== "All") { - const catMap = { - "Images": "image", - "Links": "link", - "Files": "file", - "Code": "code", - "Colors": "color" - }; if (item.contentType !== catMap[root.selectedCategory]) { return; }