mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-08 04:09:15 -04:00
045ac59a44
* feat(clipboard): Add editing capability to clipboard entries * Add split save menu for clipboard editor * Add clipboard editor shortcuts and hints * Show full clipboard text in editor * feat(Clipboard): Revive ClipboardEditor PR - Original PR #1916 by @nabaco * fix(clipboard): restore Save button targets in editor --------- Co-authored-by: Nachum Barcohen <38861757+nabaco@users.noreply.github.com>
520 lines
17 KiB
QML
520 lines
17 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import QtQuick.Controls
|
|
import qs.Common
|
|
import qs.Services
|
|
import qs.Widgets
|
|
|
|
Item {
|
|
id: root
|
|
|
|
required property var modal
|
|
property var keyController: null
|
|
|
|
property var entry: null
|
|
property string editorText: ""
|
|
|
|
function decodeEntryData(data) {
|
|
if (!data) {
|
|
return "";
|
|
}
|
|
if (typeof data !== "string") {
|
|
return String(data);
|
|
}
|
|
|
|
const sanitized = data.replace(/\s+/g, "");
|
|
if (sanitized.length === 0) {
|
|
return "";
|
|
}
|
|
|
|
try {
|
|
const chars = new Array(sanitized.length);
|
|
for (let i = 0; i < sanitized.length; i++) {
|
|
chars[i] = sanitized.charAt(i);
|
|
}
|
|
|
|
let buffer = null;
|
|
if (typeof Qt !== "undefined" && typeof Qt.atob === "function") {
|
|
buffer = Qt.atob(chars);
|
|
} else if (typeof atob === "function") {
|
|
const binary = atob(sanitized);
|
|
const bytes = new Uint8Array(binary.length);
|
|
for (let i = 0; i < binary.length; i++) {
|
|
bytes[i] = binary.charCodeAt(i);
|
|
}
|
|
buffer = bytes.buffer;
|
|
}
|
|
if (!buffer || buffer.byteLength === 0) {
|
|
return data;
|
|
}
|
|
|
|
const bytes = new Uint8Array(buffer);
|
|
let binary = "";
|
|
for (let i = 0; i < bytes.length; i++) {
|
|
binary += String.fromCharCode(bytes[i]);
|
|
}
|
|
|
|
try {
|
|
return decodeURIComponent(escape(binary));
|
|
} catch (e) {
|
|
return binary;
|
|
}
|
|
} catch (e) {
|
|
return data;
|
|
}
|
|
}
|
|
|
|
function setEntry(newEntry) {
|
|
entry = newEntry;
|
|
editorText = newEntry?.text ?? newEntry?.preview ?? "";
|
|
if (editField) {
|
|
editField.text = editorText;
|
|
}
|
|
Qt.callLater(function () {
|
|
if (editField) {
|
|
editField.forceActiveFocus();
|
|
}
|
|
});
|
|
|
|
if (!newEntry || newEntry.isImage) {
|
|
return;
|
|
}
|
|
|
|
const requestedId = newEntry.id;
|
|
DMSService.sendRequest("clipboard.getEntry", {
|
|
"id": requestedId
|
|
}, function (response) {
|
|
if (response.error) {
|
|
return;
|
|
}
|
|
if (!root.entry || root.entry.id !== requestedId) {
|
|
return;
|
|
}
|
|
const result = response.result;
|
|
let fullText = "";
|
|
if (result?.data) {
|
|
fullText = root.decodeEntryData(result.data);
|
|
} else {
|
|
fullText = result?.preview ?? "";
|
|
}
|
|
|
|
if (!fullText || fullText.length === 0) {
|
|
return;
|
|
}
|
|
root.editorText = fullText;
|
|
if (editField) {
|
|
editField.text = fullText;
|
|
}
|
|
});
|
|
}
|
|
|
|
function saveEntry(action) {
|
|
const saveAction = action ?? "history";
|
|
DMSService.sendRequest("clipboard.copy", {
|
|
"text": root.editorText
|
|
}, function (response) {
|
|
if (response.error) {
|
|
ToastService.showError(I18n.tr("Failed to update clipboard"));
|
|
return;
|
|
}
|
|
if (saveAction === "history") {
|
|
modal.mode = "history";
|
|
Qt.callLater(function () {
|
|
ClipboardService.reset();
|
|
ClipboardService.refresh();
|
|
if (keyController) {
|
|
keyController.reset();
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
if (saveAction === "close") {
|
|
modal.hide();
|
|
return;
|
|
}
|
|
if (saveAction === "paste") {
|
|
ClipboardService.pasteClipboard(modal.hide);
|
|
}
|
|
});
|
|
}
|
|
|
|
function positionSaveMenu() {
|
|
saveMenu.width = Math.max(saveMenuColumn.implicitWidth + saveMenu.padding * 2, saveButton.width);
|
|
const pos = saveButton.mapToItem(Overlay.overlay, 0, 0);
|
|
const popupW = saveMenu.width;
|
|
const popupH = saveMenu.height;
|
|
const overlayW = Overlay.overlay.width;
|
|
const overlayH = Overlay.overlay.height;
|
|
|
|
let x = pos.x + (saveButton.width - popupW) / 2;
|
|
let y = pos.y + saveButton.height + 4;
|
|
if (y + popupH > overlayH) {
|
|
y = pos.y - popupH - 4;
|
|
}
|
|
|
|
x = Math.max(8, Math.min(x, overlayW - popupW - 8));
|
|
y = Math.max(8, y);
|
|
|
|
saveMenu.x = x;
|
|
saveMenu.y = y;
|
|
}
|
|
|
|
function toggleSaveMenu() {
|
|
if (saveMenu.visible) {
|
|
saveMenu.close();
|
|
return;
|
|
}
|
|
saveMenu.open();
|
|
positionSaveMenu();
|
|
Qt.callLater(positionSaveMenu);
|
|
}
|
|
|
|
Shortcut {
|
|
sequences: ["Escape"]
|
|
enabled: modal.mode === "editor"
|
|
onActivated: modal.mode = "history"
|
|
}
|
|
|
|
Column {
|
|
anchors.fill: parent
|
|
anchors.margins: Theme.spacingM
|
|
spacing: Theme.spacingM
|
|
|
|
Item {
|
|
id: editorHeader
|
|
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: modal.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: modal.mode = "history"
|
|
}
|
|
}
|
|
|
|
StyledRect {
|
|
id: editFieldContainer
|
|
width: parent.width
|
|
height: Math.max(Theme.fontSizeMedium * 8, parent.height - editorHeader.height - editorActions.height - Theme.spacingM * 2)
|
|
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
|
|
}
|
|
|
|
DankFlickable {
|
|
id: editScroll
|
|
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
|
|
clip: true
|
|
contentWidth: width
|
|
contentHeight: editField.height
|
|
|
|
TextEdit {
|
|
id: editField
|
|
width: editScroll.width
|
|
height: Math.max(editScroll.height, contentHeight)
|
|
text: root.editorText
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.surfaceText
|
|
wrapMode: TextEdit.Wrap
|
|
selectByMouse: true
|
|
onTextChanged: root.editorText = text
|
|
Keys.onPressed: function (event) {
|
|
const hasCtrl = (event.modifiers & Qt.ControlModifier) !== 0;
|
|
const hasShift = (event.modifiers & Qt.ShiftModifier) !== 0;
|
|
|
|
if (hasCtrl && event.key === Qt.Key_S) {
|
|
root.saveEntry(hasShift ? "close" : "history");
|
|
event.accepted = true;
|
|
return;
|
|
}
|
|
if (hasCtrl && hasShift && event.key === Qt.Key_V) {
|
|
root.saveEntry("paste");
|
|
event.accepted = true;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("Edit clipboard text")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.outlineButton
|
|
anchors.left: editScroll.left
|
|
anchors.right: editScroll.right
|
|
anchors.top: editScroll.top
|
|
anchors.bottom: editScroll.bottom
|
|
visible: editField.text.length === 0 && !editField.activeFocus
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
}
|
|
|
|
Row {
|
|
id: editorActions
|
|
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: modal.mode = "history"
|
|
}
|
|
|
|
Item {
|
|
id: saveButton
|
|
|
|
readonly property int buttonHeight: cancelButton.buttonHeight
|
|
readonly property int arrowWidth: Theme.iconSizeLarge
|
|
|
|
width: cancelButton.width
|
|
height: buttonHeight
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
radius: Theme.cornerRadius
|
|
color: Theme.primary
|
|
}
|
|
|
|
Item {
|
|
id: saveMainArea
|
|
anchors.left: parent.left
|
|
anchors.right: saveArrowArea.left
|
|
anchors.top: parent.top
|
|
anchors.bottom: parent.bottom
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("Save")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
font.weight: Font.Medium
|
|
color: Theme.onPrimary
|
|
anchors.centerIn: saveMainArea
|
|
}
|
|
|
|
Item {
|
|
id: saveArrowArea
|
|
width: saveButton.arrowWidth
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
anchors.bottom: parent.bottom
|
|
}
|
|
|
|
Rectangle {
|
|
width: 1
|
|
height: parent.height - cancelButton.horizontalPadding
|
|
color: Theme.withAlpha(Theme.onPrimary, 0.2)
|
|
anchors.right: saveArrowArea.left
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
DankIcon {
|
|
name: saveMenu.visible ? "expand_less" : "expand_more"
|
|
size: Theme.iconSizeSmall
|
|
color: Theme.onPrimary
|
|
anchors.centerIn: saveArrowArea
|
|
}
|
|
|
|
StateLayer {
|
|
z: 1
|
|
anchors.fill: saveMainArea
|
|
stateColor: Theme.onPrimary
|
|
onClicked: root.saveEntry("history")
|
|
}
|
|
|
|
StateLayer {
|
|
z: 1
|
|
anchors.fill: saveArrowArea
|
|
stateColor: Theme.onPrimary
|
|
onClicked: root.toggleSaveMenu()
|
|
}
|
|
}
|
|
}
|
|
|
|
Popup {
|
|
id: saveMenu
|
|
parent: Overlay.overlay
|
|
padding: Theme.spacingM
|
|
modal: true
|
|
dim: false
|
|
focus: true
|
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
|
|
|
background: StyledRect {
|
|
radius: Theme.cornerRadius
|
|
color: Theme.surfaceContainer
|
|
border.color: Theme.outlineMedium
|
|
border.width: 1
|
|
}
|
|
|
|
contentItem: Column {
|
|
id: saveMenuColumn
|
|
spacing: Theme.spacingXS
|
|
|
|
StyledRect {
|
|
implicitWidth: saveMenuRow.implicitWidth + Theme.spacingS * 2
|
|
implicitHeight: saveMenuRow.implicitHeight + Theme.spacingS * 2
|
|
radius: Theme.cornerRadius
|
|
color: saveMenuSaveArea.containsMouse ? Theme.surfaceVariant : "transparent"
|
|
|
|
Row {
|
|
id: saveMenuRow
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Theme.spacingS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: Theme.spacingS
|
|
|
|
DankIcon {
|
|
name: "save"
|
|
size: Theme.iconSizeSmall
|
|
color: Theme.surfaceText
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("Save")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.surfaceText
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: saveMenuSaveArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
saveMenu.close();
|
|
root.saveEntry("history");
|
|
}
|
|
}
|
|
}
|
|
|
|
StyledRect {
|
|
implicitWidth: saveMenuCloseRow.implicitWidth + Theme.spacingS * 2
|
|
implicitHeight: saveMenuCloseRow.implicitHeight + Theme.spacingS * 2
|
|
radius: Theme.cornerRadius
|
|
color: saveMenuCloseArea.containsMouse ? Theme.surfaceVariant : "transparent"
|
|
|
|
Row {
|
|
id: saveMenuCloseRow
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Theme.spacingS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: Theme.spacingS
|
|
|
|
DankIcon {
|
|
name: "close"
|
|
size: Theme.iconSizeSmall
|
|
color: Theme.surfaceText
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("Save and close")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.surfaceText
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: saveMenuCloseArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
saveMenu.close();
|
|
root.saveEntry("close");
|
|
}
|
|
}
|
|
}
|
|
|
|
StyledRect {
|
|
implicitWidth: saveMenuPasteRow.implicitWidth + Theme.spacingS * 2
|
|
implicitHeight: saveMenuPasteRow.implicitHeight + Theme.spacingS * 2
|
|
radius: Theme.cornerRadius
|
|
color: saveMenuPasteArea.containsMouse ? Theme.surfaceVariant : "transparent"
|
|
opacity: modal.wtypeAvailable ? 1 : 0.5
|
|
|
|
Row {
|
|
id: saveMenuPasteRow
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Theme.spacingS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: Theme.spacingS
|
|
|
|
DankIcon {
|
|
name: "content_paste"
|
|
size: Theme.iconSizeSmall
|
|
color: Theme.surfaceText
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("Save and paste")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.surfaceText
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: saveMenuPasteArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
enabled: modal.wtypeAvailable
|
|
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
onClicked: {
|
|
saveMenu.close();
|
|
root.saveEntry("paste");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|