format, move text color to styled text area

This commit is contained in:
end-4
2025-11-14 10:57:26 +01:00
parent a5b941360c
commit f8ffe5e63f
2 changed files with 118 additions and 132 deletions
@@ -10,6 +10,7 @@ TextArea {
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
selectionColor: Appearance.colors.colSecondaryContainer selectionColor: Appearance.colors.colSecondaryContainer
placeholderTextColor: Appearance.m3colors.m3outline placeholderTextColor: Appearance.m3colors.m3outline
color: Appearance.colors.colOnLayer0
font { font {
family: Appearance.font.family.main family: Appearance.font.family.main
pixelSize: Appearance?.font.pixelSize.small ?? 15 pixelSize: Appearance?.font.pixelSize.small ?? 15
@@ -11,149 +11,146 @@ import qs.modules.ii.overlay
OverlayBackground { OverlayBackground {
id: root id: root
readonly property real panelPadding: 20
property string stickypadContents: "" property string stickypadContents: ""
property bool pendingReload: false property bool pendingReload: false
property var copylistEntries: [] property var copylistEntries: []
property string lastParsedCopylistText: "" property string lastParsedCopylistText: ""
property var parsedCopylistLines: [] property var parsedCopylistLines: []
property bool isClickthrough: false property bool isClickthrough: false
Component.onCompleted: { Component.onCompleted: {
stickypadFile.reload() stickypadFile.reload();
updateCopylistEntries() updateCopylistEntries();
} }
function saveStickypad() { function saveStickypad() {
if (!stickypadInput) if (!stickypadInput)
return return;
stickypadContents = stickypadInput.text stickypadContents = stickypadInput.text;
stickypadFile.setText(stickypadContents) stickypadFile.setText(stickypadContents);
} }
function focusStickypadAtEnd() { function focusStickypadAtEnd() {
if (!stickypadInput) if (!stickypadInput)
return return;
stickypadInput.forceActiveFocus() stickypadInput.forceActiveFocus();
const endPos = stickypadInput.text.length const endPos = stickypadInput.text.length;
applySelection(endPos, endPos) applySelection(endPos, endPos);
} }
function applySelection(cursorPos, anchorPos) { function applySelection(cursorPos, anchorPos) {
if (!stickypadInput) if (!stickypadInput)
return return;
const textLength = stickypadInput.text.length const textLength = stickypadInput.text.length;
const cursor = Math.max(0, Math.min(cursorPos, textLength)) const cursor = Math.max(0, Math.min(cursorPos, textLength));
const anchor = Math.max(0, Math.min(anchorPos, textLength)) const anchor = Math.max(0, Math.min(anchorPos, textLength));
stickypadInput.select(anchor, cursor) stickypadInput.select(anchor, cursor);
if (cursor === anchor) if (cursor === anchor)
stickypadInput.deselect() stickypadInput.deselect();
} }
function scheduleCopylistUpdate(immediate = false) { function scheduleCopylistUpdate(immediate = false) {
if (!stickypadInput) if (!stickypadInput)
return return;
if (immediate) { if (immediate) {
copyListDebounce.stop() copyListDebounce.stop();
updateCopylistEntries() updateCopylistEntries();
} else { } else {
copyListDebounce.restart() copyListDebounce.restart();
} }
} }
function updateCopylistEntries() { function updateCopylistEntries() {
if (!stickypadInput) if (!stickypadInput)
return return;
const textValue = stickypadInput.text const textValue = stickypadInput.text;
if (!textValue || textValue.length === 0) { if (!textValue || textValue.length === 0) {
lastParsedCopylistText = "" lastParsedCopylistText = "";
parsedCopylistLines = [] parsedCopylistLines = [];
copylistEntries = [] copylistEntries = [];
return return;
} }
if (textValue !== lastParsedCopylistText) { if (textValue !== lastParsedCopylistText) {
const lineRegex = /(.*?)(\r?\n|$)/g const lineRegex = /(.*?)(\r?\n|$)/g;
let match = null let match = null;
const parsed = [] const parsed = [];
while ((match = lineRegex.exec(textValue)) !== null) { while ((match = lineRegex.exec(textValue)) !== null) {
const lineText = match[1] const lineText = match[1];
const newlineText = match[2] const newlineText = match[2];
const lineStart = match.index const lineStart = match.index;
const lineEnd = lineStart + lineText.length const lineEnd = lineStart + lineText.length;
const bulletMatch = lineText.match(/^\s*-\s+(.*\S)\s*$/) const bulletMatch = lineText.match(/^\s*-\s+(.*\S)\s*$/);
if (bulletMatch) { if (bulletMatch) {
parsed.push({ parsed.push({
content: bulletMatch[1].trim(), content: bulletMatch[1].trim(),
start: lineStart, start: lineStart,
end: lineEnd end: lineEnd
}) });
} }
if (newlineText === "") if (newlineText === "")
break break;
} }
lastParsedCopylistText = textValue lastParsedCopylistText = textValue;
parsedCopylistLines = parsed parsedCopylistLines = parsed;
if (parsed.length === 0) { if (parsed.length === 0) {
copylistEntries = [] copylistEntries = [];
return return;
} }
} }
updateCopylistPositions() updateCopylistPositions();
} }
function updateCopylistPositions() { function updateCopylistPositions() {
if (!stickypadInput || parsedCopylistLines.length === 0) if (!stickypadInput || parsedCopylistLines.length === 0)
return return;
const rawSelectionStart = stickypadInput.selectionStart;
const rawSelectionStart = stickypadInput.selectionStart const rawSelectionEnd = stickypadInput.selectionEnd;
const rawSelectionEnd = stickypadInput.selectionEnd const selectionStart = rawSelectionStart === -1 ? stickypadInput.cursorPosition : rawSelectionStart;
const selectionStart = rawSelectionStart === -1 ? stickypadInput.cursorPosition : rawSelectionStart const selectionEnd = rawSelectionEnd === -1 ? stickypadInput.cursorPosition : rawSelectionEnd;
const selectionEnd = rawSelectionEnd === -1 ? stickypadInput.cursorPosition : rawSelectionEnd const rangeStart = Math.min(selectionStart, selectionEnd);
const rangeStart = Math.min(selectionStart, selectionEnd) const rangeEnd = Math.max(selectionStart, selectionEnd);
const rangeEnd = Math.max(selectionStart, selectionEnd)
const entries = parsedCopylistLines.map(line => { const entries = parsedCopylistLines.map(line => {
const caretIntersects = rangeEnd > line.start && rangeStart <= line.end const caretIntersects = rangeEnd > line.start && rangeStart <= line.end;
if (caretIntersects) if (caretIntersects)
return null return null;
const startRect = stickypadInput.positionToRectangle(line.start) const startRect = stickypadInput.positionToRectangle(line.start);
let endRect = stickypadInput.positionToRectangle(line.end) let endRect = stickypadInput.positionToRectangle(line.end);
if (!isFinite(startRect.y)) if (!isFinite(startRect.y))
return null return null;
if (!isFinite(endRect.y)) if (!isFinite(endRect.y))
endRect = startRect endRect = startRect;
const lineBottom = endRect.y + endRect.height const lineBottom = endRect.y + endRect.height;
const rectHeight = Math.max(lineBottom - startRect.y, stickypadInput.font.pixelSize + 8) const rectHeight = Math.max(lineBottom - startRect.y, stickypadInput.font.pixelSize + 8);
return { return {
content: line.content, content: line.content,
y: startRect.y, y: startRect.y,
height: rectHeight height: rectHeight
} };
}).filter(entry => entry !== null) }).filter(entry => entry !== null);
copylistEntries = entries copylistEntries = entries;
} }
ColumnLayout { ColumnLayout {
id: stickypadLayout id: stickypadLayout
anchors { anchors {
fill: parent fill: parent
margins: panelPadding margins: 16
} }
spacing: 14 spacing: 10
ScrollView { ScrollView {
id: editorScrollView id: editorScrollView
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
Layout.minimumHeight: 200
clip: true clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded ScrollBar.vertical.policy: ScrollBar.AsNeeded
onWidthChanged: root.scheduleCopylistUpdate(true) onWidthChanged: root.scheduleCopylistUpdate(true)
StyledTextArea { StyledTextArea {
id: stickypadInput id: stickypadInput
anchors { anchors {
@@ -167,19 +164,16 @@ OverlayBackground {
textFormat: TextEdit.PlainText textFormat: TextEdit.PlainText
background: null background: null
rightPadding: 44 rightPadding: 44
// Adapt text color to theme (light/dark mode) - START
color: Appearance.colors.colOnLayer0
// Adapt text color to theme (light/dark mode) - END
// Disable text area when clickthrough enabled - START // Disable text area when clickthrough enabled - START
enabled: GlobalStates.overlayOpen || !root.isClickthrough enabled: GlobalStates.overlayOpen || !root.isClickthrough
activeFocusOnTab: GlobalStates.overlayOpen || !root.isClickthrough activeFocusOnTab: GlobalStates.overlayOpen || !root.isClickthrough
// Disable text area when clickthrough enabled - END // Disable text area when clickthrough enabled - END
onTextChanged: { onTextChanged: {
if (stickypadInput.activeFocus) { if (stickypadInput.activeFocus) {
saveDebounce.restart() saveDebounce.restart();
} }
root.scheduleCopylistUpdate(true) root.scheduleCopylistUpdate(true);
} }
onCursorPositionChanged: root.scheduleCopylistUpdate() onCursorPositionChanged: root.scheduleCopylistUpdate()
onSelectionStartChanged: root.scheduleCopylistUpdate() onSelectionStartChanged: root.scheduleCopylistUpdate()
@@ -187,25 +181,25 @@ OverlayBackground {
onHeightChanged: root.scheduleCopylistUpdate(true) onHeightChanged: root.scheduleCopylistUpdate(true)
onContentHeightChanged: root.scheduleCopylistUpdate(true) onContentHeightChanged: root.scheduleCopylistUpdate(true)
} }
Item { Item {
anchors.fill: stickypadInput anchors.fill: stickypadInput
visible: copylistEntries.length > 0 visible: copylistEntries.length > 0
clip: true clip: true
Repeater { Repeater {
model: copylistEntries model: copylistEntries
delegate: Item { delegate: Item {
readonly property real lineHeight: Math.max(modelData.height, Appearance.font.pixelSize.normal + 6) readonly property real lineHeight: Math.max(modelData.height, Appearance.font.pixelSize.normal + 6)
readonly property real iconSizeLocal: Appearance.font.pixelSize.normal readonly property real iconSizeLocal: Appearance.font.pixelSize.normal
readonly property real hitPadding: 6 readonly property real hitPadding: 6
width: iconSizeLocal + hitPadding * 2 width: iconSizeLocal + hitPadding * 2
height: lineHeight height: lineHeight
y: modelData.y y: modelData.y
x: Math.max(stickypadInput.width - width - 8, 0) x: Math.max(stickypadInput.width - width - 8, 0)
z: 5 z: 5
Rectangle { Rectangle {
id: feedbackFlash id: feedbackFlash
anchors.centerIn: iconItem anchors.centerIn: iconItem
@@ -214,9 +208,9 @@ OverlayBackground {
radius: width / 2 radius: width / 2
color: Appearance.colors.colLayer2 color: Appearance.colors.colLayer2
opacity: 0 opacity: 0
z: -1 z: 999
} }
MaterialSymbol { MaterialSymbol {
id: iconItem id: iconItem
anchors.centerIn: parent anchors.centerIn: parent
@@ -237,7 +231,7 @@ OverlayBackground {
} }
} }
} }
MouseArea { MouseArea {
id: mouseArea id: mouseArea
anchors.fill: parent anchors.fill: parent
@@ -248,12 +242,12 @@ OverlayBackground {
onReleased: iconItem.scale = 1 onReleased: iconItem.scale = 1
onCanceled: iconItem.scale = 1 onCanceled: iconItem.scale = 1
onClicked: { onClicked: {
feedbackFlash.opacity = 0.6 feedbackFlash.opacity = 0.6;
feedbackFade.restart() feedbackFade.restart();
Quickshell.clipboardText = modelData.content Quickshell.clipboardText = modelData.content;
} }
} }
NumberAnimation { NumberAnimation {
id: feedbackFade id: feedbackFade
target: feedbackFlash target: feedbackFlash
@@ -266,72 +260,63 @@ OverlayBackground {
} }
} }
} }
Rectangle { StyledText {
id: statusLabel
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 28 // Layout.preferredHeight: 28
Layout.minimumHeight: 28 horizontalAlignment: Text.AlignRight
color: "transparent" text: saveDebounce.running ? Translation.tr("Saving...") : Translation.tr("Saved")
color: Appearance.colors.colSubtext
StyledText { font.pixelSize: Appearance.font.pixelSize.small
id: statusLabel font.weight: Font.Medium
anchors {
right: parent.right
verticalCenter: parent.verticalCenter
rightMargin: 8
}
text: saveDebounce.running ? "Saving..." : "Saved"
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.small
font.weight: Font.Medium
}
} }
} }
Timer { Timer {
id: saveDebounce id: saveDebounce
interval: 500 interval: 500
repeat: false repeat: false
onTriggered: saveStickypad() onTriggered: saveStickypad()
} }
Timer { Timer {
id: copyListDebounce id: copyListDebounce
interval: 100 interval: 100
repeat: false repeat: false
onTriggered: updateCopylistPositions() onTriggered: updateCopylistPositions()
} }
FileView { FileView {
id: stickypadFile id: stickypadFile
path: Qt.resolvedUrl(Directories.stickypadPath) path: Qt.resolvedUrl(Directories.stickypadPath)
onLoaded: { onLoaded: {
stickypadContents = stickypadFile.text() stickypadContents = stickypadFile.text();
if (stickypadInput && stickypadInput.text !== stickypadContents) { if (stickypadInput && stickypadInput.text !== stickypadContents) {
const previousCursor = stickypadInput.cursorPosition const previousCursor = stickypadInput.cursorPosition;
const previousAnchor = stickypadInput.selectionStart const previousAnchor = stickypadInput.selectionStart;
stickypadInput.text = stickypadContents stickypadInput.text = stickypadContents;
applySelection(previousCursor, previousAnchor) applySelection(previousCursor, previousAnchor);
} }
if (pendingReload) { if (pendingReload) {
pendingReload = false pendingReload = false;
Qt.callLater(focusStickypadAtEnd) Qt.callLater(focusStickypadAtEnd);
} }
Qt.callLater(root.updateCopylistEntries) Qt.callLater(root.updateCopylistEntries);
} }
onLoadFailed: (error) => { onLoadFailed: error => {
if (error === FileViewError.FileNotFound) { if (error === FileViewError.FileNotFound) {
stickypadContents = "" stickypadContents = "";
stickypadFile.setText(stickypadContents) stickypadFile.setText(stickypadContents);
if (stickypadInput) if (stickypadInput)
stickypadInput.text = stickypadContents stickypadInput.text = stickypadContents;
if (pendingReload) { if (pendingReload) {
pendingReload = false pendingReload = false;
Qt.callLater(focusStickypadAtEnd) Qt.callLater(focusStickypadAtEnd);
} }
Qt.callLater(root.updateCopylistEntries) Qt.callLater(root.updateCopylistEntries);
} else { } else {
console.log("[Stickypad] Error loading file: " + error) console.log("[Stickypad] Error loading file: " + error);
} }
} }
} }