From afb5e59c298057a2ba220bc3fe64c2566b21a8a7 Mon Sep 17 00:00:00 2001 From: Nachum Barcohen <38861757+nabaco@users.noreply.github.com> Date: Sun, 1 Mar 2026 00:04:11 +0200 Subject: [PATCH] feat(clipboard): Add editing capability to clipboard entries --- .../Modals/Clipboard/ClipboardContent.qml | 2 + .../Modals/Clipboard/ClipboardEntry.qml | 22 +- .../Clipboard/ClipboardHistoryModal.qml | 271 +++++++++++++++++- 3 files changed, 290 insertions(+), 5 deletions(-) diff --git a/quickshell/Modals/Clipboard/ClipboardContent.qml b/quickshell/Modals/Clipboard/ClipboardContent.qml index e00bc22e..a4599b10 100644 --- a/quickshell/Modals/Clipboard/ClipboardContent.qml +++ b/quickshell/Modals/Clipboard/ClipboardContent.qml @@ -145,6 +145,7 @@ Item { onDeleteRequested: clipboardContent.modal.deleteEntry(modelData) onPinRequested: clipboardContent.modal.pinEntry(modelData) onUnpinRequested: clipboardContent.modal.unpinEntry(modelData) + onEditRequested: clipboardContent.modal.editEntry(modelData) } } @@ -204,6 +205,7 @@ Item { onDeleteRequested: clipboardContent.modal.deletePinnedEntry(modelData) onPinRequested: clipboardContent.modal.pinEntry(modelData) onUnpinRequested: clipboardContent.modal.unpinEntry(modelData) + onEditRequested: clipboardContent.modal.editEntry(modelData) } } diff --git a/quickshell/Modals/Clipboard/ClipboardEntry.qml b/quickshell/Modals/Clipboard/ClipboardEntry.qml index 53a2ecec..d0383ff9 100644 --- a/quickshell/Modals/Clipboard/ClipboardEntry.qml +++ b/quickshell/Modals/Clipboard/ClipboardEntry.qml @@ -17,6 +17,7 @@ Rectangle { signal deleteRequested signal pinRequested signal unpinRequested + signal editRequested readonly property string entryType: modal ? modal.getEntryType(entry) : "text" readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : "" @@ -70,6 +71,20 @@ Rectangle { onClicked: entry.pinned ? unpinRequested() : pinRequested() } + DankActionButton { + iconName: "edit" + iconSize: Theme.iconSize - 6 + iconColor: Theme.surfaceText + + onClicked: { + if (entryType === "image") { + // TODO - forward to editing software + } else { + editRequested(); + } + } + } + DankActionButton { iconName: "close" iconSize: Theme.iconSize - 6 @@ -142,8 +157,11 @@ Rectangle { MouseArea { id: mouseArea - anchors.fill: parent - anchors.rightMargin: 80 + anchors.left: parent.left + anchors.right: actionButtons.left + anchors.rightMargin: Theme.spacingS + anchors.top: parent.top + anchors.bottom: parent.bottom hoverEnabled: true cursorShape: Qt.PointingHandCursor onPressed: mouse => { diff --git a/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml b/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml index 2d50403e..d224bec4 100644 --- a/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml +++ b/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml @@ -6,6 +6,7 @@ import qs.Common import qs.Modals.Clipboard import qs.Modals.Common import qs.Services +import qs.Widgets DankModal { id: clipboardHistoryModal @@ -22,6 +23,7 @@ DankModal { ClipboardService.selectedIndex = 0; ClipboardService.keyboardNavigationActive = false; } + property var editClipboardModal: null property bool showKeyboardHints: false property Component clipboardContent property int activeImageLoads: 0 @@ -43,6 +45,8 @@ DankModal { service: ClipboardService } + property string mode: "history" + function updateFilteredModel() { ClipboardService.updateFilteredModel(); } @@ -61,6 +65,7 @@ DankModal { function show() { open(); + mode = "history"; activeImageLoads = 0; shouldHaveFocus = true; ClipboardService.reset(); @@ -130,6 +135,21 @@ DankModal { return ClipboardService.getEntryType(entry); } + function editEntry(entry) { + if (!entry) { + return; + } + if (entry.isImage) { + return; + } + const editor = contentLoader.item?.editorView; + if (!editor) { + return; + } + editor.setEntry(entry); + mode = "editor"; + } + visible: false modalWidth: ClipboardConstants.modalWidth modalHeight: ClipboardConstants.modalHeight @@ -174,9 +194,254 @@ DankModal { property var confirmDialog: clearConfirmDialog clipboardContent: Component { - ClipboardContent { - modal: clipboardHistoryModal - clearConfirmDialog: clipboardHistoryModal.confirmDialog + Item { + id: viewContainer + + property alias editorView: editorView + property alias searchField: historyContent.searchField + + anchors.fill: parent + + Item { + id: historyView + + anchors.fill: parent + opacity: 1 + scale: 1 + visible: opacity > 0.01 + enabled: clipboardHistoryModal.mode === "history" + + ClipboardContent { + id: historyContent + anchors.fill: parent + modal: clipboardHistoryModal + clearConfirmDialog: clipboardHistoryModal.confirmDialog + } + } + + Item { + id: editorView + + anchors.fill: parent + opacity: 0 + scale: 0.98 + visible: opacity > 0.01 + enabled: clipboardHistoryModal.mode === "editor" + + property var entry: null + property string editorText: "" + + function setEntry(newEntry) { + entry = newEntry; + editorText = newEntry?.text ?? newEntry?.preview ?? ""; + if (editField) { + editField.text = editorText; + } + Qt.callLater(function () { + if (editField) { + editField.forceActiveFocus(); + } + }); + } + + Column { + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingM + + Item { + width: parent.width + height: ClipboardConstants.headerHeight + + DankActionButton { + iconName: "arrow_back" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + onClicked: clipboardHistoryModal.mode = "history" + } + + StyledText { + text: I18n.tr("Edit Clipboard") + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + anchors.centerIn: parent + } + + DankActionButton { + iconName: "close" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + onClicked: clipboardHistoryModal.mode = "history" + } + } + + StyledRect { + id: editFieldContainer + width: parent.width + height: Math.max(Theme.fontSizeMedium * 8, Theme.fontSizeMedium * 3) + radius: Theme.cornerRadius + color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + border.color: editField.activeFocus ? Theme.primary : Theme.outlineMedium + border.width: editField.activeFocus ? 2 : 1 + clip: true + + DankIcon { + id: editIcon + name: "edit" + size: Theme.iconSize + color: editField.activeFocus ? Theme.primary : Theme.surfaceVariantText + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.top: parent.top + anchors.topMargin: Theme.spacingM + } + + TextEdit { + id: editField + anchors.left: editIcon.right + anchors.leftMargin: Theme.spacingS + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.rightMargin: Theme.spacingM + anchors.topMargin: Theme.spacingS + anchors.bottomMargin: Theme.spacingS + text: editorView.editorText + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + wrapMode: TextEdit.Wrap + selectByMouse: true + Keys.forwardTo: [clipboardHistoryModal.modalFocusScope] + onTextChanged: editorView.editorText = text + Keys.onEscapePressed: function (event) { + clipboardHistoryModal.mode = "history"; + event.accepted = true; + } + } + + StyledText { + text: I18n.tr("Edit clipboard text") + font.pixelSize: Theme.fontSizeMedium + color: Theme.outlineButton + anchors.left: editField.left + anchors.right: editField.right + anchors.top: editField.top + anchors.bottom: editField.bottom + visible: editField.text.length === 0 && !editField.activeFocus + wrapMode: Text.WordWrap + } + } + + Row { + width: parent.width + spacing: Theme.spacingS + + Item { + id: buttonSpacer + width: Math.max(0, parent.width - cancelButton.width - saveButton.width - Theme.spacingS) + height: 1 + } + + DankButton { + id: cancelButton + text: I18n.tr("Cancel") + backgroundColor: Theme.surfaceContainerHigh + textColor: Theme.surfaceText + onClicked: clipboardHistoryModal.mode = "history" + } + + DankButton { + id: saveButton + text: I18n.tr("Save") + backgroundColor: Theme.primary + textColor: Theme.onPrimary + onClicked: { + DMSService.sendRequest("clipboard.copy", { + "text": editorView.editorText + }, function (response) { + if (response.error) { + ToastService.showError(I18n.tr("Failed to update clipboard")); + return; + } + clipboardHistoryModal.mode = "history"; + clipboardHistoryModal.refreshClipboard(); + }); + } + } + } + } + } + + states: [ + State { + name: "history" + when: clipboardHistoryModal.mode === "history" + PropertyChanges { + target: historyView + opacity: 1 + scale: 1 + } + PropertyChanges { + target: editorView + opacity: 0 + scale: 0.98 + } + }, + State { + name: "editor" + when: clipboardHistoryModal.mode === "editor" + PropertyChanges { + target: historyView + opacity: 0 + scale: 0.98 + } + PropertyChanges { + target: editorView + opacity: 1 + scale: 1 + } + } + ] + + transitions: [ + Transition { + from: "history" + to: "editor" + ParallelAnimation { + NumberAnimation { + property: "opacity" + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + NumberAnimation { + property: "scale" + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + }, + Transition { + from: "editor" + to: "history" + ParallelAnimation { + NumberAnimation { + property: "opacity" + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + NumberAnimation { + property: "scale" + duration: Theme.shortDuration + easing.type: Theme.emphasizedEasing + } + } + } + ] } } }