diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index 72e5cbb2..9bb41432 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -1372,6 +1372,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" diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index c698e8c6..b381f422 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -1372,6 +1372,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/Translations/es.json b/Assets/Translations/es.json index d68bd613..bb8aa996 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -1372,6 +1372,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" diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index 1316261c..6c3145c4 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -1372,6 +1372,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é" diff --git a/Assets/Translations/nl.json b/Assets/Translations/nl.json index 4df6d6b0..2ba5324d 100644 --- a/Assets/Translations/nl.json +++ b/Assets/Translations/nl.json @@ -1372,6 +1372,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" diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index 82cf407c..fe8f7458 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -1372,6 +1372,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" diff --git a/Assets/Translations/ru.json b/Assets/Translations/ru.json index 9d2c4e53..4a5064ed 100644 --- a/Assets/Translations/ru.json +++ b/Assets/Translations/ru.json @@ -1372,6 +1372,10 @@ "description": "Доступ к ранее скопированным элементам из запуска.", "label": "Включить историю буфера обмена" }, + "clip-preview": { + "description": "Показывать предварительный просмотр содержимого буфера обмена при использовании команды >clip.", + "label": "Включить предварительный просмотр буфера обмена" + }, "custom-launch-prefix": { "description": "Добавлять префикс к командам с помощью пользовательского запуска (например, 'runapp' для интеграции с systemd).", "label": "Пользовательский префикс запуска" diff --git a/Assets/Translations/tr.json b/Assets/Translations/tr.json index 879dec51..0f15c1a9 100644 --- a/Assets/Translations/tr.json +++ b/Assets/Translations/tr.json @@ -1372,6 +1372,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" diff --git a/Assets/Translations/uk-UA.json b/Assets/Translations/uk-UA.json index 39035b35..ac2cb1ef 100644 --- a/Assets/Translations/uk-UA.json +++ b/Assets/Translations/uk-UA.json @@ -1372,6 +1372,10 @@ "description": "Отримати доступ до раніше скопійованих елементів із запускача.", "label": "Увімкнути історію буфера обміну" }, + "clip-preview": { + "description": "Показувати попередній перегляд вмісту буфера обміну при використанні команди >clip.", + "label": "Увімкнути попередній перегляд буфера обміну" + }, "custom-launch-prefix": { "description": "Додати префікс до команд власним запускачем (напр., 'runapp' для інтеграції з systemd).", "label": "Власний префікс запуску" diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index f82b06dd..be29c979 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -1372,6 +1372,10 @@ "description": "从启动器访问之前复制的项目。", "label": "启用剪贴板历史记录" }, + "clip-preview": { + "description": "在使用 >clip 命令时显示剪贴板内容的预览。", + "label": "启用剪贴板预览" + }, "custom-launch-prefix": { "description": "使用自定义启动器前缀命令(例如,'runapp'用于systemd集成)。", "label": "自定义启动前缀" diff --git a/Assets/settings-default.json b/Assets/settings-default.json index 93d4f02e..6c31ebec 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -143,6 +143,7 @@ }, "appLauncher": { "enableClipboardHistory": false, + "enableClipPreview": true, "position": "center", "pinnedExecs": [], "useApp2Unit": false, diff --git a/Commons/Settings.qml b/Commons/Settings.qml index fd597388..72c2f831 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -296,6 +296,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/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 new file mode 100644 index 00000000..81c23f11 --- /dev/null +++ b/Modules/Panels/Launcher/ClipboardPreview.qml @@ -0,0 +1,127 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import "../../../Helpers/TextFormatter.js" as TextFormatter +import qs.Commons +import qs.Services.Keyboard +import qs.Widgets + +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 = ""; + imageDataUrl = ""; + loadingFullContent = false; + isImageContent = currentItem && currentItem.isImage; + + if (currentItem && currentItem.clipboardId) { + 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 = TextFormatter.wrapTextForDisplay(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; + } + } + } + } + } + + 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.marginS + spacing: Style.marginS + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: Color.mSurfaceVariant || "#e0e0e0" + border.color: Color.mOutline || "#aaaaaa" + border.width: 1 + radius: Style.radiusS + + BusyIndicator { + anchors.centerIn: parent + running: loadingFullContent + visible: loadingFullContent + width: Style.baseWidgetSize + height: width + } + + Item { + anchors.fill: parent + anchors.margins: Style.marginS + + NImageRounded { + anchors.fill: parent + imagePath: imageDataUrl + visible: isImageContent && !loadingFullContent && imageDataUrl !== "" + imageRadius: Style.radiusS + imageFillMode: Image.PreserveAspectFit + } + + ScrollView { + anchors.fill: parent + clip: true + visible: !isImageContent && !loadingFullContent + + TextArea { + 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 + } + } + } + } + } + } +} diff --git a/Modules/Panels/Launcher/Launcher.qml b/Modules/Panels/Launcher/Launcher.qml index ed037b5a..8e84cb65 100644 --- a/Modules/Panels/Launcher/Launcher.qml +++ b/Modules/Panels/Launcher/Launcher.qml @@ -13,8 +13,14 @@ import qs.Widgets SmartPanel { id: root + readonly property bool previewActive: searchText.startsWith(">clip") && Settings.data.appLauncher.enableClipPreview + // Panel configuration - preferredWidth: Math.round(500 * Style.uiScaleRatio) + readonly property int listPanelWidth: Math.round(600 * Style.uiScaleRatio) + readonly property int previewPanelWidth: Math.round(400 * Style.uiScaleRatio) + readonly property int totalBaseWidth: listPanelWidth + (Style.marginL * 2) + + preferredWidth: totalBaseWidth preferredHeight: Math.round(600 * Style.uiScaleRatio) preferredWidthRatio: 0.3 preferredHeightRatio: 0.5 @@ -262,13 +268,50 @@ 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: 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 + )) + 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 @@ -282,7 +325,6 @@ SmartPanel { property bool initialized: false onPositionChanged: mouse => { - // Store initial position if (!initialized) { lastX = mouse.x; lastY = mouse.y; @@ -290,7 +332,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) { @@ -300,7 +341,6 @@ SmartPanel { } } - // Reset when launcher opens Connections { target: root function onOpened() { @@ -329,298 +369,302 @@ SmartPanel { } } - ColumnLayout { + RowLayout { anchors.fill: parent - anchors.margins: Style.marginL - spacing: Style.marginM + anchors.margins: Style.marginL // Apply overall margins here + spacing: Style.marginM // Apply spacing between elements here - 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.listPanelWidth + 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 + 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); + } + 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) + 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) || ""; + } + + 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 || 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 + } + } + } + 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 - } + 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' : ''}`; + 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 } - 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..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", @@ -218,22 +215,20 @@ Item { "imageWidth": meta ? meta.w : 0, "imageHeight": meta ? meta.h : 0, "clipboardId": item.id, - "mime": item.mime + "mime": item.mime, + "preview": item.preview }; } - // 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]; @@ -250,11 +245,12 @@ Item { "name": title, "description": description, "icon": "text-x-generic", - "isImage": false + "isImage": false, + "clipboardId": item.id, + "preview": preview }; } - // 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); @@ -271,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/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") 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 diff --git a/Widgets/NImageRounded.qml b/Widgets/NImageRounded.qml index a7a5789f..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 @@ -30,12 +31,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: root.imageFillMode + horizontalAlignment: Image.AlignHCenter + verticalAlignment: Image.AlignVCenter onStatusChanged: root.statusChanged(status) } @@ -51,17 +54,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 +70,6 @@ Rectangle { } } - // Fallback icon Loader { active: fallbackIcon !== undefined && fallbackIcon !== "" && (imagePath === undefined || imagePath === "") anchors.centerIn: parent @@ -83,7 +82,6 @@ Rectangle { } } - // Border Rectangle { anchors.fill: parent radius: parent.radius