From 549a43ac7f2ae8d60f6d1ce3896856295e94a650 Mon Sep 17 00:00:00 2001 From: marepallisanthosh999333 <173584372+marepallisanthosh999333@users.noreply.github.com> Date: Sat, 8 Nov 2025 20:29:14 +0530 Subject: [PATCH] feat: add sticky overlay functionality --- .../ii/modules/common/Directories.qml | 6 +- .../ii/modules/common/Persistent.qml | 8 + .../quickshell/ii/modules/overlay/Overlay.qml | 5 +- .../ii/modules/overlay/OverlayContext.qml | 5 +- .../overlay/OverlayWidgetDelegateChooser.qml | 6 + .../modules/overlay/stickypad/Stickypad.qml | 23 ++ .../overlay/stickypad/StickypadContent.qml | 363 ++++++++++++++++++ .../ii/modules/overlay/stickypad/qmldir | 4 + 8 files changed, 417 insertions(+), 3 deletions(-) create mode 100644 dots/.config/quickshell/ii/modules/overlay/stickypad/Stickypad.qml create mode 100644 dots/.config/quickshell/ii/modules/overlay/stickypad/StickypadContent.qml create mode 100644 dots/.config/quickshell/ii/modules/overlay/stickypad/qmldir diff --git a/dots/.config/quickshell/ii/modules/common/Directories.qml b/dots/.config/quickshell/ii/modules/common/Directories.qml index 59f3dab53..7f1694f45 100644 --- a/dots/.config/quickshell/ii/modules/common/Directories.qml +++ b/dots/.config/quickshell/ii/modules/common/Directories.qml @@ -31,7 +31,11 @@ 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`) + // CUSTOM: Stickypad integration - START + property string stickypadPath: FileUtils.trimFileProtocol(`${Directories.state}/user/stickypad.txt`) + // CUSTOM: Stickypad integration - END + 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`) diff --git a/dots/.config/quickshell/ii/modules/common/Persistent.qml b/dots/.config/quickshell/ii/modules/common/Persistent.qml index 5848b1683..ee5d327cc 100644 --- a/dots/.config/quickshell/ii/modules/common/Persistent.qml +++ b/dots/.config/quickshell/ii/modules/common/Persistent.qml @@ -113,6 +113,14 @@ Singleton { property real x: 1576 property real y: 630 } + // CUSTOM: Stickypad integration - START + property JsonObject stickypad: JsonObject { + property bool pinned: true + property bool clickthrough: false + property real x: 100 + property real y: 100 + } + // CUSTOM: Stickypad integration - END } property JsonObject timer: JsonObject { diff --git a/dots/.config/quickshell/ii/modules/overlay/Overlay.qml b/dots/.config/quickshell/ii/modules/overlay/Overlay.qml index 71ba510c6..12e3d6b44 100644 --- a/dots/.config/quickshell/ii/modules/overlay/Overlay.qml +++ b/dots/.config/quickshell/ii/modules/overlay/Overlay.qml @@ -25,7 +25,10 @@ Scope { exclusionMode: ExclusionMode.Ignore WlrLayershell.namespace: "quickshell:overlay" WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.keyboardFocus: GlobalStates.overlayOpen ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None + // CUSTOM: Stickypad keyboard focus fix - START + // 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) + // CUSTOM: Stickypad keyboard focus fix - END visible: true color: "transparent" diff --git a/dots/.config/quickshell/ii/modules/overlay/OverlayContext.qml b/dots/.config/quickshell/ii/modules/overlay/OverlayContext.qml index b228deda6..4eed3e5b5 100644 --- a/dots/.config/quickshell/ii/modules/overlay/OverlayContext.qml +++ b/dots/.config/quickshell/ii/modules/overlay/OverlayContext.qml @@ -10,7 +10,10 @@ Singleton { { identifier: "volumeMixer", materialSymbol: "volume_up" }, { identifier: "crosshair", materialSymbol: "point_scan" }, { identifier: "fpsLimiter", materialSymbol: "animation" }, - { identifier: "resources", materialSymbol: "browse_activity" } + { identifier: "resources", materialSymbol: "browse_activity" }, + // CUSTOM: Stickypad integration - START + { identifier: "stickypad", materialSymbol: "note_stack" } + // CUSTOM: Stickypad integration - END ] readonly property bool hasPinnedWidgets: root.pinnedWidgetIdentifiers.length > 0 diff --git a/dots/.config/quickshell/ii/modules/overlay/OverlayWidgetDelegateChooser.qml b/dots/.config/quickshell/ii/modules/overlay/OverlayWidgetDelegateChooser.qml index fc75d1455..823b5d488 100644 --- a/dots/.config/quickshell/ii/modules/overlay/OverlayWidgetDelegateChooser.qml +++ b/dots/.config/quickshell/ii/modules/overlay/OverlayWidgetDelegateChooser.qml @@ -11,6 +11,9 @@ import qs.modules.overlay.volumeMixer import qs.modules.overlay.fpsLimiter import qs.modules.overlay.recorder import qs.modules.overlay.resources +// CUSTOM: Stickypad integration - START +import qs.modules.overlay.stickypad +// CUSTOM: Stickypad integration - END DelegateChooser { id: root @@ -21,4 +24,7 @@ DelegateChooser { DelegateChoice { roleValue: "fpsLimiter"; FpsLimiter {} } DelegateChoice { roleValue: "recorder"; Recorder {} } DelegateChoice { roleValue: "resources"; Resources {} } + // CUSTOM: Stickypad integration - START + DelegateChoice { roleValue: "stickypad"; Stickypad {} } + // CUSTOM: Stickypad integration - END } diff --git a/dots/.config/quickshell/ii/modules/overlay/stickypad/Stickypad.qml b/dots/.config/quickshell/ii/modules/overlay/stickypad/Stickypad.qml new file mode 100644 index 000000000..149b65ac0 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/overlay/stickypad/Stickypad.qml @@ -0,0 +1,23 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.modules.common +import qs.modules.overlay + +StyledOverlayWidget { + id: root + title: "Stickypad" + minWidth: 440 + showCenterButton: true + + // Override opacity to always stay fully opaque, even in clickthrough mode + opacity: 1.0 + + contentItem: StickypadContent { + implicitWidth: 440 + implicitHeight: 380 + // CUSTOM: Pass clickthrough state to content - START + isClickthrough: root.clickthrough + // CUSTOM: Pass clickthrough state to content - END + } +} diff --git a/dots/.config/quickshell/ii/modules/overlay/stickypad/StickypadContent.qml b/dots/.config/quickshell/ii/modules/overlay/stickypad/StickypadContent.qml new file mode 100644 index 000000000..db57ab40a --- /dev/null +++ b/dots/.config/quickshell/ii/modules/overlay/stickypad/StickypadContent.qml @@ -0,0 +1,363 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import qs.modules.common +import qs.modules.common.widgets + +Rectangle { + id: root + + readonly property real panelPadding: 20 + property string stickypadContents: "" + property bool pendingReload: false + property var copylistEntries: [] + property string lastParsedCopylistText: "" + property var parsedCopylistLines: [] + // CUSTOM: Track clickthrough state to disable text editing - START + property bool isClickthrough: false + // CUSTOM: Track clickthrough state to disable text editing - END + + color: Appearance.colors.colLayer0 + radius: Appearance.rounding.windowRounding - 6 + + Component.onCompleted: { + stickypadFile.reload() + updateCopylistEntries() + } + + function saveStickypad() { + if (!stickypadInput) + return + stickypadContents = stickypadInput.text + stickypadFile.setText(stickypadContents) + } + + function focusStickypadAtEnd() { + if (!stickypadInput) + return + stickypadInput.forceActiveFocus() + const endPos = stickypadInput.text.length + applySelection(endPos, endPos) + } + + function applySelection(cursorPos, anchorPos) { + if (!stickypadInput) + return + const textLength = stickypadInput.text.length + const cursor = Math.max(0, Math.min(cursorPos, textLength)) + const anchor = Math.max(0, Math.min(anchorPos, textLength)) + stickypadInput.select(anchor, cursor) + if (cursor === anchor) + stickypadInput.deselect() + } + + function scheduleCopylistUpdate(immediate = false) { + if (!stickypadInput) + return + if (immediate) { + copylistDebounce.stop() + updateCopylistEntries() + } else { + copylistDebounce.restart() + } + } + + function updateCopylistEntries() { + if (!stickypadInput) + return + const textValue = stickypadInput.text + if (!textValue || textValue.length === 0) { + lastParsedCopylistText = "" + parsedCopylistLines = [] + 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) { + copylistEntries = [] + return + } + } + + updateCopylistPositions() + } + + function updateCopylistPositions() { + if (!stickypadInput || parsedCopylistLines.length === 0) + return + + const rawSelectionStart = stickypadInput.selectionStart + const rawSelectionEnd = stickypadInput.selectionEnd + const selectionStart = rawSelectionStart === -1 ? stickypadInput.cursorPosition : rawSelectionStart + const selectionEnd = rawSelectionEnd === -1 ? stickypadInput.cursorPosition : rawSelectionEnd + const rangeStart = Math.min(selectionStart, selectionEnd) + const rangeEnd = Math.max(selectionStart, selectionEnd) + + const entries = parsedCopylistLines.map(line => { + const caretIntersects = rangeEnd > line.start && rangeStart <= line.end + if (caretIntersects) + return null + const startRect = stickypadInput.positionToRectangle(line.start) + let endRect = stickypadInput.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, stickypadInput.font.pixelSize + 8) + return { + content: line.content, + y: startRect.y, + height: rectHeight + } + }).filter(entry => entry !== null) + + copylistEntries = entries + } + + ColumnLayout { + id: stickypadLayout + anchors { + fill: parent + margins: panelPadding + } + spacing: 14 + + ScrollView { + id: editorScrollView + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: 200 + clip: true + ScrollBar.vertical.policy: ScrollBar.AsNeeded + onWidthChanged: root.scheduleCopylistUpdate(true) + + StyledTextArea { + id: stickypadInput + wrapMode: TextEdit.Wrap + placeholderText: Translation.tr("Write...") + selectByMouse: true + persistentSelection: true + textFormat: TextEdit.PlainText + background: null + rightPadding: 44 + // CUSTOM: Adapt text color to theme (light/dark mode) - START + color: Appearance.colors.colOnLayer0 + // CUSTOM: Adapt text color to theme (light/dark mode) - END + // CUSTOM: Disable text area when clickthrough enabled - START + // Allow editing when overlay is open OR when clickthrough is disabled + enabled: GlobalStates.overlayOpen || !root.isClickthrough + activeFocusOnTab: GlobalStates.overlayOpen || !root.isClickthrough + + // Release focus when clickthrough is enabled and overlay closes + Connections { + target: root + function onIsClickthroughChanged() { + if (root.isClickthrough && !GlobalStates.overlayOpen && stickypadInput.activeFocus) { + stickypadInput.focus = false + } + } + } + + Connections { + target: GlobalStates + function onOverlayOpenChanged() { + if (!GlobalStates.overlayOpen && root.isClickthrough && stickypadInput.activeFocus) { + stickypadInput.focus = false + } + } + } + // CUSTOM: Disable text area when clickthrough enabled - END + + Keys.onPressed: (event) => { + if (event.key === Qt.Key_Escape && event.modifiers === Qt.NoModifier) { + Persistent.states.overlay.open = Persistent.states.overlay.open.filter(type => type !== "stickypad") + event.accepted = true + return + } + } + onTextChanged: { + if (stickypadInput.activeFocus) { + saveDebounce.restart() + } + root.scheduleCopylistUpdate(true) + } + onCursorPositionChanged: root.scheduleCopylistUpdate() + onSelectionStartChanged: root.scheduleCopylistUpdate() + onSelectionEndChanged: root.scheduleCopylistUpdate() + onHeightChanged: root.scheduleCopylistUpdate(true) + onContentHeightChanged: root.scheduleCopylistUpdate(true) + } + + Item { + anchors.fill: stickypadInput + visible: copylistEntries.length > 0 + clip: true + + Repeater { + model: copylistEntries + delegate: Item { + readonly property real lineHeight: Math.max(modelData.height, Appearance.font.pixelSize.normal + 6) + readonly property real iconSizeLocal: Appearance.font.pixelSize.normal + readonly property real hitPadding: 6 + + width: iconSizeLocal + hitPadding * 2 + height: lineHeight + y: modelData.y + x: Math.max(stickypadInput.width - width - 8, 0) + z: 5 + + Rectangle { + id: feedbackFlash + anchors.centerIn: iconItem + width: iconSizeLocal + hitPadding + height: width + radius: width / 2 + color: Appearance.colors.colLayer2 + opacity: 0 + z: -1 + } + + MaterialSymbol { + id: iconItem + anchors.centerIn: parent + text: "content_copy" + iconSize: iconSizeLocal + color: Appearance.colors.colOnLayer1 + opacity: mouseArea.containsMouse ? 1 : 0.85 + scale: 1 + Behavior on scale { + NumberAnimation { + duration: 120 + easing.type: Easing.OutQuad + } + } + Behavior on opacity { + NumberAnimation { + duration: 120 + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + anchors.margins: hitPadding + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: iconItem.scale = 0.85 + onReleased: iconItem.scale = 1 + onCanceled: iconItem.scale = 1 + onClicked: { + feedbackFlash.opacity = 0.6 + feedbackFade.restart() + Quickshell.clipboardText = modelData.content + } + } + + NumberAnimation { + id: feedbackFade + target: feedbackFlash + property: "opacity" + to: 0 + duration: 200 + easing.type: Easing.OutQuad + } + } + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 28 + Layout.minimumHeight: 28 + color: "transparent" + + StyledText { + id: statusLabel + anchors { + right: parent.right + verticalCenter: parent.verticalCenter + rightMargin: 8 + } + text: saveDebounce.running ? "Saving..." : "Saved" + color: saveDebounce.running ? Appearance.colors.colAccent : Appearance.colors.colSubtext + font.pixelSize: Appearance.font.pixelSize.small + font.weight: Font.Medium + } + } + } + + Timer { + id: saveDebounce + interval: 500 + repeat: false + onTriggered: saveStickypad() + } + + Timer { + id: copylistDebounce + interval: 100 + repeat: false + onTriggered: updateCopylistPositions() + } + + FileView { + id: stickypadFile + path: Qt.resolvedUrl(Directories.stickypadPath) + onLoaded: { + stickypadContents = stickypadFile.text() + if (stickypadInput && stickypadInput.text !== stickypadContents) { + const previousCursor = stickypadInput.cursorPosition + const previousAnchor = stickypadInput.selectionStart + stickypadInput.text = stickypadContents + applySelection(previousCursor, previousAnchor) + } + if (pendingReload) { + pendingReload = false + Qt.callLater(focusStickypadAtEnd) + } + Qt.callLater(root.updateCopylistEntries) + } + onLoadFailed: (error) => { + if (error === FileViewError.FileNotFound) { + stickypadContents = "" + stickypadFile.setText(stickypadContents) + if (stickypadInput) + stickypadInput.text = stickypadContents + if (pendingReload) { + pendingReload = false + Qt.callLater(focusStickypadAtEnd) + } + Qt.callLater(root.updateCopylistEntries) + } else { + console.log("[Stickypad] Error loading file: " + error) + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/overlay/stickypad/qmldir b/dots/.config/quickshell/ii/modules/overlay/stickypad/qmldir new file mode 100644 index 000000000..13d756392 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/overlay/stickypad/qmldir @@ -0,0 +1,4 @@ +module qs.modules.overlay.stickypad + +Stickypad 1.0 Stickypad.qml +StickypadContent 1.0 StickypadContent.qml