diff --git a/clipboard/Main.qml b/clipboard/Main.qml index 34ed56c8c..af76d5e75 100644 --- a/clipboard/Main.qml +++ b/clipboard/Main.qml @@ -96,6 +96,34 @@ Item { // Id of the decode currently in flight ("" when idle). property string decodingId: "" + readonly property int minAutoPasteDelayMs: 100 + readonly property int maxAutoPasteDelayMs: 2000 + readonly property int fallbackAutoPasteDelayMs: 500 + + function configuredAutoPasteDelayMs() { + const cfg = pluginApi?.pluginSettings || ({}); + const defaults = pluginApi?.manifest?.metadata?.defaultSettings || ({}); + const configuredDelay = Number(cfg.autoPasteDelayMs ?? defaults.autoPasteDelayMs ?? root.fallbackAutoPasteDelayMs); + if (isNaN(configuredDelay)) + return root.fallbackAutoPasteDelayMs; + return Math.max(root.minAutoPasteDelayMs, Math.min(root.maxAutoPasteDelayMs, configuredDelay)); + } + + // Sends Ctrl+V after the panel has closed and focus has returned to the + // previously active window. The explicit Wayland environment keeps wtype + // working when Noctalia is launched as a user service. + function pasteIntoPreviousWindow() { + if (pasteProc.running) + return; + const delayMs = root.configuredAutoPasteDelayMs(); + const runtimeDir = Quickshell.env("XDG_RUNTIME_DIR") || ""; + const waylandDisplay = Quickshell.env("WAYLAND_DISPLAY") || ""; + pasteProc.command = ["bash", "-c", + 'sleep "$1"; XDG_RUNTIME_DIR="$2" WAYLAND_DISPLAY="$3" wtype -M ctrl -P v -p v -m ctrl', + "--", String(delayMs / 1000), runtimeDir, waylandDisplay]; + pasteProc.running = true; + } + // Pending ids waiting for decodeProc to become free. FIFO — delegates // are rendered top-to-bottom so the first enqueued id is the most // visible one. @@ -408,6 +436,21 @@ Item { stderr: StdioCollector {} } + // Sends the paste shortcut to the window focused after the panel closes. + Process { + id: pasteProc + + stdout: StdioCollector {} + stderr: StdioCollector {} + + onExited: exitCode => { + if (exitCode !== 0) { + Logger.w("Clipboard Plugin", "automatic paste failed:", + String(pasteProc.stderr.text).trim() || "exit " + exitCode); + } + } + } + // Deletes a specific entry, then refreshes the list on success. Process { id: removeProc @@ -1139,8 +1182,8 @@ Item { // --- Lifecycle ----------------------------------------------------------- - // Seed the list once the shell has injected pluginApi so maxHistorySize - // resolves to the user's actual setting (not the fallback 100). + // Seed or clear the list once the shell has injected pluginApi so startup + // behavior resolves to the user's actual settings. // // Also ensure the pinned-file parent directory exists. FileView.setText // does not create missing parent directories, so a fresh install with @@ -1150,7 +1193,13 @@ Item { // copied-at.json have a parent to write into. onPluginApiChanged: { if (pluginApi) { - root.refresh(); + const cfg = pluginApi?.pluginSettings || ({}); + const defaults = pluginApi?.manifest?.metadata?.defaultSettings || ({}); + if (cfg.clearHistoryOnStartup ?? defaults.clearHistoryOnStartup ?? false) { + root.wipe(); + } else { + root.refresh(); + } if (root.dataDir) { pinnedDirProc.command = ["mkdir", "-p", root.dataDir]; pinnedDirProc.running = true; @@ -1165,6 +1214,8 @@ Item { listProc.running = false; if (copyProc.running) copyProc.running = false; + if (pasteProc.running) + pasteProc.running = false; if (removeProc.running) removeProc.running = false; if (wipeProc.running) diff --git a/clipboard/Panel.qml b/clipboard/Panel.qml index 286b96d4b..a553f4ea1 100644 --- a/clipboard/Panel.qml +++ b/clipboard/Panel.qml @@ -91,6 +91,11 @@ Item { // Cached settings for reactive use inside bindings. readonly property bool showImagePreviews: pluginApi?.pluginSettings?.showImagePreviews ?? true + function shouldAutoPasteOnSelect() { + const cfg = pluginApi?.pluginSettings || ({}); + const defaults = pluginApi?.manifest?.metadata?.defaultSettings || ({}); + return cfg.autoPasteOnSelect ?? defaults.autoPasteOnSelect ?? false; + } readonly property real densitySpacing: { const d = pluginApi?.pluginSettings?.density ?? "comfortable"; if (d === "compact") return Style.marginXS; @@ -116,6 +121,12 @@ Item { // which clamps to [0, length-1] instead of resetting so the user's // focus stays on the row that slid up into the deleted position. property int selectedIndex: -1 + property bool pasteAfterClose: false + + function closeAfterCopy() { + root.pasteAfterClose = root.shouldAutoPasteOnSelect(); + closePanelTimer.restart(); + } // Sentinel index to restore AFTER a delete-triggered refresh. -1 // means "no pending clamp" (normal reset semantics apply). Any @@ -271,7 +282,11 @@ Item { Logger.w("Clipboard Plugin", "activateSelection: no entry at index", idx); return; } - root.pluginApi?.mainInstance?.copy(entry.id); + if (entry.pinned && entry.pinnedIndex >= 0) { + root.pluginApi?.mainInstance?.copyPinned(entry.pinnedIndex); + } else { + root.pluginApi?.mainInstance?.copy(entry.id); + } const typeSlug = entry.type === "file" ? "file" : entry.type === "image" ? "image" : "text"; @@ -279,7 +294,7 @@ Item { root.pluginApi?.tr("toast.item-copied-" + typeSlug + "-title"), root.pluginApi?.tr("toast.item-copied-" + typeSlug + "-body") ); - closePanelTimer.restart(); + root.closeAfterCopy(); } // Pin / unpin the selected row. Toggles based on the entry's @@ -650,6 +665,10 @@ Item { // the Quickshell log without flooding on repeat attempts. Logger.w("Clipboard Plugin", "closePanel returned false — screen:", targetScreen?.name ?? "null"); } + if (ok && root.pasteAfterClose) { + root.pluginApi?.mainInstance?.pasteIntoPreviousWindow(); + } + root.pasteAfterClose = false; } } @@ -1152,7 +1171,9 @@ Item { // (reuseItems: true) cannot carry a stale screen // reference through the deferred-close timer — see the // closePanelTimer commentary above. - onCopied: closePanelTimer.restart() + onCopied: { + root.closeAfterCopy(); + } } // A subtle scrollbar keeps the user oriented in a long list. @@ -1203,7 +1224,9 @@ Item { // background Rectangle) — so grid cells highlight // consistently on the Images tab. selected: index === root.selectedIndex - onCopied: closePanelTimer.restart() + onCopied: { + root.closeAfterCopy(); + } } } diff --git a/clipboard/README.md b/clipboard/README.md index 9e3bc0349..ee2356fa6 100644 --- a/clipboard/README.md +++ b/clipboard/README.md @@ -20,6 +20,7 @@ A clipboard history panel for the [Noctalia](https://github.com/noctalia-dev/noc - Noctalia `4.1.2`+ - [cliphist](https://github.com/sentriz/cliphist) - `wl-clipboard` +- `wtype` (optional, required for automatic paste after selection) ## Installation @@ -42,6 +43,9 @@ Settings are stored in `~/.config/noctalia/plugins/clipboard/settings.json` (cre |---|---|---| | `maxHistorySize` | `100` | Maximum number of entries retained in history. | | `showImagePreviews` | `true` | Show image thumbnails inline in the history panel. | +| `clearHistoryOnStartup` | `false` | Clear regular history whenever the shell starts; pinned items remain. | +| `autoPasteOnSelect` | `false` | Paste the selected entry into the previously focused window. Requires `wtype`. | +| `autoPasteDelayMs` | `500` | Delay before automatic paste (100-2000 ms), allowing focus to return to the previous window. | | `density` | `"comfortable"` | Visual density of the list. `"comfortable"` or `"compact"`. | ## IPC diff --git a/clipboard/Settings.qml b/clipboard/Settings.qml index 02d4eaf7b..98a3270ed 100644 --- a/clipboard/Settings.qml +++ b/clipboard/Settings.qml @@ -13,7 +13,7 @@ import qs.Widgets // - docs/specs/plugin-qml-idioms.md (two-phase save pattern, // null-guarding, widget library, i18n via pluginApi?.tr) // -// Three controls stacked vertically map to the keys in +// Controls stacked vertically map to the keys in // manifest.json metadata.defaultSettings: // maxHistorySize — NSpinBox, 20–500, default 100 // showImagePreviews — NToggle, default true @@ -29,6 +29,8 @@ ColumnLayout { // Injected by the shell after instantiation. Always null-guard every // access with optional chaining (?.) — per docs/specs/plugin-qml-idioms.md. property var pluginApi: null + property var cfg: pluginApi?.pluginSettings || ({}) + property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({}) // --- Pending state ------------------------------------------------------ // @@ -38,9 +40,12 @@ ColumnLayout { // exactly — drift between the two is a real bug (a plugin instance that // predates a defaults change falls back to the QML literal here, not // the manifest value). - property int valueMaxHistorySize: pluginApi?.pluginSettings?.maxHistorySize ?? 100 - property bool valueShowImagePreviews: pluginApi?.pluginSettings?.showImagePreviews ?? true - property string valueDensity: pluginApi?.pluginSettings?.density ?? "comfortable" + property int valueMaxHistorySize: cfg.maxHistorySize ?? defaults.maxHistorySize ?? 100 + property bool valueShowImagePreviews: cfg.showImagePreviews ?? defaults.showImagePreviews ?? true + property bool valueClearHistoryOnStartup: cfg.clearHistoryOnStartup ?? defaults.clearHistoryOnStartup ?? false + property bool valueAutoPasteOnSelect: cfg.autoPasteOnSelect ?? defaults.autoPasteOnSelect ?? false + property int valueAutoPasteDelayMs: cfg.autoPasteDelayMs ?? defaults.autoPasteDelayMs ?? 500 + property string valueDensity: cfg.density ?? defaults.density ?? "comfortable" // --- Controls ----------------------------------------------------------- @@ -69,6 +74,34 @@ ColumnLayout { onToggled: checked => { root.valueShowImagePreviews = checked; } } + NToggle { + Layout.fillWidth: true + label: pluginApi?.tr("settings.clear-history-on-startup") + description: pluginApi?.tr("settings.clear-history-on-startup-description") + checked: root.valueClearHistoryOnStartup + onToggled: checked => { root.valueClearHistoryOnStartup = checked; } + } + + NToggle { + Layout.fillWidth: true + label: pluginApi?.tr("settings.auto-paste-on-select") + description: pluginApi?.tr("settings.auto-paste-on-select-description") + checked: root.valueAutoPasteOnSelect + onToggled: checked => { root.valueAutoPasteOnSelect = checked; } + } + + NSpinBox { + Layout.fillWidth: true + visible: root.valueAutoPasteOnSelect + label: pluginApi?.tr("settings.auto-paste-delay") + description: pluginApi?.tr("settings.auto-paste-delay-description") + from: 100 + to: 2000 + stepSize: 100 + value: root.valueAutoPasteDelayMs + onValueChanged: root.valueAutoPasteDelayMs = value + } + // density — enum of "compact" / "comfortable" / "spacious". The // NComboBox model uses { key, name } entries; we bind selection through // currentKey so the stored value is the enum string itself (matching @@ -102,6 +135,9 @@ ColumnLayout { } pluginApi.pluginSettings.maxHistorySize = root.valueMaxHistorySize; pluginApi.pluginSettings.showImagePreviews = root.valueShowImagePreviews; + pluginApi.pluginSettings.clearHistoryOnStartup = root.valueClearHistoryOnStartup; + pluginApi.pluginSettings.autoPasteOnSelect = root.valueAutoPasteOnSelect; + pluginApi.pluginSettings.autoPasteDelayMs = root.valueAutoPasteDelayMs; pluginApi.pluginSettings.density = root.valueDensity; pluginApi.saveSettings(); } diff --git a/clipboard/i18n/en.json b/clipboard/i18n/en.json index e33cc6c9a..743b68017 100644 --- a/clipboard/i18n/en.json +++ b/clipboard/i18n/en.json @@ -30,6 +30,12 @@ "max-history-size-description": "Number of clipboard entries kept in history. Takes effect on the next refresh.", "show-image-previews": "Show image previews", "show-image-previews-description": "Decode and display thumbnails for image entries. Disable on low-memory systems.", + "clear-history-on-startup": "Clear history on startup", + "clear-history-on-startup-description": "Clear regular history whenever Noctalia Shell starts or restarts. Pinned items are preserved.", + "auto-paste-on-select": "Paste automatically on selection", + "auto-paste-on-select-description": "Close the panel and send Ctrl+V to the previous window after selecting an entry.", + "auto-paste-delay": "Automatic paste delay", + "auto-paste-delay-description": "Milliseconds to wait for focus to return before sending Ctrl+V (100-2000 ms).", "density": "Density", "density-description": "Spacing of clipboard entries in the panel.", "density-compact": "Compact", diff --git a/clipboard/i18n/pt.json b/clipboard/i18n/pt.json index f9ecac912..f8cf95c22 100644 --- a/clipboard/i18n/pt.json +++ b/clipboard/i18n/pt.json @@ -1,22 +1,22 @@ { "bar": { - "tooltip": "Historico da area de transferencia" + "tooltip": "Histórico da área de transferência" }, "context": { - "toggle": "Alternar historico da area de transferencia", - "settings": "Abrir configuracoes" + "toggle": "Alternar histórico da área de transferência", + "settings": "Abrir configurações" }, "panel": { - "title": "Historico da area de transferencia", - "empty": "Nenhum historico de area de transferencia ainda", - "no-matches": "Sem correspondencias", + "title": "Histórico da área de transferência", + "empty": "Nenhum histórico da área de transferência ainda", + "no-matches": "Sem correspondências", "search-placeholder": "Pesquisar...", "delete": "Excluir entrada", "pin": "Fixar entrada", "unpin": "Desfixar entrada", - "wipe": "Limpar historico", + "wipe": "Limpar histórico", "close": "Fechar", - "settings": "Configuracoes", + "settings": "Configurações", "filter-text": "Texto", "filter-images": "Imagens", "filter-files": "Arquivos", @@ -26,31 +26,37 @@ }, "settings": { "tab-general": "Geral", - "max-history-size": "Tamanho maximo do historico", - "max-history-size-description": "Numero de entradas da area de transferencia no historico. Entra em vigor na proxima atualizacao.", - "show-image-previews": "Mostrar pre-visualizacoes de imagens", - "show-image-previews-description": "Decodificar e exibir miniaturas para entradas de imagem. Desativar em sistemas com pouca memoria.", + "max-history-size": "Tamanho máximo do histórico", + "max-history-size-description": "Número de entradas da área de transferência no histórico. Entra em vigor na próxima atualização.", + "show-image-previews": "Mostrar pré-visualizações de imagens", + "show-image-previews-description": "Decodificar e exibir miniaturas para entradas de imagem. Desativar em sistemas com pouca memória.", + "clear-history-on-startup": "Limpar histórico ao iniciar", + "clear-history-on-startup-description": "Limpar o histórico comum sempre que o Noctalia Shell iniciar ou reiniciar. Itens fixados são preservados.", + "auto-paste-on-select": "Colar automaticamente ao selecionar", + "auto-paste-on-select-description": "Fechar o painel e enviar Ctrl+V para a janela anterior depois de selecionar uma entrada.", + "auto-paste-delay": "Atraso da colagem automática", + "auto-paste-delay-description": "Milissegundos de espera para o foco retornar antes de enviar Ctrl+V (100-2000 ms).", "density": "Densidade", - "density-description": "Espacamento das entradas da area de transferencia no painel.", + "density-description": "Espaçamento das entradas da área de transferência no painel.", "density-compact": "Compacto", - "density-comfortable": "Confortavel", - "density-spacious": "Espacoso" + "density-comfortable": "Confortável", + "density-spacious": "Espaçoso" }, "toast": { "item-copied-text-title": "Texto copiado", - "item-copied-text-body": "Voce pode colar o texto da area de transferencia.", + "item-copied-text-body": "Você pode colar o texto da área de transferência.", "item-copied-image-title": "Imagem copiada", - "item-copied-image-body": "Voce pode colar a imagem da area de transferencia.", + "item-copied-image-body": "Você pode colar a imagem da área de transferência.", "item-copied-file-title": "Arquivo copiado", - "item-copied-file-body": "Voce pode colar o arquivo da area de transferencia.", + "item-copied-file-body": "Você pode colar o arquivo da área de transferência.", "item-pinned": "Entrada fixada", "item-unpinned": "Entrada desfixada", - "history-cleared": "Historico da area de transferencia limpo" + "history-cleared": "Histórico da área de transferência limpo" }, "time": { "just-now": "agora mesmo", - "minutes-ago": "ha {minutes}m", - "hours-ago": "ha {hours}h", + "minutes-ago": "há {minutes}m", + "hours-ago": "há {hours}h", "yesterday": "ontem" } } diff --git a/clipboard/manifest.json b/clipboard/manifest.json index 776056a54..bfae0d497 100644 --- a/clipboard/manifest.json +++ b/clipboard/manifest.json @@ -1,7 +1,7 @@ { "id": "clipboard", "name": "Clipboard", - "version": "1.0.3", + "version": "1.1.0", "minNoctaliaVersion": "4.1.2", "author": "yanekyuk", "repository": "https://github.com/noctalia-dev/noctalia-plugins", @@ -25,6 +25,9 @@ "defaultSettings": { "maxHistorySize": 100, "showImagePreviews": true, + "clearHistoryOnStartup": false, + "autoPasteOnSelect": false, + "autoPasteDelayMs": 500, "density": "comfortable" } }