From 455ef3449e670442b7788949173787cba21b8459 Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Tue, 18 Nov 2025 02:02:54 +0800 Subject: [PATCH 01/19] feat: clip preview --- Modules/Panels/Launcher/Launcher.qml | 568 ++++++++++-------- .../Launcher/Plugins/ClipboardPlugin.qml | 7 +- Widgets/ClipboardPreview.qml | 53 ++ Widgets/NDivider.qml | 8 +- 4 files changed, 382 insertions(+), 254 deletions(-) create mode 100644 Widgets/ClipboardPreview.qml diff --git a/Modules/Panels/Launcher/Launcher.qml b/Modules/Panels/Launcher/Launcher.qml index ed037b5a..4a89dafd 100644 --- a/Modules/Panels/Launcher/Launcher.qml +++ b/Modules/Panels/Launcher/Launcher.qml @@ -13,12 +13,23 @@ import qs.Widgets SmartPanel { id: root + readonly property int baseWidth: Math.round(500 * Style.uiScaleRatio) + readonly property int previewWidth: Math.round(400 * Style.uiScaleRatio) + readonly property bool previewActive: activePlugin && activePlugin.name === I18n.tr("plugins.clipboard") && results.length > 0 + // Panel configuration - preferredWidth: Math.round(500 * Style.uiScaleRatio) + preferredWidth: baseWidth + (previewActive ? previewWidth : 0) preferredHeight: Math.round(600 * Style.uiScaleRatio) preferredWidthRatio: 0.3 preferredHeightRatio: 0.5 + Behavior on preferredWidth { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCirc + } + } + // Positioning readonly property string panelPosition: { if (Settings.data.appLauncher.position === "follow_bar") { @@ -329,298 +340,357 @@ SmartPanel { } } - ColumnLayout { + RowLayout { anchors.fill: parent - anchors.margins: Style.marginL - spacing: Style.marginM + spacing: 0 - NTextInput { - id: searchInput - Layout.fillWidth: true - - fontSize: Style.fontSizeL - fontWeight: Style.fontWeightSemiBold - - text: searchText - placeholderText: I18n.tr("placeholders.search-launcher") - - onTextChanged: searchText = text - - Component.onCompleted: { - if (searchInput.inputItem) { - searchInput.inputItem.forceActiveFocus(); - // Intercept Tab keys before TextField handles them - searchInput.inputItem.Keys.onPressed.connect(function (event) { - if (event.key === Qt.Key_Tab) { - root.onTabPressed(); - event.accepted = true; - } else if (event.key === Qt.Key_Backtab) { - root.onBackTabPressed(); - event.accepted = true; - } - }); - } - } - } - - // Results list - NListView { - id: resultsList - - horizontalPolicy: ScrollBar.AlwaysOff - verticalPolicy: ScrollBar.AsNeeded - - Layout.fillWidth: true + // --- Left Pane --- + ColumnLayout { + id: leftPane Layout.fillHeight: true - spacing: Style.marginXXS - model: results - currentIndex: selectedIndex - cacheBuffer: resultsList.height * 2 - onCurrentIndexChanged: { - cancelFlick(); - if (currentIndex >= 0) { - positionViewAtIndex(currentIndex, ListView.Contain); + Layout.preferredWidth: root.baseWidth + anchors.margins: Style.marginL + spacing: Style.marginM + + NTextInput { + id: searchInput + Layout.fillWidth: true + + fontSize: Style.fontSizeL + fontWeight: Style.fontWeightSemiBold + + text: searchText + placeholderText: I18n.tr("placeholders.search-launcher") + + onTextChanged: searchText = text + + Component.onCompleted: { + if (searchInput.inputItem) { + searchInput.inputItem.forceActiveFocus(); + // Intercept Tab keys before TextField handles them + searchInput.inputItem.Keys.onPressed.connect(function (event) { + if (event.key === Qt.Key_Tab) { + root.onTabPressed(); + event.accepted = true; + } else if (event.key === Qt.Key_Backtab) { + root.onBackTabPressed(); + event.accepted = true; + } + }); + } } } - onModelChanged: {} - delegate: Rectangle { - id: entry + // Results list + NListView { + id: resultsList - property bool isSelected: (!root.ignoreMouseHover && mouseArea.containsMouse) || (index === selectedIndex) - // Accessor for app id - property string appId: (modelData && modelData.appId) ? String(modelData.appId) : "" + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AsNeeded - // Pin helpers - function togglePin(appId) { - if (!appId) - return; - let arr = (Settings.data.dock.pinnedApps || []).slice(); - const idx = arr.indexOf(appId); - if (idx >= 0) - arr.splice(idx, 1); - else - arr.push(appId); - Settings.data.dock.pinnedApps = arr; - } - - function isPinned(appId) { - const arr = Settings.data.dock.pinnedApps || []; - return appId && arr.indexOf(appId) >= 0; - } - - // Property to reliably track the current item's ID. - // This changes whenever the delegate is recycled for a new item. - property var currentClipboardId: modelData.isImage ? modelData.clipboardId : "" - - // When this delegate is assigned a new image item, trigger the decode. - onCurrentClipboardIdChanged: { - // Check if it's a valid ID and if the data isn't already cached. - if (currentClipboardId && !ClipboardService.getImageData(currentClipboardId)) { - ClipboardService.decodeToDataUrl(currentClipboardId, modelData.mime, null); + Layout.fillWidth: true + Layout.fillHeight: true + spacing: Style.marginXXS + model: results + currentIndex: selectedIndex + cacheBuffer: resultsList.height * 2 + onCurrentIndexChanged: { + cancelFlick(); + if (currentIndex >= 0) { + positionViewAtIndex(currentIndex, ListView.Contain); + } + // Pass data to preview loader + if (clipboardPreviewLoader.item) { + clipboardPreviewLoader.item.currentItem = results[currentIndex] || null; } } + onModelChanged: {} - width: resultsList.width - Style.marginS - implicitHeight: entryHeight - radius: Style.radiusM - color: entry.isSelected ? Color.mHover : Color.mSurface + delegate: Rectangle { + id: entry - Behavior on color { - ColorAnimation { - duration: Style.animationFast - easing.type: Easing.OutCirc + property bool isSelected: (!root.ignoreMouseHover && mouseArea.containsMouse) || (index === selectedIndex) + // Accessor for app id + property string appId: (modelData && modelData.appId) ? String(modelData.appId) : "" + + // Pin helpers + function togglePin(appId) { + if (!appId) + return; + let arr = (Settings.data.dock.pinnedApps || []).slice(); + const idx = arr.indexOf(appId); + if (idx >= 0) + arr.splice(idx, 1); + else + arr.push(appId); + Settings.data.dock.pinnedApps = arr; } - } - ColumnLayout { - id: contentLayout - anchors.fill: parent - anchors.margins: Style.marginM - spacing: Style.marginM + function isPinned(appId) { + const arr = Settings.data.dock.pinnedApps || []; + return appId && arr.indexOf(appId) >= 0; + } - // Top row - Main entry content with pin button - RowLayout { - Layout.fillWidth: true + // Property to reliably track the current item's ID. + // This changes whenever the delegate is recycled for a new item. + property var currentClipboardId: modelData.isImage ? modelData.clipboardId : "" + + // When this delegate is assigned a new image item, trigger the decode. + onCurrentClipboardIdChanged: { + // Check if it's a valid ID and if the data isn't already cached. + if (currentClipboardId && !ClipboardService.getImageData(currentClipboardId)) { + ClipboardService.decodeToDataUrl(currentClipboardId, modelData.mime, null); + } + } + + width: resultsList.width - Style.marginS + implicitHeight: entryHeight + radius: Style.radiusM + 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: Style.marginM spacing: Style.marginM - // Icon badge or Image preview - Rectangle { - Layout.preferredWidth: badgeSize - Layout.preferredHeight: badgeSize - radius: Style.radiusM - color: Color.mSurfaceVariant + // Top row - Main entry content with pin button + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM - // Image preview for clipboard images - NImageRounded { - id: imagePreview - anchors.fill: parent - visible: modelData.isImage - imageRadius: Style.radiusM - - // This property creates a dependency on the service's revision counter - readonly property int _rev: ClipboardService.revision - - // Fetches from the service's cache. - // The dependency on `_rev` ensures this binding is re-evaluated when the cache is updated. - imagePath: { - _rev; - return ClipboardService.getImageData(modelData.clipboardId) || ""; - } - - // Loading indicator - 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 - } - } - - // Error fallback - onStatusChanged: status => { - if (status === Image.Error) { - iconLoader.visible = true; - imagePreview.visible = false; - } - } - } - - // Icon fallback - Loader { - id: iconLoader - anchors.fill: parent - anchors.margins: Style.marginXS - - visible: !modelData.isImage || imagePreview.status === Image.Error - active: visible - - sourceComponent: Component { - IconImage { - anchors.fill: parent - source: modelData.icon ? ThemeIcons.iconFromName(modelData.icon, "application-x-executable") : "" - visible: modelData.icon && source !== "" - asynchronous: true - } - } - } - - // Fallback text if no icon and no image - NText { - anchors.centerIn: parent - visible: !imagePreview.visible && !iconLoader.visible - text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?" - pointSize: Style.fontSizeXXL - font.weight: Style.fontWeightBold - color: Color.mOnPrimary - } - - // Image type indicator overlay + // Icon badge or Image preview Rectangle { - visible: modelData.isImage && imagePreview.visible - anchors.bottom: parent.bottom - anchors.right: parent.right - anchors.margins: 2 - width: formatLabel.width + 6 - height: formatLabel.height + 2 + Layout.preferredWidth: badgeSize + Layout.preferredHeight: badgeSize radius: Style.radiusM color: Color.mSurfaceVariant + // Image preview for clipboard images + NImageRounded { + id: imagePreview + anchors.fill: parent + visible: modelData.isImage + imageRadius: Style.radiusM + + // This property creates a dependency on the service's revision counter + readonly property int _rev: ClipboardService.revision + + // Fetches from the service's cache. + // The dependency on `_rev` ensures this binding is re-evaluated when the cache is updated. + imagePath: { + _rev; + return ClipboardService.getImageData(modelData.clipboardId) || ""; + } + + // Loading indicator + 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 + } + } + + // Error fallback + onStatusChanged: status => { + if (status === Image.Error) { + iconLoader.visible = true; + imagePreview.visible = false; + } + } + } + + // Icon fallback + Loader { + id: iconLoader + anchors.fill: parent + anchors.margins: Style.marginXS + + visible: !modelData.isImage || imagePreview.status === Image.Error + active: visible + + sourceComponent: Component { + IconImage { + anchors.fill: parent + source: modelData.icon ? ThemeIcons.iconFromName(modelData.icon, "application-x-executable") : "" + visible: modelData.icon && source !== "" + asynchronous: true + } + } + } + + // Fallback text if no icon and no image NText { - id: formatLabel anchors.centerIn: parent - text: { - if (!modelData.isImage) - return ""; - const desc = modelData.description || ""; - const parts = desc.split(" • "); - return parts[0] || "IMG"; + visible: !imagePreview.visible && !iconLoader.visible + text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?" + pointSize: Style.fontSizeXXL + font.weight: Style.fontWeightBold + color: 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 + 6 + height: formatLabel.height + 2 + radius: Style.radiusM + color: Color.mSurfaceVariant + + NText { + id: formatLabel + anchors.centerIn: parent + text: { + if (!modelData.isImage) + return ""; + const desc = modelData.description || ""; + const parts = desc.split(" • "); + return parts[0] || "IMG"; + } + pointSize: Style.fontSizeXXS + color: Color.mPrimary } - pointSize: Style.fontSizeXXS - color: Color.mPrimary } } - } - // 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 + // 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 + Layout.fillWidth: true + } + + NText { + text: modelData.description || "" + pointSize: Style.fontSizeS + color: entry.isSelected ? Color.mOnHover : Color.mOnSurfaceVariant + elide: Text.ElideRight + Layout.fillWidth: true + visible: text !== "" + } } - NText { - text: modelData.description || "" - pointSize: Style.fontSizeS - color: entry.isSelected ? Color.mOnHover : Color.mOnSurfaceVariant - elide: Text.ElideRight - Layout.fillWidth: true - visible: text !== "" + // Pin/Unpin action icon button + NIconButton { + visible: !!entry.appId && !modelData.isImage && entry.isSelected && (Settings.data.dock.monitors && Settings.data.dock.monitors.length > 0) + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + icon: entry.isPinned(entry.appId) ? "unpin" : "pin" + tooltipText: entry.isPinned(entry.appId) ? I18n.tr("launcher.unpin") : I18n.tr("launcher.pin") + onClicked: entry.togglePin(entry.appId) } } - - // Pin/Unpin action icon button - NIconButton { - visible: !!entry.appId && !modelData.isImage && entry.isSelected && (Settings.data.dock.monitors && Settings.data.dock.monitors.length > 0) - Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - icon: entry.isPinned(entry.appId) ? "unpin" : "pin" - tooltipText: entry.isPinned(entry.appId) ? I18n.tr("launcher.unpin") : I18n.tr("launcher.pin") - onClicked: entry.togglePin(entry.appId) - } } - } - MouseArea { - id: mouseArea - anchors.fill: parent - z: -1 - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: { - if (!root.ignoreMouseHover) { - selectedIndex = index; + MouseArea { + id: mouseArea + anchors.fill: parent + z: -1 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: { + if (!root.ignoreMouseHover) { + selectedIndex = index; + } } - } - onClicked: mouse => { - if (mouse.button === Qt.LeftButton) { - selectedIndex = index; - root.activate(); - mouse.accepted = true; + onClicked: mouse => { + if (mouse.button === Qt.LeftButton) { + selectedIndex = index; + root.activate(); + mouse.accepted = true; + } } - } - acceptedButtons: Qt.LeftButton + acceptedButtons: Qt.LeftButton + } } } - } + NDivider { + Layout.fillWidth: true + } + + // Status + NText { + Layout.fillWidth: true + text: { + if (results.length === 0) + return searchText ? "No results" : ""; + const prefix = activePlugin?.name ? `${activePlugin.name}: ` : ""; + return prefix + `${results.length} result${results.length !== 1 ? 's' : ''}`; + } + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + horizontalAlignment: Text.AlignCenter + } + } + + // --- Vertical Divider --- NDivider { - Layout.fillWidth: true + Layout.fillHeight: true + vertical: true + visible: root.previewActive } + + // --- Right Pane (Preview) --- + Item { + Layout.fillHeight: true + Layout.preferredWidth: root.previewActive ? root.previewWidth : 0 + visible: root.previewActive + + Behavior on Layout.preferredWidth { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCirc + } + } + + // --- Preview Loader --- + Loader { + id: clipboardPreviewLoader + anchors.fill: parent + anchors.leftMargin: Style.marginM + anchors.rightMargin: Style.marginL + anchors.topMargin: Style.marginL + anchors.bottomMargin: Style.marginL + active: root.previewActive + source: active ? "../../../Widgets/ClipboardPreview.qml" : "" + + // Access the loaded component to set the current item + onLoaded: { + if (selectedIndex >= 0 && results[selectedIndex] && item) { + item.currentItem = results[selectedIndex]; + } + } - // Status - NText { - Layout.fillWidth: true - text: { - if (results.length === 0) - return searchText ? "No results" : ""; - const prefix = activePlugin?.name ? `${activePlugin.name}: ` : ""; - return prefix + `${results.length} result${results.length !== 1 ? 's' : ''}`; + // When the item is loaded, ensure it gets the current selection + onItemChanged: { + if (item && selectedIndex >= 0 && results[selectedIndex]) { + item.currentItem = results[selectedIndex]; + } + } } - pointSize: Style.fontSizeXS - color: Color.mOnSurfaceVariant - horizontalAlignment: Text.AlignCenter } } } diff --git a/Modules/Panels/Launcher/Plugins/ClipboardPlugin.qml b/Modules/Panels/Launcher/Plugins/ClipboardPlugin.qml index cb2d7b31..50d13065 100644 --- a/Modules/Panels/Launcher/Plugins/ClipboardPlugin.qml +++ b/Modules/Panels/Launcher/Plugins/ClipboardPlugin.qml @@ -218,7 +218,8 @@ Item { "imageWidth": meta ? meta.w : 0, "imageHeight": meta ? meta.h : 0, "clipboardId": item.id, - "mime": item.mime + "mime": item.mime, + "preview": item.preview }; } @@ -250,7 +251,9 @@ Item { "name": title, "description": description, "icon": "text-x-generic", - "isImage": false + "isImage": false, + "clipboardId": item.id, + "preview": preview }; } diff --git a/Widgets/ClipboardPreview.qml b/Widgets/ClipboardPreview.qml new file mode 100644 index 00000000..04b8f176 --- /dev/null +++ b/Widgets/ClipboardPreview.qml @@ -0,0 +1,53 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import qs.Commons +import qs.Widgets + +Item { + id: previewPanel + + property var currentItem: null + + implicitHeight: contentColumn.implicitHeight + Style.marginL * 2 + implicitWidth: 350 // A default width + + Rectangle { + anchors.fill: parent + color: Color.mSurface || "#f5f5f5" + border.color: Color.mOutlineVariant || "#cccccc" + border.width: 1 + radius: Style.radiusM + + ColumnLayout { + id: contentColumn + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + NText { + text: "Preview" + font.weight: Style.fontWeightBold + Layout.fillWidth: true + } + + ScrollView { + Layout.fillWidth: true + Layout.preferredHeight: 400 // A default height + clip: true + + TextArea { + text: { + if (currentItem && currentItem.preview != null) { + return String(currentItem.preview); + } + return ""; + } + readOnly: true + wrapMode: Text.Wrap + font.family: Style.monospaceFontFamily + } + } + } + } +} diff --git a/Widgets/NDivider.qml b/Widgets/NDivider.qml index 737807d7..7320f573 100644 --- a/Widgets/NDivider.qml +++ b/Widgets/NDivider.qml @@ -4,10 +4,12 @@ import Quickshell.Widgets import qs.Commons Rectangle { - width: parent.width - height: Style.borderS + property bool vertical: false + + width: vertical ? Style.borderS : parent.width + height: vertical ? parent.height : Style.borderS gradient: Gradient { - orientation: Gradient.Horizontal + orientation: vertical ? Gradient.Vertical : Gradient.Horizontal GradientStop { position: 0.0 color: Color.transparent From 6eaffb0e65847cb46ec5f107655becaa37640004 Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Tue, 18 Nov 2025 03:16:23 +0800 Subject: [PATCH 02/19] feat: implement full content preview with async loading in ClipboardPreview --- Widgets/ClipboardPreview.qml | 66 +++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 13 deletions(-) diff --git a/Widgets/ClipboardPreview.qml b/Widgets/ClipboardPreview.qml index 04b8f176..f439a5a8 100644 --- a/Widgets/ClipboardPreview.qml +++ b/Widgets/ClipboardPreview.qml @@ -3,15 +3,34 @@ import QtQuick.Layouts import QtQuick.Controls import qs.Commons import qs.Widgets +import qs.Services.Keyboard // Import ClipboardService Item { id: previewPanel property var currentItem: null + property string fullContent: "" + property bool loadingFullContent: false implicitHeight: contentColumn.implicitHeight + Style.marginL * 2 implicitWidth: 350 // A default width + Connections { + target: previewPanel + function onCurrentItemChanged() { + fullContent = ""; // Clear previous content + loadingFullContent = false; + + if (currentItem && currentItem.clipboardId) { + loadingFullContent = true; + ClipboardService.decode(currentItem.clipboardId, function(content) { + fullContent = content; + loadingFullContent = false; + }); + } + } + } + Rectangle { anchors.fill: parent color: Color.mSurface || "#f5f5f5" @@ -26,26 +45,47 @@ Item { spacing: Style.marginM NText { - text: "Preview" + text: currentItem ? (currentItem.name || "Preview") : "Preview" font.weight: Style.fontWeightBold Layout.fillWidth: true + pointSize: Style.fontSizeM + color: Color.mOnSurface + } + + NDivider { + Layout.fillWidth: true } - ScrollView { + Rectangle { // Frame around the content Layout.fillWidth: true - Layout.preferredHeight: 400 // A default height - clip: true + Layout.fillHeight: true + color: Color.mSurfaceVariant || "#e0e0e0" + border.color: Color.mOutline || "#aaaaaa" + border.width: 1 + radius: Style.radiusS - TextArea { - text: { - if (currentItem && currentItem.preview != null) { - return String(currentItem.preview); - } - return ""; + // Loading indicator + BusyIndicator { + anchors.centerIn: parent + running: loadingFullContent + visible: loadingFullContent + width: Style.baseWidgetSize + height: width + } + + ScrollView { + Layout.fillHeight: true // Explicitly fill height + anchors.fill: parent + anchors.margins: Style.marginS + clip: true + visible: !loadingFullContent // Hide scrollview while loading + + TextArea { + Layout.fillHeight: true // Explicitly fill height + text: fullContent // Bind to fullContent + readOnly: true + wrapMode: Text.Wrap } - readOnly: true - wrapMode: Text.Wrap - font.family: Style.monospaceFontFamily } } } From ca89a0dc35898f58fa26cf392fc577c62f88e4ae Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Tue, 18 Nov 2025 03:25:20 +0800 Subject: [PATCH 03/19] fix: Fix the proportions of the list and the preview --- Modules/Panels/Launcher/Launcher.qml | 15 ++++++++++----- Widgets/ClipboardPreview.qml | 1 - 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Modules/Panels/Launcher/Launcher.qml b/Modules/Panels/Launcher/Launcher.qml index 4a89dafd..8185221e 100644 --- a/Modules/Panels/Launcher/Launcher.qml +++ b/Modules/Panels/Launcher/Launcher.qml @@ -13,12 +13,12 @@ import qs.Widgets SmartPanel { id: root - readonly property int baseWidth: Math.round(500 * Style.uiScaleRatio) - readonly property int previewWidth: Math.round(400 * Style.uiScaleRatio) + readonly property real clipListRatio: 0.6 // 60% for the list + readonly property real clipPreviewRatio: 0.4 // 40% for the preview readonly property bool previewActive: activePlugin && activePlugin.name === I18n.tr("plugins.clipboard") && results.length > 0 // Panel configuration - preferredWidth: baseWidth + (previewActive ? previewWidth : 0) + preferredWidth: Math.round(700 * Style.uiScaleRatio) // Base width when both are shown, scaled appropriately preferredHeight: Math.round(600 * Style.uiScaleRatio) preferredWidthRatio: 0.3 preferredHeightRatio: 0.5 @@ -348,7 +348,10 @@ SmartPanel { ColumnLayout { id: leftPane Layout.fillHeight: true - Layout.preferredWidth: root.baseWidth + Layout.fillWidth: root.previewActive // Fill width proportionally when preview is active + Layout.preferredWidth: root.previewActive ? + Math.round(root.clipListRatio * (root.preferredWidth - (Style.marginL * 2))) : + (root.preferredWidth - (Style.marginL * 2)) anchors.margins: Style.marginL spacing: Style.marginM @@ -656,7 +659,9 @@ SmartPanel { // --- Right Pane (Preview) --- Item { Layout.fillHeight: true - Layout.preferredWidth: root.previewActive ? root.previewWidth : 0 + Layout.fillWidth: root.previewActive // Fill width proportionally when preview is active + Layout.preferredWidth: root.previewActive ? + Math.round(root.clipPreviewRatio * (root.preferredWidth - (Style.marginL * 2))) : 0 visible: root.previewActive Behavior on Layout.preferredWidth { diff --git a/Widgets/ClipboardPreview.qml b/Widgets/ClipboardPreview.qml index f439a5a8..4e29ef86 100644 --- a/Widgets/ClipboardPreview.qml +++ b/Widgets/ClipboardPreview.qml @@ -13,7 +13,6 @@ Item { property bool loadingFullContent: false implicitHeight: contentColumn.implicitHeight + Style.marginL * 2 - implicitWidth: 350 // A default width Connections { target: previewPanel From 79f79e0cffdd627879adde439a189f9a409a010d Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Tue, 18 Nov 2025 03:44:34 +0800 Subject: [PATCH 04/19] feat: Add image preview logic --- Modules/Panels/Launcher/Launcher.qml | 15 ++-- .../Launcher/Plugins/ClipboardPlugin.qml | 9 --- Widgets/ClipboardPreview.qml | 78 ++++++++++++++----- Widgets/NImageRounded.qml | 11 +-- 4 files changed, 72 insertions(+), 41 deletions(-) diff --git a/Modules/Panels/Launcher/Launcher.qml b/Modules/Panels/Launcher/Launcher.qml index 8185221e..9654711a 100644 --- a/Modules/Panels/Launcher/Launcher.qml +++ b/Modules/Panels/Launcher/Launcher.qml @@ -13,12 +13,15 @@ import qs.Widgets SmartPanel { id: root - readonly property real clipListRatio: 0.6 // 60% for the list - readonly property real clipPreviewRatio: 0.4 // 40% for the preview readonly property bool previewActive: activePlugin && activePlugin.name === I18n.tr("plugins.clipboard") && results.length > 0 - // Panel configuration - preferredWidth: Math.round(700 * Style.uiScaleRatio) // Base width when both are shown, scaled appropriately + // Panel configuration - use proportional widths + readonly property real clipListRatio: 0.6 // 60% for the list + readonly property real clipPreviewRatio: 0.4 // 40% for the preview + readonly property int totalBaseWidth: Math.round(600 * Style.uiScaleRatio) // Base width when no preview + readonly property int totalExpandedWidth: Math.round(900 * Style.uiScaleRatio) // Width when preview active + + preferredWidth: previewActive ? totalExpandedWidth : totalBaseWidth preferredHeight: Math.round(600 * Style.uiScaleRatio) preferredWidthRatio: 0.3 preferredHeightRatio: 0.5 @@ -348,10 +351,9 @@ SmartPanel { ColumnLayout { id: leftPane Layout.fillHeight: true - Layout.fillWidth: root.previewActive // Fill width proportionally when preview is active Layout.preferredWidth: root.previewActive ? Math.round(root.clipListRatio * (root.preferredWidth - (Style.marginL * 2))) : - (root.preferredWidth - (Style.marginL * 2)) + (root.preferredWidth - (Style.marginL * 2)) // Use full width when preview not active anchors.margins: Style.marginL spacing: Style.marginM @@ -659,7 +661,6 @@ SmartPanel { // --- Right Pane (Preview) --- Item { Layout.fillHeight: true - Layout.fillWidth: root.previewActive // Fill width proportionally when preview is active Layout.preferredWidth: root.previewActive ? Math.round(root.clipPreviewRatio * (root.preferredWidth - (Style.marginL * 2))) : 0 visible: root.previewActive diff --git a/Modules/Panels/Launcher/Plugins/ClipboardPlugin.qml b/Modules/Panels/Launcher/Plugins/ClipboardPlugin.qml index 50d13065..df485e11 100644 --- a/Modules/Panels/Launcher/Plugins/ClipboardPlugin.qml +++ b/Modules/Panels/Launcher/Plugins/ClipboardPlugin.qml @@ -204,12 +204,9 @@ Item { return results; } - // Helper: Format image clipboard entry function formatImageEntry(item) { const meta = parseImageMeta(item.preview); - // The launcher's delegate will now be responsible for fetching the image data. - // This function's role is to provide the necessary metadata for that request. return { "name": meta ? `Image ${meta.w}×${meta.h}` : "Image", "description": meta ? `${meta.fmt} • ${meta.size}` : item.mime || "Image data", @@ -223,18 +220,15 @@ Item { }; } - // Helper: Format text clipboard entry with preview function formatTextEntry(item) { const preview = (item.preview || "").trim(); const lines = preview.split('\n').filter(l => l.trim()); - // Use first line as title, limit length let title = lines[0] || "Empty text"; if (title.length > 60) { title = title.substring(0, 57) + "..."; } - // Use second line or character count as description let description = ""; if (lines.length > 1) { description = lines[1]; @@ -257,7 +251,6 @@ Item { }; } - // Helper: Parse image metadata from 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); @@ -274,8 +267,6 @@ Item { }; } - // Public method to get image data for a clipboard item - // This can be called by the launcher when rendering function getImageForItem(clipboardId) { return ClipboardService.getImageData ? ClipboardService.getImageData(clipboardId) : null; } diff --git a/Widgets/ClipboardPreview.qml b/Widgets/ClipboardPreview.qml index 4e29ef86..0e15abab 100644 --- a/Widgets/ClipboardPreview.qml +++ b/Widgets/ClipboardPreview.qml @@ -3,29 +3,63 @@ import QtQuick.Layouts import QtQuick.Controls import qs.Commons import qs.Widgets -import qs.Services.Keyboard // Import ClipboardService +import qs.Services.Keyboard Item { id: previewPanel property var currentItem: null property string fullContent: "" + property string imageDataUrl: "" property bool loadingFullContent: false + property bool isImageContent: false implicitHeight: contentColumn.implicitHeight + Style.marginL * 2 Connections { target: previewPanel function onCurrentItemChanged() { - fullContent = ""; // Clear previous content + fullContent = ""; + imageDataUrl = ""; loadingFullContent = false; + isImageContent = currentItem && currentItem.isImage; if (currentItem && currentItem.clipboardId) { - loadingFullContent = true; - ClipboardService.decode(currentItem.clipboardId, function(content) { - fullContent = content; - loadingFullContent = false; - }); + if (isImageContent) { + imageDataUrl = ClipboardService.getImageData(currentItem.clipboardId) || ""; + loadingFullContent = !imageDataUrl; + + if (!imageDataUrl && currentItem.mime) { + ClipboardService.decodeToDataUrl(currentItem.clipboardId, currentItem.mime, null); + } + } else { + loadingFullContent = true; + ClipboardService.decode(currentItem.clipboardId, function(content) { + fullContent = content; + loadingFullContent = false; + }); + } + } + } + } + + readonly property int _rev: ClipboardService.revision + + Timer { + id: imageUpdateTimer + interval: 200 + running: currentItem && currentItem.isImage && imageDataUrl === "" + repeat: currentItem && currentItem.isImage && imageDataUrl === "" + + onTriggered: { + if (currentItem && currentItem.clipboardId) { + const newData = ClipboardService.getImageData(currentItem.clipboardId) || ""; + if (newData !== imageDataUrl) { + imageDataUrl = newData; + if (newData) { + loadingFullContent = false; + } + } } } } @@ -55,7 +89,7 @@ Item { Layout.fillWidth: true } - Rectangle { // Frame around the content + Rectangle { Layout.fillWidth: true Layout.fillHeight: true color: Color.mSurfaceVariant || "#e0e0e0" @@ -63,7 +97,6 @@ Item { border.width: 1 radius: Style.radiusS - // Loading indicator BusyIndicator { anchors.centerIn: parent running: loadingFullContent @@ -72,18 +105,27 @@ Item { height: width } - ScrollView { - Layout.fillHeight: true // Explicitly fill height + Item { anchors.fill: parent anchors.margins: Style.marginS - clip: true - visible: !loadingFullContent // Hide scrollview while loading - TextArea { - Layout.fillHeight: true // Explicitly fill height - text: fullContent // Bind to fullContent - readOnly: true - wrapMode: Text.Wrap + NImageRounded { + anchors.fill: parent + imagePath: imageDataUrl + visible: isImageContent && !loadingFullContent && imageDataUrl !== "" + imageRadius: Style.radiusS + } + + ScrollView { + anchors.fill: parent + clip: true + visible: !isImageContent && !loadingFullContent + + TextArea { + text: fullContent + readOnly: true + wrapMode: Text.Wrap + } } } } diff --git a/Widgets/NImageRounded.qml b/Widgets/NImageRounded.qml index a7a5789f..76770f58 100644 --- a/Widgets/NImageRounded.qml +++ b/Widgets/NImageRounded.qml @@ -30,12 +30,14 @@ Rectangle { id: img anchors.fill: parent source: imagePath - visible: false // Hide since we're using it as shader source + visible: false mipmap: true smooth: true asynchronous: true antialiasing: true - fillMode: Image.PreserveAspectCrop + fillMode: Image.PreserveAspectFit + horizontalAlignment: Image.AlignHCenter + verticalAlignment: Image.AlignVCenter onStatusChanged: root.statusChanged(status) } @@ -51,17 +53,14 @@ Rectangle { format: ShaderEffectSource.RGBA } - // Use custom property names to avoid conflicts with final properties property real itemWidth: root.width property real itemHeight: root.height property real cornerRadius: root.radius property real imageOpacity: root.opacity fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/rounded_image.frag.qsb") - // Qt6 specific properties - ensure proper blending supportsAtlasTextures: false blending: true - // Make sure the background is transparent Rectangle { id: background anchors.fill: parent @@ -70,7 +69,6 @@ Rectangle { } } - // Fallback icon Loader { active: fallbackIcon !== undefined && fallbackIcon !== "" && (imagePath === undefined || imagePath === "") anchors.centerIn: parent @@ -83,7 +81,6 @@ Rectangle { } } - // Border Rectangle { anchors.fill: parent radius: parent.radius From 12766e411d71b8d57eb24479eda2a4bfe1c28fc6 Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Tue, 18 Nov 2025 04:28:50 +0800 Subject: [PATCH 05/19] widget: new fillMode and smt --- .../Panels/Launcher}/ClipboardPreview.qml | 1 + Modules/Panels/Launcher/Launcher.qml | 31 ++++++++----------- Widgets/NImageRounded.qml | 3 +- 3 files changed, 16 insertions(+), 19 deletions(-) rename {Widgets => Modules/Panels/Launcher}/ClipboardPreview.qml (98%) diff --git a/Widgets/ClipboardPreview.qml b/Modules/Panels/Launcher/ClipboardPreview.qml similarity index 98% rename from Widgets/ClipboardPreview.qml rename to Modules/Panels/Launcher/ClipboardPreview.qml index 0e15abab..12254afa 100644 --- a/Widgets/ClipboardPreview.qml +++ b/Modules/Panels/Launcher/ClipboardPreview.qml @@ -114,6 +114,7 @@ Item { imagePath: imageDataUrl visible: isImageContent && !loadingFullContent && imageDataUrl !== "" imageRadius: Style.radiusS + imageFillMode: Image.PreserveAspectFit } ScrollView { diff --git a/Modules/Panels/Launcher/Launcher.qml b/Modules/Panels/Launcher/Launcher.qml index 9654711a..2151fcfa 100644 --- a/Modules/Panels/Launcher/Launcher.qml +++ b/Modules/Panels/Launcher/Launcher.qml @@ -15,11 +15,13 @@ SmartPanel { readonly property bool previewActive: activePlugin && activePlugin.name === I18n.tr("plugins.clipboard") && results.length > 0 - // Panel configuration - use proportional widths - readonly property real clipListRatio: 0.6 // 60% for the list - readonly property real clipPreviewRatio: 0.4 // 40% for the preview - readonly property int totalBaseWidth: Math.round(600 * Style.uiScaleRatio) // Base width when no preview - readonly property int totalExpandedWidth: Math.round(900 * Style.uiScaleRatio) // Width when preview active + // Panel configuration + readonly property int listPanelWidth: Math.round(600 * Style.uiScaleRatio) + readonly property int previewPanelWidth: Math.round(300 * Style.uiScaleRatio) + readonly property int dividerWidth: Style.borderS + + readonly property int totalBaseWidth: listPanelWidth + (Style.marginL * 2) + readonly property int totalExpandedWidth: listPanelWidth + dividerWidth + previewPanelWidth + (Style.marginL * 2) + (Style.marginM * 2) preferredWidth: previewActive ? totalExpandedWidth : totalBaseWidth preferredHeight: Math.round(600 * Style.uiScaleRatio) @@ -345,16 +347,14 @@ SmartPanel { RowLayout { anchors.fill: parent - spacing: 0 + anchors.margins: Style.marginL // Apply overall margins here + spacing: Style.marginM // Apply spacing between elements here // --- Left Pane --- ColumnLayout { id: leftPane Layout.fillHeight: true - Layout.preferredWidth: root.previewActive ? - Math.round(root.clipListRatio * (root.preferredWidth - (Style.marginL * 2))) : - (root.preferredWidth - (Style.marginL * 2)) // Use full width when preview not active - anchors.margins: Style.marginL + Layout.preferredWidth: root.listPanelWidth spacing: Style.marginM NTextInput { @@ -404,7 +404,6 @@ SmartPanel { if (currentIndex >= 0) { positionViewAtIndex(currentIndex, ListView.Contain); } - // Pass data to preview loader if (clipboardPreviewLoader.item) { clipboardPreviewLoader.item.currentItem = results[currentIndex] || null; } @@ -656,13 +655,13 @@ SmartPanel { Layout.fillHeight: true vertical: true visible: root.previewActive + Layout.preferredWidth: root.dividerWidth } // --- Right Pane (Preview) --- Item { Layout.fillHeight: true - Layout.preferredWidth: root.previewActive ? - Math.round(root.clipPreviewRatio * (root.preferredWidth - (Style.marginL * 2))) : 0 + Layout.preferredWidth: root.previewActive ? root.previewPanelWidth : 0 visible: root.previewActive Behavior on Layout.preferredWidth { @@ -676,12 +675,8 @@ SmartPanel { Loader { id: clipboardPreviewLoader anchors.fill: parent - anchors.leftMargin: Style.marginM - anchors.rightMargin: Style.marginL - anchors.topMargin: Style.marginL - anchors.bottomMargin: Style.marginL active: root.previewActive - source: active ? "../../../Widgets/ClipboardPreview.qml" : "" + source: active ? "./ClipboardPreview.qml" : "" // Access the loaded component to set the current item onLoaded: { diff --git a/Widgets/NImageRounded.qml b/Widgets/NImageRounded.qml index 76770f58..67d3a178 100644 --- a/Widgets/NImageRounded.qml +++ b/Widgets/NImageRounded.qml @@ -11,6 +11,7 @@ Rectangle { property color borderColor: Color.transparent property real borderWidth: 0 property real imageRadius: width * 0.5 + property int imageFillMode: Image.PreserveAspectCrop property string fallbackIcon: "" property real fallbackIconSize: Style.fontSizeXXL @@ -35,7 +36,7 @@ Rectangle { smooth: true asynchronous: true antialiasing: true - fillMode: Image.PreserveAspectFit + fillMode: root.imageFillMode horizontalAlignment: Image.AlignHCenter verticalAlignment: Image.AlignVCenter From 529869f7963779261e8ead7f6c3a2f0bf8126b78 Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Tue, 18 Nov 2025 20:14:18 +0800 Subject: [PATCH 06/19] fix: External clipboard preview panel positioning and styling --- Modules/Panels/Launcher/Launcher.qml | 109 ++++++++++----------------- 1 file changed, 40 insertions(+), 69 deletions(-) diff --git a/Modules/Panels/Launcher/Launcher.qml b/Modules/Panels/Launcher/Launcher.qml index 2151fcfa..3de1c721 100644 --- a/Modules/Panels/Launcher/Launcher.qml +++ b/Modules/Panels/Launcher/Launcher.qml @@ -13,28 +13,18 @@ import qs.Widgets SmartPanel { id: root - readonly property bool previewActive: activePlugin && activePlugin.name === I18n.tr("plugins.clipboard") && results.length > 0 + readonly property bool previewActive: searchText.startsWith(">clip") // Panel configuration readonly property int listPanelWidth: Math.round(600 * Style.uiScaleRatio) readonly property int previewPanelWidth: Math.round(300 * Style.uiScaleRatio) - readonly property int dividerWidth: Style.borderS - readonly property int totalBaseWidth: listPanelWidth + (Style.marginL * 2) - readonly property int totalExpandedWidth: listPanelWidth + dividerWidth + previewPanelWidth + (Style.marginL * 2) + (Style.marginM * 2) - preferredWidth: previewActive ? totalExpandedWidth : totalBaseWidth + preferredWidth: totalBaseWidth preferredHeight: Math.round(600 * Style.uiScaleRatio) preferredWidthRatio: 0.3 preferredHeightRatio: 0.5 - Behavior on preferredWidth { - NumberAnimation { - duration: Style.animationFast - easing.type: Easing.OutCirc - } - } - // Positioning readonly property string panelPosition: { if (Settings.data.appLauncher.position === "follow_bar") { @@ -278,13 +268,48 @@ SmartPanel { } } - // UI panelContent: Rectangle { id: ui color: Color.transparent opacity: resultsReady ? 1.0 : 0.0 - // Global MouseArea to detect mouse movement + // Preview Panel (external) + NBox { + id: previewBox + visible: root.previewActive + width: root.previewPanelWidth + height: ui.height + x: ui.width + Style.marginM + y: 0 + z: -1 // Draw behind main panel content if it ever overlaps + + opacity: visible ? 1.0 : 0.0 + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + + Loader { + id: clipboardPreviewLoader + anchors.fill: parent + active: root.previewActive + source: active ? "./ClipboardPreview.qml" : "" + + onLoaded: { + if (selectedIndex >= 0 && results[selectedIndex] && item) { + item.currentItem = results[selectedIndex]; + } + } + + onItemChanged: { + if (item && selectedIndex >= 0 && results[selectedIndex]) { + item.currentItem = results[selectedIndex]; + } + } + } + } + MouseArea { id: mouseMovementDetector anchors.fill: parent @@ -298,7 +323,6 @@ SmartPanel { property bool initialized: false onPositionChanged: mouse => { - // Store initial position if (!initialized) { lastX = mouse.x; lastY = mouse.y; @@ -306,7 +330,6 @@ SmartPanel { return; } - // Check if mouse actually moved const deltaX = Math.abs(mouse.x - lastX); const deltaY = Math.abs(mouse.y - lastY); if (deltaX > 1 || deltaY > 1) { @@ -316,7 +339,6 @@ SmartPanel { } } - // Reset when launcher opens Connections { target: root function onOpened() { @@ -350,7 +372,7 @@ SmartPanel { anchors.margins: Style.marginL // Apply overall margins here spacing: Style.marginM // Apply spacing between elements here - // --- Left Pane --- + // Left Pane ColumnLayout { id: leftPane Layout.fillHeight: true @@ -386,7 +408,6 @@ SmartPanel { } } - // Results list NListView { id: resultsList @@ -414,7 +435,6 @@ SmartPanel { id: entry property bool isSelected: (!root.ignoreMouseHover && mouseArea.containsMouse) || (index === selectedIndex) - // Accessor for app id property string appId: (modelData && modelData.appId) ? String(modelData.appId) : "" // Pin helpers @@ -494,7 +514,6 @@ SmartPanel { return ClipboardService.getImageData(modelData.clipboardId) || ""; } - // Loading indicator Rectangle { anchors.fill: parent visible: parent.status === Image.Loading @@ -508,7 +527,6 @@ SmartPanel { } } - // Error fallback onStatusChanged: status => { if (status === Image.Error) { iconLoader.visible = true; @@ -517,7 +535,6 @@ SmartPanel { } } - // Icon fallback Loader { id: iconLoader anchors.fill: parent @@ -536,7 +553,6 @@ SmartPanel { } } - // Fallback text if no icon and no image NText { anchors.centerIn: parent visible: !imagePreview.visible && !iconLoader.visible @@ -635,7 +651,6 @@ SmartPanel { Layout.fillWidth: true } - // Status NText { Layout.fillWidth: true text: { @@ -649,50 +664,6 @@ SmartPanel { horizontalAlignment: Text.AlignCenter } } - - // --- Vertical Divider --- - NDivider { - Layout.fillHeight: true - vertical: true - visible: root.previewActive - Layout.preferredWidth: root.dividerWidth - } - - // --- Right Pane (Preview) --- - Item { - Layout.fillHeight: true - Layout.preferredWidth: root.previewActive ? root.previewPanelWidth : 0 - visible: root.previewActive - - Behavior on Layout.preferredWidth { - NumberAnimation { - duration: Style.animationFast - easing.type: Easing.OutCirc - } - } - - // --- Preview Loader --- - Loader { - id: clipboardPreviewLoader - anchors.fill: parent - active: root.previewActive - source: active ? "./ClipboardPreview.qml" : "" - - // Access the loaded component to set the current item - onLoaded: { - if (selectedIndex >= 0 && results[selectedIndex] && item) { - item.currentItem = results[selectedIndex]; - } - } - - // When the item is loaded, ensure it gets the current selection - onItemChanged: { - if (item && selectedIndex >= 0 && results[selectedIndex]) { - item.currentItem = results[selectedIndex]; - } - } - } - } } } } From ed373df99dfed611b931ae6402c941b78900df5b Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Wed, 19 Nov 2025 05:56:54 +0800 Subject: [PATCH 07/19] feat(launcher): Add toggle for clip plugin preview --- Assets/Translations/en.json | 4 ++++ Assets/settings-default.json | 1 + Commons/Settings.qml | 1 + Modules/Panels/Launcher/Launcher.qml | 2 +- Modules/Panels/Settings/Tabs/LauncherTab.qml | 7 +++++++ 5 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 55ff382c..2b14de6b 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -1341,6 +1341,10 @@ "description": "Access previously copied items from the launcher.", "label": "Enable clipboard history" }, + "clip-preview": { + "description": "Show a preview of the clipboard content when using the >clip command.", + "label": "Enable clip preview" + }, "custom-launch-prefix": { "description": "Prefix commands with a custom launcher (e.g., 'runapp' for systemd integration).", "label": "Custom launch prefix" diff --git a/Assets/settings-default.json b/Assets/settings-default.json index 5bd6841b..1336f6db 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -139,6 +139,7 @@ }, "appLauncher": { "enableClipboardHistory": false, + "enableClipPreview": true, "position": "center", "pinnedExecs": [], "useApp2Unit": false, diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 568d069c..a344cbe0 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -295,6 +295,7 @@ Singleton { // applauncher property JsonObject appLauncher: JsonObject { property bool enableClipboardHistory: false + property bool enableClipPreview: true // Position: center, top_left, top_right, bottom_left, bottom_right, bottom_center, top_center property string position: "center" property list pinnedExecs: [] diff --git a/Modules/Panels/Launcher/Launcher.qml b/Modules/Panels/Launcher/Launcher.qml index 3de1c721..cf6f08b9 100644 --- a/Modules/Panels/Launcher/Launcher.qml +++ b/Modules/Panels/Launcher/Launcher.qml @@ -13,7 +13,7 @@ import qs.Widgets SmartPanel { id: root - readonly property bool previewActive: searchText.startsWith(">clip") + readonly property bool previewActive: searchText.startsWith(">clip") && Settings.data.appLauncher.enableClipPreview // Panel configuration readonly property int listPanelWidth: Math.round(600 * Style.uiScaleRatio) diff --git a/Modules/Panels/Settings/Tabs/LauncherTab.qml b/Modules/Panels/Settings/Tabs/LauncherTab.qml index 976d4b50..2edbbf62 100644 --- a/Modules/Panels/Settings/Tabs/LauncherTab.qml +++ b/Modules/Panels/Settings/Tabs/LauncherTab.qml @@ -65,6 +65,13 @@ ColumnLayout { onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked } + NToggle { + label: I18n.tr("settings.launcher.settings.clip-preview.label") + description: I18n.tr("settings.launcher.settings.clip-preview.description") + checked: Settings.data.appLauncher.enableClipPreview + onToggled: checked => Settings.data.appLauncher.enableClipPreview = checked + } + NToggle { label: I18n.tr("settings.launcher.settings.sort-by-usage.label") description: I18n.tr("settings.launcher.settings.sort-by-usage.description") From 87f62b288b6f440062381b67e839902aae207419 Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Wed, 19 Nov 2025 07:06:00 +0800 Subject: [PATCH 08/19] feat(launcher): UI improvements for clipboard preview window --- Modules/Panels/Launcher/ClipboardPreview.qml | 14 ++------------ Modules/Panels/Launcher/Launcher.qml | 12 +++++++++--- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/Modules/Panels/Launcher/ClipboardPreview.qml b/Modules/Panels/Launcher/ClipboardPreview.qml index 12254afa..3c8e188a 100644 --- a/Modules/Panels/Launcher/ClipboardPreview.qml +++ b/Modules/Panels/Launcher/ClipboardPreview.qml @@ -74,20 +74,10 @@ Item { ColumnLayout { id: contentColumn anchors.fill: parent - anchors.margins: Style.marginM - spacing: Style.marginM + anchors.margins: Style.marginS + spacing: Style.marginS - NText { - text: currentItem ? (currentItem.name || "Preview") : "Preview" - font.weight: Style.fontWeightBold - Layout.fillWidth: true - pointSize: Style.fontSizeM - color: Color.mOnSurface - } - NDivider { - Layout.fillWidth: true - } Rectangle { Layout.fillWidth: true diff --git a/Modules/Panels/Launcher/Launcher.qml b/Modules/Panels/Launcher/Launcher.qml index cf6f08b9..b51f256f 100644 --- a/Modules/Panels/Launcher/Launcher.qml +++ b/Modules/Panels/Launcher/Launcher.qml @@ -17,7 +17,7 @@ SmartPanel { // Panel configuration readonly property int listPanelWidth: Math.round(600 * Style.uiScaleRatio) - readonly property int previewPanelWidth: Math.round(300 * Style.uiScaleRatio) + readonly property int previewPanelWidth: Math.round(400 * Style.uiScaleRatio) readonly property int totalBaseWidth: listPanelWidth + (Style.marginL * 2) preferredWidth: totalBaseWidth @@ -278,9 +278,15 @@ SmartPanel { id: previewBox visible: root.previewActive width: root.previewPanelWidth - height: ui.height + height: Math.round(400 * Style.uiScaleRatio) x: ui.width + Style.marginM - y: 0 + y: Math.max( + Style.marginL, // Minimum y is the top margin of the content area + Math.min( + resultsList.mapToItem(ui, 0, (root.selectedIndex * (root.entryHeight + resultsList.spacing)) - resultsList.contentY).y, + ui.height - previewBox.height - Style.marginL // Maximum y, considering bottom margin + ) + ) z: -1 // Draw behind main panel content if it ever overlaps opacity: visible ? 1.0 : 0.0 From b2978113c54eb4d3e895f3f6baeddbc791d7c7b7 Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Wed, 19 Nov 2025 07:32:37 +0800 Subject: [PATCH 09/19] feat(launcher): Integrate TextFormatter for enhanced preview UI --- Helpers/TextFormatter.js | 34 ++++++++++++++++++++ Modules/Panels/Launcher/ClipboardPreview.qml | 6 +++- 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 Helpers/TextFormatter.js diff --git a/Helpers/TextFormatter.js b/Helpers/TextFormatter.js new file mode 100644 index 00000000..36f0b375 --- /dev/null +++ b/Helpers/TextFormatter.js @@ -0,0 +1,34 @@ +.pragma library + +/** + * Wrap text in a nicely styled HTML container for display + * @param {string} text - The text to display + * @returns {string} HTML string + */ +function wrapTextForDisplay(text) { + // Escape HTML special characters + const escapeHtml = (s) => + s.replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + + return ` +
+${escapeHtml(text)} +
+`; +} diff --git a/Modules/Panels/Launcher/ClipboardPreview.qml b/Modules/Panels/Launcher/ClipboardPreview.qml index 3c8e188a..7b9ee16e 100644 --- a/Modules/Panels/Launcher/ClipboardPreview.qml +++ b/Modules/Panels/Launcher/ClipboardPreview.qml @@ -4,6 +4,7 @@ import QtQuick.Controls import qs.Commons import qs.Widgets import qs.Services.Keyboard +import "../../../Helpers/TextFormatter.js" as TextFormatter Item { id: previewPanel @@ -35,7 +36,7 @@ Item { } else { loadingFullContent = true; ClipboardService.decode(currentItem.clipboardId, function(content) { - fullContent = content; + fullContent = TextFormatter.wrapTextForDisplay(content); loadingFullContent = false; }); } @@ -116,6 +117,9 @@ Item { text: fullContent readOnly: true wrapMode: Text.Wrap + textFormat: TextArea.RichText // Enable HTML rendering + font.pointSize: Style.fontSizeM // Adjust font size for readability + color: Color.mOnSurface // Consistent text color } } } From 948c3c7e18fe2490dec75c42494f5e73cb01016d Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Wed, 19 Nov 2025 07:38:54 +0800 Subject: [PATCH 10/19] feat(i18n): Add clip preview translations for German (de) --- Assets/Translations/de.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index ae0618ca..70483d6b 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -1341,6 +1341,10 @@ "description": "Zugriff auf zuvor kopierte Elemente über den Launcher.", "label": "Zwischenablageverlauf aktivieren" }, + "clip-preview": { + "description": "Zeigt eine Vorschau des Inhalts der Zwischenablage an, wenn der Befehl >clip verwendet wird.", + "label": "Clip-Vorschau aktivieren" + }, "custom-launch-prefix": { "description": "Befehle mit einem benutzerdefinierten Launcher präfixieren (z.B. 'runapp' für systemd-Integration).", "label": "Benutzerdefiniertes Start-Präfix" From 9bf8fd16d63ce780f67e3cb79d100a8e4d8ac81d Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Wed, 19 Nov 2025 07:47:38 +0800 Subject: [PATCH 11/19] feat(i18n): Add clip preview translations for Spanish (es) --- Assets/Translations/es.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index 2b79eb26..1034d7db 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -1341,6 +1341,10 @@ "description": "Accede a los elementos copiados anteriormente desde el lanzador.", "label": "Activar historial del portapapeles" }, + "clip-preview": { + "description": "Muestra una vista previa del contenido del portapapeles al usar el comando >clip.", + "label": "Activar vista previa del portapapeles" + }, "custom-launch-prefix": { "description": "Prefijar comandos con un lanzador personalizado (ej. 'runapp' para integración con systemd).", "label": "Prefijo de lanzamiento personalizado" From a18be7927cb946a8f96368512c19949c101feeb1 Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Wed, 19 Nov 2025 07:54:40 +0800 Subject: [PATCH 12/19] feat(i18n): Add clip preview translations for French (fr) --- Assets/Translations/fr.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index f0f192f3..71839bc1 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -1341,6 +1341,10 @@ "description": "Accédez aux éléments précédemment copiés depuis le lanceur.", "label": "Activer l'historique du presse-papiers" }, + "clip-preview": { + "description": "Afficher un aperçu du contenu du presse-papiers lors de l'utilisation de la commande >clip.", + "label": "Activer l'aperçu du presse-papiers" + }, "custom-launch-prefix": { "description": "Préfixer les commandes avec un lanceur personnalisé (ex. 'runapp' pour l'intégration systemd).", "label": "Préfixe de lancement personnalisé" From 2917f02621ce09a09edc899d3685cb0ff122aff6 Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Wed, 19 Nov 2025 07:57:44 +0800 Subject: [PATCH 13/19] feat(i18n): Add clip preview translations for Dutch (nl) --- Assets/Translations/nl.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Assets/Translations/nl.json b/Assets/Translations/nl.json index e43b6329..06d05d78 100644 --- a/Assets/Translations/nl.json +++ b/Assets/Translations/nl.json @@ -1341,6 +1341,10 @@ "description": "Toegang tot eerder gekopieerde items vanuit de launcher.", "label": "Klembordgeschiedenis inschakelen" }, + "clip-preview": { + "description": "Toon een voorbeeld van de inhoud van het klembord bij gebruik van het >clip-commando.", + "label": "Klembordvoorbeeld inschakelen" + }, "custom-launch-prefix": { "description": "Voorzie commando's van een aangepaste launcher-prefix (bijv. 'runapp' voor systemd-integratie).", "label": "Aangepaste startprefix" From 03554120be80051b414c1bf40ba092286d00493c Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Wed, 19 Nov 2025 08:01:14 +0800 Subject: [PATCH 14/19] feat(i18n): Add clip preview translations for Portuguese (pt) --- Assets/Translations/pt.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index 93f94a9f..b12c839b 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -1341,6 +1341,10 @@ "description": "Acesse itens copiados anteriormente a partir do lançador.", "label": "Ativar histórico da área de transferência" }, + "clip-preview": { + "description": "Mostra uma pré-visualização do conteúdo da área de transferência ao usar o comando >clip.", + "label": "Ativar pré-visualização da área de transferência" + }, "custom-launch-prefix": { "description": "Prefixar comandos com um inicializador personalizado (ex. 'runapp' para integração systemd).", "label": "Prefixo de inicialização personalizado" From 5ade827a4ccd5a948e37d2fcbdfa410cc7e04758 Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Wed, 19 Nov 2025 08:03:52 +0800 Subject: [PATCH 15/19] feat(i18n): Add clip preview translations for Russian (ru) --- Assets/Translations/ru.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Assets/Translations/ru.json b/Assets/Translations/ru.json index c0fb0d89..198c7820 100644 --- a/Assets/Translations/ru.json +++ b/Assets/Translations/ru.json @@ -1341,6 +1341,10 @@ "description": "Доступ к ранее скопированным элементам из запуска.", "label": "Включить историю буфера обмена" }, + "clip-preview": { + "description": "Показывать предварительный просмотр содержимого буфера обмена при использовании команды >clip.", + "label": "Включить предварительный просмотр буфера обмена" + }, "custom-launch-prefix": { "description": "Добавлять префикс к командам с помощью пользовательского запуска (например, 'runapp' для интеграции с systemd).", "label": "Пользовательский префикс запуска" From 97ba831cb44301e9a10981ba815fccf0baa23cb8 Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Wed, 19 Nov 2025 08:04:56 +0800 Subject: [PATCH 16/19] feat(i18n): Add clip preview translations for Turkish (tr) --- Assets/Translations/tr.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Assets/Translations/tr.json b/Assets/Translations/tr.json index 55088cb7..fb8eceec 100644 --- a/Assets/Translations/tr.json +++ b/Assets/Translations/tr.json @@ -1341,6 +1341,10 @@ "description": "Başlatıcıdan daha önce kopyalanan öğelere erişin.", "label": "Pano geçmişini etkinleştir" }, + "clip-preview": { + "description": ">clip komutu kullanılırken panodaki içeriğin önizlemesini gösterir.", + "label": "Panoyu önizlemeyi etkinleştir" + }, "custom-launch-prefix": { "description": "Komutlara özel bir başlatıcı ile ön ek ekleyin (örn., systemd entegrasyonu için 'runapp').", "label": "Özel başlatma ön eki" From 5e2f8c1462f894e24d2ea4b7e8b70591b90c8a4a Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Wed, 19 Nov 2025 08:06:36 +0800 Subject: [PATCH 17/19] feat(i18n): Add clip preview translations for Ukrainian (uk-UA) --- Assets/Translations/uk-UA.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Assets/Translations/uk-UA.json b/Assets/Translations/uk-UA.json index 664d4916..39c7e8a7 100644 --- a/Assets/Translations/uk-UA.json +++ b/Assets/Translations/uk-UA.json @@ -1341,6 +1341,10 @@ "description": "Отримати доступ до раніше скопійованих елементів із запускача.", "label": "Увімкнути історію буфера обміну" }, + "clip-preview": { + "description": "Показувати попередній перегляд вмісту буфера обміну при використанні команди >clip.", + "label": "Увімкнути попередній перегляд буфера обміну" + }, "custom-launch-prefix": { "description": "Додати префікс до команд власним запускачем (напр., 'runapp' для інтеграції з systemd).", "label": "Власний префікс запуску" From 4a0c2b7ef3b092c0e57190e0192401ff542a329d Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Wed, 19 Nov 2025 08:08:15 +0800 Subject: [PATCH 18/19] feat(i18n): Add clip preview translations for Simplified Chinese (zh-CN) --- Assets/Translations/zh-CN.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index 9346021b..4855bcbe 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -1341,6 +1341,10 @@ "description": "从启动器访问之前复制的项目。", "label": "启用剪贴板历史记录" }, + "clip-preview": { + "description": "在使用 >clip 命令时显示剪贴板内容的预览。", + "label": "启用剪贴板预览" + }, "custom-launch-prefix": { "description": "使用自定义启动器前缀命令(例如,'runapp'用于systemd集成)。", "label": "自定义启动前缀" From e4e3b1b85c1897fd7fd7fa1108d6ca56fd39a9a6 Mon Sep 17 00:00:00 2001 From: loner <2788892716@qq.com> Date: Wed, 19 Nov 2025 08:17:31 +0800 Subject: [PATCH 19/19] qml format --- Modules/Panels/Launcher/ClipboardPreview.qml | 12 +++++------- Modules/Panels/Launcher/Launcher.qml | 10 +++------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/Modules/Panels/Launcher/ClipboardPreview.qml b/Modules/Panels/Launcher/ClipboardPreview.qml index 7b9ee16e..81c23f11 100644 --- a/Modules/Panels/Launcher/ClipboardPreview.qml +++ b/Modules/Panels/Launcher/ClipboardPreview.qml @@ -1,10 +1,10 @@ import QtQuick -import QtQuick.Layouts import QtQuick.Controls -import qs.Commons -import qs.Widgets -import qs.Services.Keyboard +import QtQuick.Layouts import "../../../Helpers/TextFormatter.js" as TextFormatter +import qs.Commons +import qs.Services.Keyboard +import qs.Widgets Item { id: previewPanel @@ -35,7 +35,7 @@ Item { } } else { loadingFullContent = true; - ClipboardService.decode(currentItem.clipboardId, function(content) { + ClipboardService.decode(currentItem.clipboardId, function (content) { fullContent = TextFormatter.wrapTextForDisplay(content); loadingFullContent = false; }); @@ -78,8 +78,6 @@ Item { anchors.margins: Style.marginS spacing: Style.marginS - - Rectangle { Layout.fillWidth: true Layout.fillHeight: true diff --git a/Modules/Panels/Launcher/Launcher.qml b/Modules/Panels/Launcher/Launcher.qml index b51f256f..8e84cb65 100644 --- a/Modules/Panels/Launcher/Launcher.qml +++ b/Modules/Panels/Launcher/Launcher.qml @@ -280,13 +280,9 @@ SmartPanel { width: root.previewPanelWidth height: Math.round(400 * Style.uiScaleRatio) x: ui.width + Style.marginM - y: Math.max( - Style.marginL, // Minimum y is the top margin of the content area - Math.min( - resultsList.mapToItem(ui, 0, (root.selectedIndex * (root.entryHeight + resultsList.spacing)) - resultsList.contentY).y, - ui.height - previewBox.height - Style.marginL // Maximum y, considering bottom margin - ) - ) + y: Math.max(Style.marginL // Minimum y is the top margin of the content area + , Math.min(resultsList.mapToItem(ui, 0, (root.selectedIndex * (root.entryHeight + resultsList.spacing)) - resultsList.contentY).y, ui.height - previewBox.height - Style.marginL // Maximum y, considering bottom margin + )) z: -1 // Draw behind main panel content if it ever overlaps opacity: visible ? 1.0 : 0.0