forked from Shinonome/dots-hyprland
@@ -32,7 +32,9 @@ Singleton {
|
||||
property string shellConfig: FileUtils.trimFileProtocol(`${Directories.config}/illogical-impulse`)
|
||||
property string shellConfigName: "config.json"
|
||||
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 generatedMaterialThemePath: FileUtils.trimFileProtocol(`${Directories.state}/user/generated/colors.json`)
|
||||
property string generatedWallpaperCategoryPath: FileUtils.trimFileProtocol(`${Directories.state}/user/generated/wallpaper/category.txt`)
|
||||
|
||||
@@ -131,6 +131,14 @@ Singleton {
|
||||
property real height: 600
|
||||
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 {
|
||||
|
||||
@@ -10,6 +10,7 @@ TextArea {
|
||||
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
|
||||
selectionColor: Appearance.colors.colSecondaryContainer
|
||||
placeholderTextColor: Appearance.m3colors.m3outline
|
||||
color: Appearance.colors.colOnLayer0
|
||||
font {
|
||||
family: Appearance.font.family.main
|
||||
pixelSize: Appearance?.font.pixelSize.small ?? 15
|
||||
|
||||
@@ -25,7 +25,8 @@ Scope {
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.namespace: "quickshell: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
|
||||
color: "transparent"
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ Singleton {
|
||||
{ identifier: "floatingImage", materialSymbol: "imagesmode" },
|
||||
{ identifier: "recorder", materialSymbol: "screen_record" },
|
||||
{ identifier: "resources", materialSymbol: "browse_activity" },
|
||||
{ identifier: "notes", materialSymbol: "note_stack" },
|
||||
{ identifier: "volumeMixer", materialSymbol: "volume_up" },
|
||||
]
|
||||
|
||||
|
||||
@@ -8,10 +8,11 @@ import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import qs.modules.ii.overlay.crosshair
|
||||
import qs.modules.ii.overlay.volumeMixer
|
||||
import qs.modules.ii.overlay.floatingImage
|
||||
import qs.modules.ii.overlay.fpsLimiter
|
||||
import qs.modules.ii.overlay.recorder
|
||||
import qs.modules.ii.overlay.resources
|
||||
import qs.modules.ii.overlay.floatingImage
|
||||
import qs.modules.ii.overlay.notes
|
||||
|
||||
DelegateChooser {
|
||||
id: root
|
||||
@@ -22,5 +23,6 @@ DelegateChooser {
|
||||
DelegateChoice { roleValue: "fpsLimiter"; FpsLimiter {} }
|
||||
DelegateChoice { roleValue: "recorder"; Recorder {} }
|
||||
DelegateChoice { roleValue: "resources"; Resources {} }
|
||||
DelegateChoice { roleValue: "notes"; Notes {} }
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user