From 9781005a213880bf78684eef1be79b636710bef1 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Tue, 2 Sep 2025 14:32:02 +0200 Subject: [PATCH] Added Thumbnail to ClipboardHistory images, fix keyboard navigation ClipboardHistory: Add image thumbnail, fix navigation viewport Launcher: replace TextField with NTextInput --- Modules/Launcher/ClipboardHistory.qml | 11 +- Modules/Launcher/Launcher.qml | 141 ++++++++++++++------------ Services/CliphistService.qml | 7 ++ Widgets/NTextInput.qml | 1 + 4 files changed, 87 insertions(+), 73 deletions(-) diff --git a/Modules/Launcher/ClipboardHistory.qml b/Modules/Launcher/ClipboardHistory.qml index c52ddb1..ed3af89 100644 --- a/Modules/Launcher/ClipboardHistory.qml +++ b/Modules/Launcher/ClipboardHistory.qml @@ -23,12 +23,7 @@ QtObject { const normalized = (preview || "").replace(/\s+/g, ' ').trim() const lines = normalized.split(/\n+/) const title = (lines[0] || "Text").slice(0, 60) - let subtitle = "" - if (lines.length > 1) { - subtitle = lines[1].slice(0, 80) - } else { - subtitle = `${normalized.length} chars` - } + const subtitle = (lines.length > 1) ? lines[1].slice(0, 80) : "" return { "title": title, "subtitle": subtitle @@ -39,7 +34,7 @@ QtObject { if (item.isImage) { const meta = parseImageMeta(item.preview) const title = meta ? `Image ${meta.w}×${meta.h}` : "Image" - const subtitle = meta ? `${meta.size} · ${meta.fmt}` : (item.preview || "") + const subtitle = "" return { "isClipboard": true, "name": title, @@ -54,7 +49,7 @@ QtObject { return { "isClipboard": true, "name": parts.title, - "content": parts.subtitle, + "content": "", "icon": "content_paste", "type": 'text', "id": item.id diff --git a/Modules/Launcher/Launcher.qml b/Modules/Launcher/Launcher.qml index 7147bd2..3030ed5 100644 --- a/Modules/Launcher/Launcher.qml +++ b/Modules/Launcher/Launcher.qml @@ -52,6 +52,17 @@ NPanel { searchText = "" selectedIndex = 0 } + // Focus search input on open and place cursor at end + Qt.callLater(() => { + if (searchInputBox && searchInputBox.inputItem) { + searchInputBox.inputItem.forceActiveFocus() + if (searchText && searchText.length > 0) { + searchInputBox.inputItem.cursorPosition = searchText.length + } else { + searchInputBox.inputItem.cursorPosition = 0 + } + } + }) } onClosed: { @@ -244,74 +255,48 @@ NPanel { anchors.margins: Style.marginL * scaling spacing: Style.marginM * scaling - // Search bar - Rectangle { + RowLayout { Layout.fillWidth: true Layout.preferredHeight: Math.round(Style.barHeight * scaling) Layout.bottomMargin: Style.marginM * scaling - radius: Style.radiusM * scaling - color: Color.mSurface - border.color: searchInput.activeFocus ? Color.mSecondary : Color.mOutline - border.width: Math.max(1, searchInput.activeFocus ? Style.borderM * scaling : Style.borderS * scaling) + // Wrapper ensures the input stretches to full width under RowLayout Item { - anchors.fill: parent - anchors.margins: Style.marginM * scaling + id: searchInputWrap + Layout.fillWidth: true + Layout.preferredHeight: Math.round(Style.barHeight * scaling) - NIcon { - id: searchIcon - text: "search" - font.pointSize: Style.fontSizeXL * scaling - color: searchInput.activeFocus ? Color.mPrimary : Color.mOnSurface - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - } - - TextField { - id: searchInput - placeholderText: searchText === "" ? "Search applications... (use > to view commands)" : "Search applications..." - color: Color.mOnSurface - placeholderTextColor: Color.mOnSurfaceVariant - background: null - font.pointSize: Style.fontSizeL * scaling - anchors.left: searchIcon.right - anchors.leftMargin: Style.marginS * scaling - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter + NTextInput { + id: searchInputBox + anchors.fill: parent + placeholderText: "Search applications... (use > to view commands)" text: searchText - onTextChanged: { - // Update the parent searchText property - if (searchText !== text) { - searchText = text - } - // Defer selectedIndex reset to avoid binding loops - Qt.callLater(() => selectedIndex = 0) - - // Reset cursor position if needed - if (shouldResetCursor && text === "") { - cursorPosition = 0 - shouldResetCursor = false - } - } - selectedTextColor: Color.mOnSurface - selectionColor: Color.mPrimary - padding: 0 - verticalAlignment: TextInput.AlignVCenter - leftPadding: 0 - rightPadding: 0 - topPadding: 0 - bottomPadding: 0 + inputMaxWidth: 100000 + // Tune vertical centering on inner input Component.onCompleted: { - // Focus the search bar by default and set cursor position + searchInputBox.inputItem.font.pointSize = Style.fontSizeL * scaling + searchInputBox.inputItem.verticalAlignment = TextInput.AlignVCenter + // Ensure focus when launcher first appears Qt.callLater(() => { - selectedIndex = 0 - searchInput.forceActiveFocus() - // Set cursor to end if there's already text + searchInputBox.inputItem.forceActiveFocus() if (searchText && searchText.length > 0) { - searchInput.cursorPosition = searchText.length + searchInputBox.inputItem.cursorPosition = searchText.length + } else { + searchInputBox.inputItem.cursorPosition = 0 } }) } + onTextChanged: { + if (searchText !== text) { + searchText = text + } + Qt.callLater(() => selectedIndex = 0) + if (shouldResetCursor && text === "") { + searchInputBox.inputItem.cursorPosition = 0 + shouldResetCursor = false + } + } + // Forward key navigation to behave like before Keys.onDownPressed: selectNext() Keys.onUpPressed: selectPrev() Keys.onEnterPressed: activateSelected() @@ -355,16 +340,13 @@ NPanel { } } - Behavior on border.color { - ColorAnimation { - duration: Style.animationFast - } - } - - Behavior on border.width { - NumberAnimation { - duration: Style.animationFast - } + // Clear-all action to the right of the input + NIconButton { + Layout.alignment: Qt.AlignVCenter + visible: searchText.startsWith(">clip") + icon: "delete_sweep" + tooltipText: "Clear clipboard history" + onClicked: CliphistService.wipeAll() } } @@ -390,6 +372,22 @@ NPanel { policy: ScrollBar.AsNeeded } + // Keep viewport anchored to the selected item when the clipboard model refreshes + Connections { + target: CliphistService + function onRevisionChanged() { + if (Settings.data.appLauncher.enableClipboardHistory && searchText.startsWith(">clip")) { + // Clamp selection in case the list shrank + if (selectedIndex >= filteredEntries.length) { + selectedIndex = Math.max(0, filteredEntries.length - 1) + } + Qt.callLater(() => { + appsList.positionViewAtIndex(selectedIndex, ListView.Contain) + }) + } + } + } + delegate: Rectangle { width: appsList.width - Style.marginS * scaling height: 65 * scaling @@ -431,6 +429,19 @@ NPanel { && iconImg.status !== Image.Error && iconImg.source !== "") visible: !searchText.startsWith(">calc") + // Decode image thumbnails on demand + Component.onCompleted: { + if (modelData && modelData.type === 'image' && !CliphistService.imageDataById[modelData.id]) { + CliphistService.decodeToDataUrl(modelData.id, modelData.mime || "image/*", function () {}) + } + } + onVisibleChanged: { + if (visible && modelData && modelData.type === 'image' + && !CliphistService.imageDataById[modelData.id]) { + CliphistService.decodeToDataUrl(modelData.id, modelData.mime || "image/*", function () {}) + } + } + // Clipboard image display (pull from cache) Image { id: clipboardImage diff --git a/Services/CliphistService.qml b/Services/CliphistService.qml index 4b73c68..87f14f3 100644 --- a/Services/CliphistService.qml +++ b/Services/CliphistService.qml @@ -27,6 +27,9 @@ Singleton { property var imageDataById: ({}) property int revision: 0 + // Approximate first-seen timestamps for entries this session (seconds) + property var firstSeenById: ({}) + // Internal: store callback for decode property var _decodeCallback: null @@ -131,6 +134,10 @@ Singleton { else mime = "image/*" } + // Record first seen time for new ids (approximate copy time) + if (!root.firstSeenById[id]) { + root.firstSeenById[id] = Time.timestamp + } return { "id": id, "preview": preview, diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml index f05cef5..3ffb924 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -19,6 +19,7 @@ ColumnLayout { property alias text: input.text property alias placeholderText: input.placeholderText property alias inputMethodHints: input.inputMethodHints + property alias inputItem: input signal editingFinished