feat: add sticky overlay functionality

This commit is contained in:
marepallisanthosh999333
2025-11-08 20:29:14 +05:30
parent 769ed3bf71
commit 549a43ac7f
8 changed files with 417 additions and 3 deletions
@@ -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`)
@@ -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 {
@@ -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"
@@ -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
@@ -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
}
@@ -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
}
}
@@ -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)
}
}
}
}
@@ -0,0 +1,4 @@
module qs.modules.overlay.stickypad
Stickypad 1.0 Stickypad.qml
StickypadContent 1.0 StickypadContent.qml