Added Thumbnail to ClipboardHistory images, fix keyboard navigation

ClipboardHistory: Add image thumbnail, fix navigation viewport
Launcher: replace TextField with NTextInput
This commit is contained in:
Ly-sec 2025-09-02 14:32:02 +02:00
parent 468272d4c9
commit 9781005a21
4 changed files with 87 additions and 73 deletions

View file

@ -23,12 +23,7 @@ QtObject {
const normalized = (preview || "").replace(/\s+/g, ' ').trim() const normalized = (preview || "").replace(/\s+/g, ' ').trim()
const lines = normalized.split(/\n+/) const lines = normalized.split(/\n+/)
const title = (lines[0] || "Text").slice(0, 60) const title = (lines[0] || "Text").slice(0, 60)
let subtitle = "" const subtitle = (lines.length > 1) ? lines[1].slice(0, 80) : ""
if (lines.length > 1) {
subtitle = lines[1].slice(0, 80)
} else {
subtitle = `${normalized.length} chars`
}
return { return {
"title": title, "title": title,
"subtitle": subtitle "subtitle": subtitle
@ -39,7 +34,7 @@ QtObject {
if (item.isImage) { if (item.isImage) {
const meta = parseImageMeta(item.preview) const meta = parseImageMeta(item.preview)
const title = meta ? `Image ${meta.w}×${meta.h}` : "Image" const title = meta ? `Image ${meta.w}×${meta.h}` : "Image"
const subtitle = meta ? `${meta.size} · ${meta.fmt}` : (item.preview || "") const subtitle = ""
return { return {
"isClipboard": true, "isClipboard": true,
"name": title, "name": title,
@ -54,7 +49,7 @@ QtObject {
return { return {
"isClipboard": true, "isClipboard": true,
"name": parts.title, "name": parts.title,
"content": parts.subtitle, "content": "",
"icon": "content_paste", "icon": "content_paste",
"type": 'text', "type": 'text',
"id": item.id "id": item.id

View file

@ -52,6 +52,17 @@ NPanel {
searchText = "" searchText = ""
selectedIndex = 0 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: { onClosed: {
@ -244,74 +255,48 @@ NPanel {
anchors.margins: Style.marginL * scaling anchors.margins: Style.marginL * scaling
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
// Search bar RowLayout {
Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: Math.round(Style.barHeight * scaling) Layout.preferredHeight: Math.round(Style.barHeight * scaling)
Layout.bottomMargin: Style.marginM * 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 { Item {
id: searchInputWrap
Layout.fillWidth: true
Layout.preferredHeight: Math.round(Style.barHeight * scaling)
NTextInput {
id: searchInputBox
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginM * scaling placeholderText: "Search applications... (use > to view commands)"
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
text: searchText text: searchText
onTextChanged: { inputMaxWidth: 100000
// Update the parent searchText property // Tune vertical centering on inner input
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
Component.onCompleted: { 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(() => { Qt.callLater(() => {
selectedIndex = 0 searchInputBox.inputItem.forceActiveFocus()
searchInput.forceActiveFocus()
// Set cursor to end if there's already text
if (searchText && searchText.length > 0) { 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.onDownPressed: selectNext()
Keys.onUpPressed: selectPrev() Keys.onUpPressed: selectPrev()
Keys.onEnterPressed: activateSelected() Keys.onEnterPressed: activateSelected()
@ -355,16 +340,13 @@ NPanel {
} }
} }
Behavior on border.color { // Clear-all action to the right of the input
ColorAnimation { NIconButton {
duration: Style.animationFast Layout.alignment: Qt.AlignVCenter
} visible: searchText.startsWith(">clip")
} icon: "delete_sweep"
tooltipText: "Clear clipboard history"
Behavior on border.width { onClicked: CliphistService.wipeAll()
NumberAnimation {
duration: Style.animationFast
}
} }
} }
@ -390,6 +372,22 @@ NPanel {
policy: ScrollBar.AsNeeded 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 { delegate: Rectangle {
width: appsList.width - Style.marginS * scaling width: appsList.width - Style.marginS * scaling
height: 65 * scaling height: 65 * scaling
@ -431,6 +429,19 @@ NPanel {
&& iconImg.status !== Image.Error && iconImg.source !== "") && iconImg.status !== Image.Error && iconImg.source !== "")
visible: !searchText.startsWith(">calc") 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) // Clipboard image display (pull from cache)
Image { Image {
id: clipboardImage id: clipboardImage

View file

@ -27,6 +27,9 @@ Singleton {
property var imageDataById: ({}) property var imageDataById: ({})
property int revision: 0 property int revision: 0
// Approximate first-seen timestamps for entries this session (seconds)
property var firstSeenById: ({})
// Internal: store callback for decode // Internal: store callback for decode
property var _decodeCallback: null property var _decodeCallback: null
@ -131,6 +134,10 @@ Singleton {
else else
mime = "image/*" mime = "image/*"
} }
// Record first seen time for new ids (approximate copy time)
if (!root.firstSeenById[id]) {
root.firstSeenById[id] = Time.timestamp
}
return { return {
"id": id, "id": id,
"preview": preview, "preview": preview,

View file

@ -19,6 +19,7 @@ ColumnLayout {
property alias text: input.text property alias text: input.text
property alias placeholderText: input.placeholderText property alias placeholderText: input.placeholderText
property alias inputMethodHints: input.inputMethodHints property alias inputMethodHints: input.inputMethodHints
property alias inputItem: input
signal editingFinished signal editingFinished