diff --git a/Common/SessionData.qml b/Common/SessionData.qml index f5e26ddc..dc40f4aa 100644 --- a/Common/SessionData.qml +++ b/Common/SessionData.qml @@ -37,6 +37,7 @@ Singleton { property int wallpaperCyclingInterval: 300 // seconds (5 minutes) property string wallpaperCyclingTime: "06:00" // HH:mm format property string lastBrightnessDevice: "" + property string notepadContent: "" Component.onCompleted: { loadSettings() @@ -104,6 +105,7 @@ Singleton { !== undefined ? settings.wallpaperCyclingTime : "06:00" lastBrightnessDevice = settings.lastBrightnessDevice !== undefined ? settings.lastBrightnessDevice : "" + notepadContent = settings.notepadContent !== undefined ? settings.notepadContent : "" } } catch (e) { @@ -137,7 +139,8 @@ Singleton { "wallpaperCyclingMode": wallpaperCyclingMode, "wallpaperCyclingInterval": wallpaperCyclingInterval, "wallpaperCyclingTime": wallpaperCyclingTime, - "lastBrightnessDevice": lastBrightnessDevice + "lastBrightnessDevice": lastBrightnessDevice, + "notepadContent": notepadContent }, null, 2)) } diff --git a/Modals/FileBrowserModal.qml b/Modals/FileBrowserModal.qml index 6fc14105..c85de582 100644 --- a/Modals/FileBrowserModal.qml +++ b/Modals/FileBrowserModal.qml @@ -25,6 +25,8 @@ DankModal { property int selectedIndex: -1 property bool keyboardNavigationActive: false property bool backButtonFocused: false + property bool saveMode: false // Enable save functionality + property string defaultFileName: "" // Default filename for save mode FolderListModel { id: folderModel @@ -624,6 +626,72 @@ DankModal { } } + // Save functionality - positioned at bottom in save mode + Row { + id: saveRow + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacingL + height: saveMode ? 40 : 0 + visible: saveMode + spacing: Theme.spacingM + + DankTextField { + id: fileNameInput + width: parent.width - saveButton.width - Theme.spacingM + height: 36 + text: defaultFileName + placeholderText: "Enter filename..." + ignoreLeftRightKeys: false // Allow arrow key navigation + focus: saveMode // Auto-focus when in save mode + + Component.onCompleted: { + if (saveMode) { + Qt.callLater(() => { + forceActiveFocus(); + }); + } + } + + onAccepted: { + if (text.trim() !== "") { + var fullPath = currentPath + "/" + text.trim(); + fileSelected(fullPath); + fileBrowserModal.close(); + } + } + } + + StyledRect { + id: saveButton + width: 80 + height: 36 + color: fileNameInput.text.trim() !== "" ? Theme.primary : Theme.surfaceVariant + radius: Theme.cornerRadius + + StyledText { + anchors.centerIn: parent + text: "Save" + color: fileNameInput.text.trim() !== "" ? Theme.primaryText : Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeMedium + } + + StateLayer { + stateColor: Theme.primary + cornerRadius: Theme.cornerRadius + enabled: fileNameInput.text.trim() !== "" + onClicked: { + if (fileNameInput.text.trim() !== "") { + var fullPath = currentPath + "/" + fileNameInput.text.trim(); + fileSelected(fullPath); + fileBrowserModal.close(); + } + } + } + } + } + FileBrowserKeyboardHints { id: keyboardHints anchors.bottom: parent.bottom diff --git a/Modals/NotepadModal.qml b/Modals/NotepadModal.qml new file mode 100644 index 00000000..f69655e7 --- /dev/null +++ b/Modals/NotepadModal.qml @@ -0,0 +1,399 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Io +import qs.Common +import qs.Services +import qs.Widgets + +DankModal { + id: root + + property bool notepadModalVisible: false + property bool fileDialogOpen: false + property string currentFileName: "" // Track the currently loaded file + + function show() { + notepadModalVisible = true; + shouldHaveFocus = Qt.binding(() => { + return notepadModalVisible && !fileDialogOpen; + }); + open(); + } + + function hide() { + notepadModalVisible = false; + // Clear filename when closing (so it doesn't persist between sessions) + currentFileName = ""; + close(); + } + + function toggle() { + if (notepadModalVisible) + hide(); + else + show(); + } + + visible: notepadModalVisible + width: 700 + height: 500 + enableShadow: true + onShouldHaveFocusChanged: { + console.log("Notepad: shouldHaveFocus changed to", shouldHaveFocus, "modalVisible:", notepadModalVisible, "dialogOpen:", fileDialogOpen); + } + onBackgroundClicked: hide() + + content: Component { + Item { + id: contentItem + + anchors.fill: parent + property alias textArea: textArea + + Connections { + target: root + function onNotepadModalVisibleChanged() { + if (root.notepadModalVisible) { + Qt.callLater(() => { + textArea.forceActiveFocus(); + }); + } + } + } + + Column { + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + Row { + width: parent.width + height: 40 + + Row { + width: parent.width - closeButton.width + spacing: Theme.spacingM + + Column { + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: "Notepad" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + } + + StyledText { + text: currentFileName + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + visible: currentFileName !== "" + elide: Text.ElideMiddle + maximumLineCount: 1 + width: 200 + } + } + + StyledText { + text: SessionData.notepadContent.length > 0 ? `${SessionData.notepadContent.length} characters` : "Empty" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + anchors.verticalCenter: parent.verticalCenter + } + + } + + DankActionButton { + id: closeButton + + iconName: "close" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + hoverColor: Theme.errorHover + onClicked: root.hide() + } + + } + + StyledRect { + width: parent.width + height: parent.height - 80 + color: Theme.surface + border.color: Theme.outlineMedium + border.width: 1 + radius: Theme.cornerRadius + + ScrollView { + id: scrollView + + anchors.fill: parent + anchors.margins: 1 + clip: true + + TextArea { + id: textArea + + text: SessionData.notepadContent + placeholderText: "Start typing your notes here..." + font.family: SettingsData.monoFontFamily + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + selectByMouse: true + selectByKeyboard: true + wrapMode: TextArea.Wrap + focus: root.notepadModalVisible + activeFocusOnTab: true + + onTextChanged: { + if (text !== SessionData.notepadContent) { + SessionData.notepadContent = text; + saveTimer.restart(); + } + } + + Keys.onEscapePressed: (event) => { + root.hide(); + event.accepted = true; + } + + Component.onCompleted: { + if (root.notepadModalVisible) { + Qt.callLater(() => { + forceActiveFocus(); + }); + } + } + + background: Rectangle { + color: "transparent" + } + + } + + } + + } + + Row { + width: parent.width + height: 32 + spacing: Theme.spacingL + + Row { + spacing: Theme.spacingS + + DankActionButton { + iconName: "save" + iconSize: Theme.iconSize - 2 + iconColor: Theme.primary + hoverColor: Theme.primaryHover + onClicked: { + console.log("Notepad: Opening save dialog, releasing modal focus"); + root.allowFocusOverride = true; + root.shouldHaveFocus = false; + fileDialogOpen = true; + saveBrowser.open(); + } + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: "Save to file" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + } + } + + Row { + spacing: Theme.spacingS + + DankActionButton { + iconName: "folder_open" + iconSize: Theme.iconSize - 2 + iconColor: Theme.secondary + hoverColor: Theme.secondaryHover + onClicked: { + console.log("Notepad: Opening load dialog, releasing modal focus"); + root.allowFocusOverride = true; + root.shouldHaveFocus = false; + fileDialogOpen = true; + loadBrowser.open(); + } + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: "Load file" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + } + } + + Item { + width: 1 + height: 1 + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: saveTimer.running ? "Auto-saving..." : "Auto-saved" + font.pixelSize: Theme.fontSizeSmall + color: saveTimer.running ? Theme.primary : Theme.surfaceTextMedium + opacity: SessionData.notepadContent.length > 0 ? 1 : 0 + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + + } + + Timer { + id: saveTimer + + interval: 1000 + repeat: false + onTriggered: SessionData.saveSettings() + } + + FileBrowserModal { + id: saveBrowser + + browserTitle: "Save Notepad File" + browserIcon: "save" + browserType: "notepad_save" + fileExtensions: ["*.txt", "*.*"] + allowStacking: true + saveMode: true + defaultFileName: "note.txt" + + onFileSelected: (path) => { + fileDialogOpen = false; + selectedFilePath = path; + const content = textArea.text; + if (content.length > 0) { + writeFileProcess.command = ["sh", "-c", `echo '${content.replace(/'/g, "'\\''")}' > '${path}'`]; + writeFileProcess.running = true; + } + close(); + // Restore modal focus + root.allowFocusOverride = false; + root.shouldHaveFocus = Qt.binding(() => { + return root.notepadModalVisible && !fileDialogOpen; + }); + // Restore focus to TextArea after dialog closes + Qt.callLater(() => { + textArea.forceActiveFocus(); + }); + } + + onDialogClosed: { + fileDialogOpen = false; + // Restore modal focus + root.allowFocusOverride = false; + root.shouldHaveFocus = Qt.binding(() => { + return root.notepadModalVisible && !fileDialogOpen; + }); + // Restore focus to TextArea after dialog closes + Qt.callLater(() => { + textArea.forceActiveFocus(); + }); + } + + property string selectedFilePath: "" + } + + FileBrowserModal { + id: loadBrowser + + browserTitle: "Load Notepad File" + browserIcon: "folder_open" + browserType: "notepad_load" + fileExtensions: ["*.txt", "*.*"] + allowStacking: true + + onFileSelected: (path) => { + fileDialogOpen = false; + // Clean the file path - remove file:// prefix if present + var cleanPath = path.toString().replace(/^file:\/\//, ''); + // Extract filename from path + var fileName = cleanPath.split('/').pop(); + currentFileName = fileName; + console.log("Notepad: Loading file from path:", cleanPath); + readFileProcess.command = ["cat", cleanPath]; + readFileProcess.running = true; + close(); + // Restore modal focus + root.allowFocusOverride = false; + root.shouldHaveFocus = Qt.binding(() => { + return root.notepadModalVisible && !fileDialogOpen; + }); + // Restore focus to TextArea after dialog closes + Qt.callLater(() => { + textArea.forceActiveFocus(); + }); + } + + onDialogClosed: { + fileDialogOpen = false; + // Restore modal focus + root.allowFocusOverride = false; + root.shouldHaveFocus = Qt.binding(() => { + return root.notepadModalVisible && !fileDialogOpen; + }); + // Restore focus to TextArea after dialog closes + Qt.callLater(() => { + textArea.forceActiveFocus(); + }); + } + } + + Process { + id: writeFileProcess + + command: [] + running: false + onExited: (exitCode) => { + if (exitCode === 0) + console.log("Notepad: File saved successfully"); + else + console.warn("Notepad: Failed to save file, exit code:", exitCode); + } + } + + Process { + id: readFileProcess + + command: [] + running: false + + stdout: StdioCollector { + onStreamFinished: { + console.log("Notepad: File content loaded, length:", text.length); + textArea.text = text; + SessionData.notepadContent = text; + SessionData.saveSettings(); + console.log("Notepad: File loaded and saved to session"); + } + } + + onExited: (exitCode) => { + console.log("Notepad: File read process exited with code:", exitCode); + if (exitCode !== 0) { + console.warn("Notepad: Failed to load file, exit code:", exitCode); + } + } + } + + } + + } + +} diff --git a/Modules/Settings/TopBarTab.qml b/Modules/Settings/TopBarTab.qml index c86cba6f..88fc1a4c 100644 --- a/Modules/Settings/TopBarTab.qml +++ b/Modules/Settings/TopBarTab.qml @@ -151,6 +151,12 @@ Item { "text": "Keyboard Layout Name", "description": "Displays the active keyboard layout and allows switching", "icon": "keyboard", + }, { + "id": "notepadButton", + "text": "Notepad", + "description": "Quick access to notepad", + "icon": "assignment", + "enabled": true }] property var defaultLeftWidgets: [{ "id": "launcherButton", diff --git a/Modules/Settings/WidgetsTab.qml b/Modules/Settings/WidgetsTab.qml index 69ba55a1..300a87c8 100644 --- a/Modules/Settings/WidgetsTab.qml +++ b/Modules/Settings/WidgetsTab.qml @@ -107,6 +107,12 @@ Item { "description": "Access to notifications and do not disturb", "icon": "notifications", "enabled": true + }, { + "id": "notepadButton", + "text": "Notepad", + "description": "Quick access to notepad", + "icon": "assignment", + "enabled": true }, { "id": "battery", "text": "Battery", @@ -419,9 +425,9 @@ Item { SettingsData.setTopBarCenterWidgets(defaultCenterWidgets) if (!SettingsData.topBarRightWidgets) - SettingsData.setTopBarRightWidgets( - defaultRightWidgets)["left""center""right"].forEach( - sectionId => { + SettingsData.setTopBarRightWidgets(defaultRightWidgets) + + ["left", "center", "right"].forEach(sectionId => { var widgets = [] if (sectionId === "left") widgets = SettingsData.topBarLeftWidgets.slice() diff --git a/Modules/TopBar/NotepadButton.qml b/Modules/TopBar/NotepadButton.qml new file mode 100644 index 00000000..827c38af --- /dev/null +++ b/Modules/TopBar/NotepadButton.qml @@ -0,0 +1,67 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property bool isActive: false + property string section: "right" + property var popupTarget: null + property var parentScreen: null + property real widgetHeight: 30 + property real barHeight: 48 + readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30)) + + signal clicked + + width: notepadIcon.width + horizontalPadding * 2 + height: widgetHeight + radius: SettingsData.topBarNoBackground ? 0 : Theme.cornerRadius + color: { + if (SettingsData.topBarNoBackground) return "transparent" + const baseColor = notepadArea.containsMouse + || root.isActive ? Theme.primaryPressed : Theme.secondaryHover + return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, + baseColor.a * Theme.widgetTransparency) + } + + DankIcon { + id: notepadIcon + anchors.centerIn: parent + name: "assignment" + size: Theme.iconSize - 6 + color: notepadArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText + } + + Rectangle { + width: 6 + height: 6 + radius: 3 + color: Theme.primary + anchors.right: parent.right + anchors.top: parent.top + anchors.rightMargin: SettingsData.topBarNoBackground ? 0 : 4 + anchors.topMargin: SettingsData.topBarNoBackground ? 0 : 4 + visible: SessionData.notepadContent.length > 0 + opacity: 0.8 + } + + MouseArea { + id: notepadArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: { + root.clicked() + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } +} \ No newline at end of file diff --git a/Modules/TopBar/TopBar.qml b/Modules/TopBar/TopBar.qml index 4cef86e8..2939cfcc 100644 --- a/Modules/TopBar/TopBar.qml +++ b/Modules/TopBar/TopBar.qml @@ -341,6 +341,8 @@ PanelWindow { return true case "vpn": return true + case "notepadButton": + return true default: return false } @@ -394,6 +396,8 @@ PanelWindow { return keyboardLayoutNameComponent case "vpn": return vpnComponent + case "notepadButton": + return notepadButtonComponent default: return null } @@ -1152,6 +1156,36 @@ PanelWindow { KeyboardLayoutName {} } + + Component { + id: notepadButtonComponent + + NotepadButton { + isActive: notepadModalLoader.item ? notepadModalLoader.item.visible : false + widgetHeight: root.widgetHeight + barHeight: root.effectiveBarHeight + section: { + if (parent && parent.parent === leftSection) + return "left" + if (parent && parent.parent === rightSection) + return "right" + if (parent && parent.parent === centerSection) + return "center" + return "right" + } + popupTarget: { + notepadModalLoader.active = true + return notepadModalLoader.item + } + parentScreen: root.screen + onClicked: { + notepadModalLoader.active = true + if (notepadModalLoader.item) { + notepadModalLoader.item.toggle() + } + } + } + } } } } diff --git a/README.md b/README.md index e3072671..11978563 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ https://github.com/user-attachments/assets/5ad934bb-e7aa-4c04-8d40-149181bd2d29 - **Dock** A dock with pinned apps support, recent apps support, and currently running application support. - **Control Center** A full control center with user profile information, network, bluetooth, audio input/output, display controls, and night mode automation. - **Lock Screen** Using quickshell's WlSessionLock +- **Notepad** A simple text notepad/scratchpad with auto-save to session data and file export/import functionality. **Features:** @@ -299,6 +300,9 @@ binds { Mod+Comma hotkey-overlay-title="Settings" { spawn "qs" "-c" "dms" "ipc" "call" "settings" "toggle"; } + Mod+P hotkey-overlay-title="Notepad" { + spawn "qs" "-c" "dms" "ipc" "call" "notepad" "toggle"; + } Super+Alt+L hotkey-overlay-title="Lock Screen" { spawn "qs" "-c" "dms" "ipc" "call" "lock" "lock"; } @@ -358,6 +362,7 @@ bind = SUPER, V, exec, qs -c dms ipc call clipboard toggle bind = SUPER, M, exec, qs -c dms ipc call processlist toggle bind = SUPER, N, exec, qs -c dms ipc call notifications toggle bind = SUPER, comma, exec, qs -c dms ipc call settings toggle +bind = SUPER, P, exec, qs -c dms ipc call notepad toggle bind = SUPERALT, L, exec, qs -c dms ipc call lock lock bind = SUPER, X, exec, qs -c dms ipc call powermenu toggle @@ -389,6 +394,7 @@ qs -c dms ipc call audio mute # Launch applications ```bash qs -c dms ipc call spotlight toggle +qs -c dms ipc call notepad toggle qs -c dms ipc call processlist toggle qs -c dms ipc call powermenu toggle ``` diff --git a/docs/IPC.md b/docs/IPC.md index d14dc824..01c48b3b 100644 --- a/docs/IPC.md +++ b/docs/IPC.md @@ -392,6 +392,14 @@ Power menu modal control for system power actions. - `close` - Hide power menu modal - `toggle` - Toggle power menu modal visibility +### Target: `notepad` +Notepad/scratchpad modal control for quick note-taking. + +**Functions:** +- `open` - Show notepad modal +- `close` - Hide notepad modal +- `toggle` - Toggle notepad modal visibility + ### Target: `file` File browser controls for selecting wallpapers and profile images. @@ -422,6 +430,9 @@ qs -c dms ipc call processlist toggle # Show power menu qs -c dms ipc call powermenu toggle +# Open notepad +qs -c dms ipc call notepad toggle + # Open file browsers qs -c dms ipc call file browse wallpaper qs -c dms ipc call file browse profile @@ -437,6 +448,7 @@ These IPC commands are designed to be used with window manager keybindings. Exam binds { Mod+Space { spawn "qs" "-c" "dms" "ipc" "call" "spotlight" "toggle"; } Mod+V { spawn "qs" "-c" "dms" "ipc" "call" "clipboard" "toggle"; } + Mod+P { spawn "qs" "-c" "dms" "ipc" "call" "notepad" "toggle"; } Mod+X { spawn "qs" "-c" "dms" "ipc" "call" "powermenu" "toggle"; } XF86AudioRaiseVolume { spawn "qs" "-c" "dms" "ipc" "call" "audio" "increment" "3"; } XF86MonBrightnessUp { spawn "qs" "-c" "dms" "ipc" "call" "brightness" "increment" "5" ""; } diff --git a/shell.qml b/shell.qml index 72da03d7..fa135b8a 100644 --- a/shell.qml +++ b/shell.qml @@ -283,6 +283,16 @@ ShellRoot { } + LazyLoader { + id: notepadModalLoader + + active: false + + NotepadModal { + id: notepadModal + } + } + LazyLoader { id: powerMenuModalLoader @@ -373,6 +383,33 @@ ShellRoot { target: "processlist" } + IpcHandler { + function open() { + notepadModalLoader.active = true + if (notepadModalLoader.item) + notepadModalLoader.item.show() + + return "NOTEPAD_OPEN_SUCCESS" + } + + function close() { + if (notepadModalLoader.item) + notepadModalLoader.item.hide() + + return "NOTEPAD_CLOSE_SUCCESS" + } + + function toggle() { + notepadModalLoader.active = true + if (notepadModalLoader.item) + notepadModalLoader.item.toggle() + + return "NOTEPAD_TOGGLE_SUCCESS" + } + + target: "notepad" + } + Variants { model: SettingsData.getFilteredScreens("toast")