overlay: notes widget (closes #2231) (#2402)

This commit is contained in:
end-4
2025-11-14 14:35:47 +01:00
committed by GitHub
8 changed files with 325 additions and 3 deletions
@@ -32,7 +32,9 @@ Singleton {
property string shellConfig: FileUtils.trimFileProtocol(`${Directories.config}/illogical-impulse`) property string shellConfig: FileUtils.trimFileProtocol(`${Directories.config}/illogical-impulse`)
property string shellConfigName: "config.json" property string shellConfigName: "config.json"
property string shellConfigPath: `${Directories.shellConfig}/${Directories.shellConfigName}` property string shellConfigPath: `${Directories.shellConfig}/${Directories.shellConfigName}`
property string todoPath: FileUtils.trimFileProtocol(`${Directories.state}/user/todo.json`) property string todoPath: FileUtils.trimFileProtocol(`${Directories.state}/user/todo.json`)
property string notesPath: FileUtils.trimFileProtocol(`${Directories.state}/user/notes.txt`)
property string conflictCachePath: FileUtils.trimFileProtocol(`${Directories.cache}/conflict-killer`)
property string notificationsPath: FileUtils.trimFileProtocol(`${Directories.cache}/notifications/notifications.json`) property string notificationsPath: FileUtils.trimFileProtocol(`${Directories.cache}/notifications/notifications.json`)
property string generatedMaterialThemePath: FileUtils.trimFileProtocol(`${Directories.state}/user/generated/colors.json`) property string generatedMaterialThemePath: FileUtils.trimFileProtocol(`${Directories.state}/user/generated/colors.json`)
property string generatedWallpaperCategoryPath: FileUtils.trimFileProtocol(`${Directories.state}/user/generated/wallpaper/category.txt`) property string generatedWallpaperCategoryPath: FileUtils.trimFileProtocol(`${Directories.state}/user/generated/wallpaper/category.txt`)
@@ -131,6 +131,14 @@ Singleton {
property real height: 600 property real height: 600
property int tabIndex: 0 property int tabIndex: 0
} }
property JsonObject notes: JsonObject {
property bool pinned: false
property bool clickthrough: true
property real x: 1400
property real y: 42
property real width: 460
property real height: 330
}
} }
property JsonObject timer: JsonObject { property JsonObject timer: JsonObject {
@@ -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
@@ -25,7 +25,8 @@ Scope {
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "quickshell:overlay" WlrLayershell.namespace: "quickshell:overlay"
WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: GlobalStates.overlayOpen ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None // Use OnDemand for pinned widgets to allow focus switching with mouse clicks
WlrLayershell.keyboardFocus: GlobalStates.overlayOpen ? WlrKeyboardFocus.Exclusive : (OverlayContext.clickableWidgets.length > 0 ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None)
visible: true visible: true
color: "transparent" color: "transparent"
@@ -11,6 +11,7 @@ Singleton {
{ identifier: "floatingImage", materialSymbol: "imagesmode" }, { identifier: "floatingImage", materialSymbol: "imagesmode" },
{ identifier: "recorder", materialSymbol: "screen_record" }, { identifier: "recorder", materialSymbol: "screen_record" },
{ identifier: "resources", materialSymbol: "browse_activity" }, { identifier: "resources", materialSymbol: "browse_activity" },
{ identifier: "notes", materialSymbol: "note_stack" },
{ identifier: "volumeMixer", materialSymbol: "volume_up" }, { identifier: "volumeMixer", materialSymbol: "volume_up" },
] ]
@@ -8,10 +8,11 @@ import Quickshell
import Quickshell.Bluetooth import Quickshell.Bluetooth
import qs.modules.ii.overlay.crosshair import qs.modules.ii.overlay.crosshair
import qs.modules.ii.overlay.volumeMixer import qs.modules.ii.overlay.volumeMixer
import qs.modules.ii.overlay.floatingImage
import qs.modules.ii.overlay.fpsLimiter import qs.modules.ii.overlay.fpsLimiter
import qs.modules.ii.overlay.recorder import qs.modules.ii.overlay.recorder
import qs.modules.ii.overlay.resources import qs.modules.ii.overlay.resources
import qs.modules.ii.overlay.floatingImage import qs.modules.ii.overlay.notes
DelegateChooser { DelegateChooser {
id: root id: root
@@ -22,5 +23,6 @@ DelegateChooser {
DelegateChoice { roleValue: "fpsLimiter"; FpsLimiter {} } DelegateChoice { roleValue: "fpsLimiter"; FpsLimiter {} }
DelegateChoice { roleValue: "recorder"; Recorder {} } DelegateChoice { roleValue: "recorder"; Recorder {} }
DelegateChoice { roleValue: "resources"; Resources {} } DelegateChoice { roleValue: "resources"; Resources {} }
DelegateChoice { roleValue: "notes"; Notes {} }
DelegateChoice { roleValue: "volumeMixer"; VolumeMixer {} } DelegateChoice { roleValue: "volumeMixer"; VolumeMixer {} }
} }
@@ -0,0 +1,17 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.services
import qs.modules.common
import qs.modules.ii.overlay
StyledOverlayWidget {
id: root
title: Translation.tr("Notes")
showCenterButton: true
contentItem: NotesContent {
radius: root.contentRadius
isClickthrough: root.clickthrough
}
}
@@ -0,0 +1,290 @@
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);
}
}
}
}