Files
dots-hyprland/dots/.config/quickshell/ii/modules/ii/overlay/notes/NotesContent.qml
T
2025-11-14 14:32:58 +01:00

291 lines
10 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.ii.overlay
OverlayBackground {
id: root
property alias content: textInput.text
property bool pendingReload: false
property var copyListEntries: []
property string lastParsedCopylistText: ""
property var parsedCopylistLines: []
property bool isClickthrough: false
property real maxCopyButtonSize: 20
Component.onCompleted: {
noteFile.reload();
updateCopyListEntries();
}
function saveContent() {
if (!textInput)
return;
noteFile.setText(root.content);
}
function focusAtEnd() {
if (!textInput)
return;
textInput.forceActiveFocus();
const endPos = root.content.length;
applySelection(endPos, endPos);
}
function applySelection(cursorPos, anchorPos) {
if (!textInput)
return;
const textLength = root.content.length;
const cursor = Math.max(0, Math.min(cursorPos, textLength));
const anchor = Math.max(0, Math.min(anchorPos, textLength));
textInput.select(anchor, cursor);
if (cursor === anchor)
textInput.deselect();
}
function scheduleCopylistUpdate(immediate = false) {
if (!textInput)
return;
if (immediate) {
copyListDebounce?.stop();
updateCopyListEntries();
} else {
copyListDebounce.restart();
}
}
function updateCopyListEntries() {
if (!textInput)
return;
const textValue = root.content;
if (!textValue || textValue.length === 0) {
lastParsedCopylistText = "";
parsedCopylistLines = [];
root.copyListEntries = [];
return;
}
if (textValue !== lastParsedCopylistText) {
const lineRegex = /(.*?)(\r?\n|$)/g;
let match = null;
const parsed = [];
while ((match = lineRegex.exec(textValue)) !== null) {
const lineText = match[1];
const newlineText = match[2];
const lineStart = match.index;
const lineEnd = lineStart + lineText.length;
const bulletMatch = lineText.match(/^\s*-\s+(.*\S)\s*$/);
if (bulletMatch) {
parsed.push({
content: bulletMatch[1].trim(),
start: lineStart,
end: lineEnd
});
}
if (newlineText === "")
break;
}
lastParsedCopylistText = textValue;
parsedCopylistLines = parsed;
if (parsed.length === 0) {
root.copyListEntries = [];
return;
}
}
updateCopylistPositions();
}
function updateCopylistPositions() {
if (!textInput || parsedCopylistLines.length === 0)
return;
const rawSelectionStart = textInput.selectionStart;
const rawSelectionEnd = textInput.selectionEnd;
const selectionStart = rawSelectionStart === -1 ? textInput.cursorPosition : rawSelectionStart;
const selectionEnd = rawSelectionEnd === -1 ? textInput.cursorPosition : rawSelectionEnd;
const rangeStart = Math.min(selectionStart, selectionEnd);
const rangeEnd = Math.max(selectionStart, selectionEnd);
const entries = parsedCopylistLines.map(line => {
// Don't show copy button if line is (partially) selected
const caretIntersects = rangeEnd > line.start && rangeStart <= line.end;
if (caretIntersects)
return null;
const startRect = textInput.positionToRectangle(line.start);
let endRect = textInput.positionToRectangle(line.end);
if (!isFinite(startRect.y))
return null;
if (!isFinite(endRect.y))
endRect = startRect;
const lineBottom = endRect.y + endRect.height;
const rectHeight = Math.max(lineBottom - startRect.y, textInput.font.pixelSize + 8);
return {
content: line.content,
y: startRect.y,
height: rectHeight
};
}).filter(entry => entry !== null);
root.copyListEntries = entries;
}
ColumnLayout {
id: contentItem
anchors.fill: parent
spacing: -16
ScrollView {
id: editorScrollView
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
onWidthChanged: root.scheduleCopylistUpdate(true)
StyledTextArea { // This has to be a direct child of ScrollView for proper scrolling
id: textInput
anchors {
left: parent.left
right: parent.right
}
wrapMode: TextEdit.Wrap
placeholderText: Translation.tr("Write something here...\nUse '-' to create copyable bullet points, like this:\n\nSheep fricker\n- 4x Slab\n- 1x Boat\n- 4x Redstone Dust\n- 1x Sticky Piston\n- 1x End Rod\n- 4x Redstone Repeater\n- 1x Redstone Torch\n- 1x Sheep")
selectByMouse: true
persistentSelection: true
textFormat: TextEdit.PlainText
background: null
padding: 16
rightPadding: root.maxCopyButtonSize + padding
onTextChanged: {
if (textInput.activeFocus) {
saveDebounce.restart();
}
root.scheduleCopylistUpdate(true);
}
onHeightChanged: root.scheduleCopylistUpdate(true)
onContentHeightChanged: root.scheduleCopylistUpdate(true)
onCursorPositionChanged: root.scheduleCopylistUpdate()
onSelectionStartChanged: root.scheduleCopylistUpdate()
onSelectionEndChanged: root.scheduleCopylistUpdate()
}
Item {
anchors.fill: textInput
visible: root.copyListEntries.length > 0
clip: true
Repeater {
model: ScriptModel {
values: root.copyListEntries
}
delegate: RippleButton {
id: copyButton
required property var modelData
readonly property real lineHeight: Math.min(Math.max(modelData.height, Appearance.font.pixelSize.normal + 6), root.maxCopyButtonSize)
readonly property real iconSizeLocal: Appearance.font.pixelSize.normal
readonly property real hitPadding: 6
property bool justCopied: false
implicitHeight: lineHeight
implicitWidth: lineHeight
buttonRadius: height / 2
y: modelData.y
anchors.right: parent.right
anchors.rightMargin: 16
z: 5
Timer {
id: resetState
interval: 700
onTriggered: {
copyButton.justCopied = false;
}
}
onClicked: {
Quickshell.clipboardText = copyButton.modelData.content;
justCopied = true;
resetState.start();
}
contentItem: Item {
anchors.centerIn: parent
MaterialSymbol {
id: iconItem
anchors.centerIn: parent
text: copyButton.justCopied ? "check" : "content_copy"
iconSize: copyButton.iconSizeLocal
color: Appearance.colors.colOnLayer1
}
}
}
}
}
}
StyledText {
id: statusLabel
Layout.fillWidth: true
Layout.margins: 16
horizontalAlignment: Text.AlignRight
text: saveDebounce.running ? Translation.tr("Saving...") : Translation.tr("Saved ")
color: Appearance.colors.colSubtext
}
}
Timer {
id: saveDebounce
interval: 500
repeat: false
onTriggered: saveContent()
}
Timer {
id: copyListDebounce
interval: 100
repeat: false
onTriggered: updateCopylistPositions()
}
FileView {
id: noteFile
path: Qt.resolvedUrl(Directories.notesPath)
onLoaded: {
root.content = noteFile.text();
if (root.content !== root.content) {
const previousCursor = textInput.cursorPosition;
const previousAnchor = textInput.selectionStart;
root.content = root.content;
applySelection(previousCursor, previousAnchor);
}
if (pendingReload) {
pendingReload = false;
Qt.callLater(root.focusAtEnd);
}
Qt.callLater(root.updateCopyListEntries);
}
onLoadFailed: error => {
if (error === FileViewError.FileNotFound) {
root.content = "";
noteFile.setText(root.content);
if (pendingReload) {
pendingReload = false;
Qt.callLater(root.focusAtEnd);
}
Qt.callLater(root.updateCopyListEntries);
} else {
console.log("[Overlay Notes] Error loading file: " + error);
}
}
}
}