pragma ComponentBehavior: Bound import QtQuick import "Util.js" as Util FocusScope { id: root required property Theme theme required property Clipboard clipboardService property var sessionState: ({ "query": "", "selectedId": "", "scrollY": 0 }) readonly property string query: String(sessionState.query || "") property bool stateReady: false readonly property var panelState: clipboardService ? Util.parseJson(clipboardService.search(query), { "rows": [], "privateMode": false, "lastAction": {} }) : ({ "rows": [], "privateMode": false, "lastAction": {} }) readonly property var rows: panelState.rows || [] readonly property int selectedIndex: selectionIndex() readonly property var selectedRow: selectedIndex >= 0 ? rows[selectedIndex] : ({}) readonly property int rowHeight: 56 readonly property int rowSpacing: 6 readonly property int rowStride: rowHeight + rowSpacing signal pasteRequested(string id) function updateState(values) { Object.assign(sessionState, values); // Notify bindings when the owner supplies a plain JS object. sessionStateChanged(); } function selectionIndex() { const index = rows.findIndex(row => String(row.id) === String(sessionState.selectedId)); return index >= 0 ? index : (rows.length > 0 ? 0 : -1); } function reconcileSelection() { const id = selectedIndex >= 0 ? String(rows[selectedIndex].id) : ""; if (sessionState.selectedId !== id) updateState({ "selectedId": id }); if (stateReady) ensureSelectedVisible(); } function search(text) { updateState({ "query": String(text) }); } function select(index) { if (rows.length === 0) return; const i = Math.max(0, Math.min(rows.length - 1, Number(index))); updateState({ "selectedId": String(rows[i].id) }); ensureSelectedVisible(); } function rememberScroll() { if (stateReady && sessionState.scrollY !== list.contentY) updateState({ "scrollY": list.contentY }); } function ensureSelectedVisible() { if (selectedIndex < 0) return; const top = selectedIndex * rowStride; const bottom = top + rowHeight; if (top < list.contentY) updateState({ "scrollY": top }); else if (bottom > list.contentY + list.height) updateState({ "scrollY": bottom - list.height }); } function copy(id) { if (clipboardService) clipboardService.copy(id); } function paste(id) { if (clipboardService && rows.some(row => String(row.id) === String(id))) pasteRequested(String(id)); } function togglePin(id) { if (clipboardService) clipboardService.togglePin(id); } function deleteRow(id) { if (clipboardService) clipboardService.deleteRow(id); } function setPrivateMode(enabled) { if (clipboardService) clipboardService.setPrivateMode(enabled); } width: parent ? parent.width : 0 height: parent ? parent.height : implicitHeight implicitHeight: 510 focus: true onRowsChanged: selectionTimer.restart() Timer { id: selectionTimer interval: 0 running: true onTriggered: { root.reconcileSelection(); if (!root.stateReady) { root.stateReady = true; searchInput.input.forceActiveFocus(); } } } Keys.onUpPressed: event => { event.accepted = true; select(selectedIndex - 1); } Keys.onDownPressed: event => { event.accepted = true; select(selectedIndex + 1); } Keys.onReturnPressed: event => { event.accepted = true; if (!event.isAutoRepeat) paste(selectedRow.id); } Keys.onEnterPressed: event => { event.accepted = true; if (!event.isAutoRepeat) paste(selectedRow.id); } Shortcut { sequences: ["Return", "Enter"] enabled: searchInput.input.activeFocus autoRepeat: false onActivated: root.paste(root.selectedRow.id) } StableListModel { id: clipboardRows rows: root.rows } Row { id: searchRow anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top spacing: 8 height: 38 PopupTextField { id: searchInput objectName: "searchField" theme: root.theme width: searchRow.width - privateButton.width - searchRow.spacing height: searchRow.height icon: "" placeholderText: "search clipboard" text: root.query onEdited: text => root.search(text) } PopupButton { id: privateButton objectName: "privateModeChip" theme: root.theme width: 38 height: searchRow.height icon: root.panelState.privateMode ? "" : "" text: root.panelState.privateMode ? "private" : "normal" showText: false role: "ghost" accessibleName: "private clipboard mode" tooltip: root.panelState.privateMode ? "resume clipboard capture" : "pause clipboard capture" checked: root.panelState.privateMode enabled: !!root.clipboardService onClicked: root.setPrivateMode(!root.panelState.privateMode) } } Item { objectName: "clipboardHistoryPanel" anchors.left: parent.left anchors.right: parent.right anchors.top: searchRow.bottom anchors.bottom: pasteFeedback.top anchors.topMargin: 10 anchors.bottomMargin: 10 Flickable { id: list objectName: "clipboardResultList" anchors.fill: parent contentHeight: rowColumn.implicitHeight contentY: Math.max(0, Math.min(Number(root.sessionState.scrollY || 0), contentHeight - height)) onContentYChanged: root.rememberScroll() clip: true boundsBehavior: Flickable.StopAtBounds Column { id: rowColumn width: list.width spacing: root.rowSpacing Repeater { model: clipboardRows delegate: PopupCard { id: clipRow required property int index required property var rowData readonly property var modelData: rowData readonly property bool isSelected: index === root.selectedIndex objectName: "clipRow" theme: root.theme padding: 0 active: isSelected hoverable: true externallyPressed: rowArea.pressed activeFocusOnTab: isSelected Accessible.role: Accessible.ListItem Accessible.name: String(modelData.title || "clipboard item") Accessible.selected: isSelected onActiveFocusChanged: if (activeFocus) root.select(index) width: rowColumn.width height: root.rowHeight MouseArea { id: rowArea anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { root.select(clipRow.index); clipRow.forceActiveFocus(); } onDoubleClicked: root.paste(clipRow.modelData.id) } Item { id: leadingSlot anchors.left: parent.left anchors.leftMargin: 9 anchors.verticalCenter: parent.verticalCenter width: 36 height: 36 Image { id: thumb anchors.fill: parent visible: clipRow.modelData.isImage && thumb.status !== Image.Error source: clipRow.modelData.isImage ? "file://" + encodeURI(clipRow.modelData.imagePath || "") : "" sourceSize.width: 36 sourceSize.height: 36 fillMode: Image.PreserveAspectCrop asynchronous: true clip: true smooth: true } Rectangle { id: swatch anchors.fill: parent visible: !clipRow.modelData.isImage && !!clipRow.modelData.swatch radius: 9 color: clipRow.modelData.swatch ? clipRow.modelData.swatch : "transparent" border.width: 1 border.color: root.theme.withAlpha(root.theme.ui.normal, 0.25) } Text { anchors.centerIn: parent visible: !swatch.visible && !thumb.visible text: clipRow.modelData.icon || "" color: root.theme.ui.accent font.family: "Symbols Nerd Font Mono" font.pixelSize: 18 } } Column { anchors.left: leadingSlot.right anchors.right: pinBadge.left anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: 11 anchors.rightMargin: 8 spacing: 3 NuguText { theme: root.theme selected: clipRow.isSelected text: clipRow.modelData.title width: parent.width elide: Text.ElideRight } NuguText { theme: root.theme variant: "meta" selected: clipRow.isSelected text: clipRow.modelData.kind + " · " + clipRow.modelData.age width: parent.width elide: Text.ElideRight } } Text { id: pinBadge anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter width: 16 text: clipRow.modelData.pinned ? "" : "" Accessible.name: clipRow.modelData.pinned ? "pinned" : "" color: root.theme.ui.focus font.family: "Symbols Nerd Font Mono" font.pixelSize: 12 } } } } } NuguText { anchors.centerIn: parent width: parent.width horizontalAlignment: Text.AlignHCenter theme: root.theme variant: "meta" visible: root.rows.length === 0 text: !root.clipboardService ? "clipboard unavailable" : (root.query.length > 0 ? "no matches" : "clipboard is empty") } } NuguText { id: pasteFeedback anchors.left: parent.left anchors.right: parent.right anchors.bottom: actionRail.top anchors.bottomMargin: text.length > 0 ? 8 : 0 theme: root.theme text: root.panelState.lastAction.error || "" color: root.theme.error height: text.length > 0 ? implicitHeight : 0 wrapMode: Text.Wrap } Row { id: actionRail objectName: "clipboardActionRail" anchors.left: parent.left anchors.bottom: parent.bottom height: 38 spacing: 8 enabled: root.selectedIndex >= 0 && !!root.clipboardService PopupButton { objectName: "clipboardPaste" theme: root.theme height: actionRail.height text: "paste" icon: "" role: "primary" tooltip: "paste selected item into the previous window" enabled: !root.panelState.lastAction.pending onClicked: root.paste(root.selectedRow.id) } PopupButton { objectName: "clipboardCopy" theme: root.theme width: 38 height: actionRail.height icon: "" showText: false accessibleName: "copy selected item" tooltip: "copy selected item to the clipboard" onClicked: root.copy(root.selectedRow.id) } PopupButton { objectName: "clipboardPin" theme: root.theme width: 38 height: actionRail.height icon: "" showText: false checked: !!root.selectedRow.pinned accessibleName: root.selectedRow.pinned ? "unpin selected item" : "pin selected item" tooltip: root.selectedRow.pinned ? "unpin selected item" : "keep selected item at the top" onClicked: root.togglePin(root.selectedRow.id) } PopupButton { objectName: "clipboardDelete" theme: root.theme width: 38 height: actionRail.height icon: "" showText: false role: "destructive" accessibleName: "delete selected item" tooltip: "remove selected item from clipboard history" onClicked: root.deleteRow(root.selectedRow.id) } } }