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:
parent
468272d4c9
commit
9781005a21
4 changed files with 87 additions and 73 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue