mirror of
https://github.com/end-4/dots-hyprland.git
synced 2026-06-05 23:09:26 -05:00
feat: add sticky overlay functionality
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user