From d4816bd174901cb5582151dac6ead636cf96090d Mon Sep 17 00:00:00 2001 From: purian23 Date: Sat, 20 Sep 2025 14:22:13 -0400 Subject: [PATCH] Modularize the Notepad --- Modules/Notepad/Notepad.qml | 505 ++++++++++ Modules/Notepad/NotepadSettings.qml | 268 ++++++ Modules/Notepad/NotepadTabs.qml | 153 +++ Modules/Notepad/NotepadTextEditor.qml | 291 ++++++ Modules/NotepadSlideout.qml | 1278 ------------------------- Modules/TopBar/TopBar.qml | 12 +- Widgets/DankSlideout.qml | 175 ++++ shell.qml | 64 +- 8 files changed, 1430 insertions(+), 1316 deletions(-) create mode 100644 Modules/Notepad/Notepad.qml create mode 100644 Modules/Notepad/NotepadSettings.qml create mode 100644 Modules/Notepad/NotepadTabs.qml create mode 100644 Modules/Notepad/NotepadTextEditor.qml delete mode 100644 Modules/NotepadSlideout.qml create mode 100644 Widgets/DankSlideout.qml diff --git a/Modules/Notepad/Notepad.qml b/Modules/Notepad/Notepad.qml new file mode 100644 index 00000000..fe65ec29 --- /dev/null +++ b/Modules/Notepad/Notepad.qml @@ -0,0 +1,505 @@ +import QtQuick +import QtQuick.Controls +import QtCore +import Quickshell +import Quickshell.Wayland +import Quickshell.Io +import qs.Common +import qs.Modals.Common +import qs.Modals.FileBrowser +import qs.Services +import qs.Widgets + +pragma ComponentBehavior: Bound + +Item { + id: root + + property bool fileDialogOpen: false + property string currentFileName: "" + property url currentFileUrl + property bool confirmationDialogOpen: false + property string pendingAction: "" + property url pendingFileUrl + property string lastSavedFileContent: "" + property var currentTab: NotepadStorageService.tabs.length > NotepadStorageService.currentTabIndex ? NotepadStorageService.tabs[NotepadStorageService.currentTabIndex] : null + property bool showSettingsMenu: false + property string pendingSaveContent: "" + + signal hideRequested() + + function hasUnsavedChanges() { + return textEditor.hasUnsavedChanges() + } + + function hasUnsavedTemporaryContent() { + return hasUnsavedChanges() + } + + function createNewTab() { + performCreateNewTab() + } + + function performCreateNewTab() { + NotepadStorageService.createNewTab() + textEditor.text = "" + textEditor.lastSavedContent = "" + textEditor.contentLoaded = true + textEditor.textArea.forceActiveFocus() + } + + function closeTab(tabIndex) { + if (tabIndex === NotepadStorageService.currentTabIndex && hasUnsavedChanges()) { + root.pendingAction = "close_tab_" + tabIndex + root.confirmationDialogOpen = true + confirmationDialog.open() + } else { + performCloseTab(tabIndex) + } + } + + function performCloseTab(tabIndex) { + NotepadStorageService.closeTab(tabIndex) + Qt.callLater(() => { + textEditor.loadCurrentTabContent() + }) + } + + function switchToTab(tabIndex) { + if (tabIndex < 0 || tabIndex >= NotepadStorageService.tabs.length) return + + if (textEditor.contentLoaded) { + textEditor.autoSaveToSession() + } + + NotepadStorageService.switchToTab(tabIndex) + Qt.callLater(() => { + textEditor.loadCurrentTabContent() + if (currentTab) { + root.currentFileName = currentTab.fileName || "" + root.currentFileUrl = currentTab.fileUrl || "" + } + }) + } + + function saveToFile(fileUrl) { + if (!currentTab) return + + var content = textEditor.text + var filePath = fileUrl.toString().replace(/^file:\/\//, '') + + saveFileView.path = "" + pendingSaveContent = content + saveFileView.path = filePath + + Qt.callLater(() => { + saveFileView.setText(pendingSaveContent) + }) + } + + function loadFromFile(fileUrl) { + if (hasUnsavedTemporaryContent()) { + root.pendingFileUrl = fileUrl + root.pendingAction = "load_file" + root.confirmationDialogOpen = true + confirmationDialog.open() + } else { + performLoadFromFile(fileUrl) + } + } + + function performLoadFromFile(fileUrl) { + const filePath = fileUrl.toString().replace(/^file:\/\//, '') + const fileName = filePath.split('/').pop() + + loadFileView.path = "" + loadFileView.path = filePath + + if (loadFileView.waitForJob()) { + Qt.callLater(() => { + var content = loadFileView.text() + if (currentTab && content !== undefined && content !== null) { + textEditor.text = content + textEditor.lastSavedContent = content + textEditor.contentLoaded = true + root.lastSavedFileContent = content + + NotepadStorageService.updateTabMetadata(NotepadStorageService.currentTabIndex, { + title: fileName, + filePath: filePath, + isTemporary: false + }) + + root.currentFileName = fileName + root.currentFileUrl = fileUrl + textEditor.saveCurrentTabContent() + } + }) + } + } + + Column { + anchors.fill: parent + spacing: Theme.spacingM + + NotepadTabs { + id: tabBar + width: parent.width + contentLoaded: textEditor.contentLoaded + + onTabSwitched: (tabIndex) => { + switchToTab(tabIndex) + } + + onTabClosed: (tabIndex) => { + closeTab(tabIndex) + } + + onNewTabRequested: { + createNewTab() + } + } + + NotepadTextEditor { + id: textEditor + width: parent.width + height: parent.height - tabBar.height - Theme.spacingM * 2 + + onSaveRequested: { + if (currentTab && !currentTab.isTemporary && currentTab.filePath) { + var fileUrl = "file://" + currentTab.filePath + saveToFile(fileUrl) + } else { + root.fileDialogOpen = true + saveBrowser.open() + } + } + + onOpenRequested: { + if (hasUnsavedChanges()) { + root.pendingAction = "open" + root.confirmationDialogOpen = true + confirmationDialog.open() + } else { + root.fileDialogOpen = true + loadBrowser.open() + } + } + + onNewRequested: { + if (hasUnsavedChanges()) { + root.pendingAction = "new" + root.confirmationDialogOpen = true + confirmationDialog.open() + } else { + createNewTab() + } + } + + onEscapePressed: { + root.hideRequested() + } + + onSettingsRequested: { + showSettingsMenu = !showSettingsMenu + } + } + } + + NotepadSettings { + id: notepadSettings + anchors.fill: parent + isVisible: showSettingsMenu + onSettingsRequested: showSettingsMenu = !showSettingsMenu + } + + FileView { + id: saveFileView + blockWrites: true + preload: false + atomicWrites: true + printErrors: true + + onSaved: { + if (currentTab && saveFileView.path && pendingSaveContent) { + NotepadStorageService.updateTabMetadata(NotepadStorageService.currentTabIndex, { + hasUnsavedChanges: false, + lastSavedContent: pendingSaveContent + }) + root.lastSavedFileContent = pendingSaveContent + pendingSaveContent = "" + } + } + + onSaveFailed: (error) => { + pendingSaveContent = "" + } + } + + FileView { + id: loadFileView + blockLoading: true + preload: true + atomicWrites: true + printErrors: true + + onLoadFailed: (error) => { + } + } + + FileBrowserModal { + id: saveBrowser + + browserTitle: qsTr("Save Notepad File") + browserIcon: "save" + browserType: "notepad_save" + fileExtensions: ["*.txt", "*.md", "*.*"] + allowStacking: true + saveMode: true + defaultFileName: { + if (currentTab && currentTab.title && currentTab.title !== "Untitled") { + return currentTab.title + } else if (currentTab && !currentTab.isTemporary && currentTab.filePath) { + return currentTab.filePath.split('/').pop() + } else { + return "note.txt" + } + } + + WlrLayershell.layer: WlrLayershell.Overlay + + onFileSelected: (path) => { + root.fileDialogOpen = false + const cleanPath = path.toString().replace(/^file:\/\//, '') + const fileName = cleanPath.split('/').pop() + const fileUrl = "file://" + cleanPath + + root.currentFileName = fileName + root.currentFileUrl = fileUrl + + if (currentTab) { + NotepadStorageService.saveTabAs( + NotepadStorageService.currentTabIndex, + cleanPath + ) + } + + saveToFile(fileUrl) + + if (root.pendingAction === "new") { + Qt.callLater(() => { + createNewTab() + }) + } else if (root.pendingAction === "open") { + Qt.callLater(() => { + root.fileDialogOpen = true + loadBrowser.open() + }) + } else if (root.pendingAction.startsWith("close_tab_")) { + Qt.callLater(() => { + var tabIndex = parseInt(root.pendingAction.split("_")[2]) + performCloseTab(tabIndex) + }) + } + root.pendingAction = "" + + close() + } + + onDialogClosed: { + root.fileDialogOpen = false + } + } + + FileBrowserModal { + id: loadBrowser + + browserTitle: qsTr("Open Notepad File") + browserIcon: "folder_open" + browserType: "notepad_load" + fileExtensions: ["*.txt", "*.md", "*.*"] + allowStacking: true + + WlrLayershell.layer: WlrLayershell.Overlay + + onFileSelected: (path) => { + root.fileDialogOpen = false + const cleanPath = path.toString().replace(/^file:\/\//, '') + const fileName = cleanPath.split('/').pop() + const fileUrl = "file://" + cleanPath + + root.currentFileName = fileName + root.currentFileUrl = fileUrl + + loadFromFile(fileUrl) + close() + } + + onDialogClosed: { + root.fileDialogOpen = false + } + } + + DankModal { + id: confirmationDialog + + width: 400 + height: 180 + shouldBeVisible: false + allowStacking: true + + onBackgroundClicked: { + close() + root.confirmationDialogOpen = false + } + + content: Component { + FocusScope { + anchors.fill: parent + focus: true + + Keys.onEscapePressed: event => { + confirmationDialog.close() + root.confirmationDialogOpen = false + event.accepted = true + } + + Column { + anchors.centerIn: parent + width: parent.width - Theme.spacingM * 2 + spacing: Theme.spacingM + + Row { + width: parent.width + + Column { + width: parent.width - 40 + spacing: Theme.spacingXS + + StyledText { + text: qsTr("Unsaved Changes") + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + } + + StyledText { + text: root.pendingAction === "new" ? + qsTr("You have unsaved changes. Save before creating a new file?") : + root.pendingAction.startsWith("close_tab_") ? + qsTr("You have unsaved changes. Save before closing this tab?") : + root.pendingAction === "load_file" || root.pendingAction === "open" ? + qsTr("You have unsaved changes. Save before opening a file?") : + qsTr("You have unsaved changes. Save before continuing?") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceTextMedium + width: parent.width + wrapMode: Text.Wrap + } + } + + DankActionButton { + iconName: "close" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: { + confirmationDialog.close() + root.confirmationDialogOpen = false + } + } + } + + Item { + width: parent.width + height: 40 + + Row { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + Rectangle { + width: Math.max(80, discardText.contentWidth + Theme.spacingM * 2) + height: 36 + radius: Theme.cornerRadius + color: discardArea.containsMouse ? Theme.surfaceTextHover : "transparent" + border.color: Theme.surfaceVariantAlpha + border.width: 1 + + StyledText { + id: discardText + anchors.centerIn: parent + text: qsTr("Don't Save") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + font.weight: Font.Medium + } + + MouseArea { + id: discardArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + confirmationDialog.close() + root.confirmationDialogOpen = false + if (root.pendingAction === "new") { + createNewTab() + } else if (root.pendingAction === "open") { + root.fileDialogOpen = true + loadBrowser.open() + } else if (root.pendingAction === "load_file") { + performLoadFromFile(root.pendingFileUrl) + } else if (root.pendingAction.startsWith("close_tab_")) { + var tabIndex = parseInt(root.pendingAction.split("_")[2]) + performCloseTab(tabIndex) + } + root.pendingAction = "" + root.pendingFileUrl = "" + } + } + } + + Rectangle { + width: Math.max(70, saveAsText.contentWidth + Theme.spacingM * 2) + height: 36 + radius: Theme.cornerRadius + color: saveAsArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary + + StyledText { + id: saveAsText + anchors.centerIn: parent + text: qsTr("Save") + font.pixelSize: Theme.fontSizeMedium + color: Theme.background + font.weight: Font.Medium + } + + MouseArea { + id: saveAsArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + confirmationDialog.close() + root.confirmationDialogOpen = false + root.fileDialogOpen = true + saveBrowser.open() + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + + } + } + } + } + } + } +} \ No newline at end of file diff --git a/Modules/Notepad/NotepadSettings.qml b/Modules/Notepad/NotepadSettings.qml new file mode 100644 index 00000000..1bca53d2 --- /dev/null +++ b/Modules/Notepad/NotepadSettings.qml @@ -0,0 +1,268 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Services +import qs.Widgets + +pragma ComponentBehavior: Bound + +Item { + id: root + + property bool isVisible: false + property var cachedFontFamilies: [] + property var cachedMonoFamilies: [] + property bool fontsEnumerated: false + + signal settingsRequested() + + function enumerateFonts() { + var fonts = ["Default"] + var availableFonts = Qt.fontFamilies() + var rootFamilies = [] + var seenFamilies = new Set() + for (var i = 0; i < availableFonts.length; i++) { + var fontName = availableFonts[i] + if (fontName.startsWith(".")) + continue + + if (fontName === SettingsData.defaultFontFamily) + continue + + var rootName = fontName.replace(/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i, "").replace(/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i, + "").replace(/ (UI|Display|Text|Mono|Sans|Serif)$/i, function (match, suffix) { + return match + }).trim() + if (!seenFamilies.has(rootName) && rootName !== "") { + seenFamilies.add(rootName) + rootFamilies.push(rootName) + } + } + cachedFontFamilies = fonts.concat(rootFamilies.sort()) + var monoFonts = ["Default"] + var monoFamilies = [] + var seenMonoFamilies = new Set() + for (var j = 0; j < availableFonts.length; j++) { + var fontName2 = availableFonts[j] + if (fontName2.startsWith(".")) + continue + + if (fontName2 === SettingsData.defaultMonoFontFamily) + continue + + var lowerName = fontName2.toLowerCase() + if (lowerName.includes("mono") || lowerName.includes("code") || lowerName.includes("console") || lowerName.includes("terminal") || lowerName.includes("courier") || lowerName.includes("dejavu sans mono") || lowerName.includes( + "jetbrains") || lowerName.includes("fira") || lowerName.includes("hack") || lowerName.includes("source code") || lowerName.includes("ubuntu mono") || lowerName.includes("cascadia")) { + var rootName2 = fontName2.replace(/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i, "").replace(/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i, "").trim() + if (!seenMonoFamilies.has(rootName2) && rootName2 !== "") { + seenMonoFamilies.add(rootName2) + monoFamilies.push(rootName2) + } + } + } + cachedMonoFamilies = monoFonts.concat(monoFamilies.sort()) + fontsEnumerated = true + } + + Component.onCompleted: { + if (!fontsEnumerated) { + enumerateFonts() + } + } + + MouseArea { + anchors.fill: parent + visible: root.isVisible + onClicked: root.settingsRequested() + z: 50 + } + + Rectangle { + id: settingsMenu + visible: root.isVisible + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + width: 360 + height: settingsColumn.implicitHeight + Theme.spacingXL * 2 + radius: Theme.cornerRadius + color: Theme.popupBackground() + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + z: 100 + + Rectangle { + anchors.fill: parent + anchors.topMargin: 4 + anchors.leftMargin: 2 + anchors.rightMargin: -2 + anchors.bottomMargin: -4 + radius: parent.radius + color: Qt.rgba(0, 0, 0, 0.15) + z: parent.z - 1 + } + + Column { + id: settingsColumn + width: parent.width - Theme.spacingXL * 2 + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: Theme.spacingXL + spacing: Theme.spacingS + + Rectangle { + width: parent.width + height: 36 + color: "transparent" + + StyledText { + anchors.left: parent.left + anchors.leftMargin: -Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + text: "Notepad Font Settings" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + } + + Rectangle { + width: parent.width + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + } + + DankToggle { + anchors.left: parent.left + anchors.leftMargin: -Theme.spacingM + width: parent.width + Theme.spacingM + text: "Use Monospace Font" + description: "Toggle fonts" + checked: SettingsData.notepadUseMonospace + onToggled: checked => { + SettingsData.notepadUseMonospace = checked + } + } + + Rectangle { + width: parent.width + height: visible ? (fontDropdown.height + Theme.spacingS) : 0 + color: "transparent" + visible: !SettingsData.notepadUseMonospace + + DankDropdown { + id: fontDropdown + anchors.left: parent.left + anchors.leftMargin: -Theme.spacingM + width: parent.width + Theme.spacingM + text: "Font Family" + options: cachedFontFamilies + currentValue: { + if (!SettingsData.notepadFontFamily || SettingsData.notepadFontFamily === "") + return "Default (Global)" + else + return SettingsData.notepadFontFamily + } + enableFuzzySearch: true + onValueChanged: value => { + if (value && (value.startsWith("Default") || value === "Default (Global)")) { + SettingsData.notepadFontFamily = "" + } else { + SettingsData.notepadFontFamily = value + } + } + } + } + + Rectangle { + width: parent.width + height: fontSizeRow.height + Theme.spacingS + color: "transparent" + + Row { + id: fontSizeRow + width: parent.width + spacing: Theme.spacingS + + Column { + width: parent.width - fontSizeControls.width - Theme.spacingM + spacing: Theme.spacingXS + + StyledText { + text: "Font Size" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: SettingsData.notepadFontSize + "px" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: parent.width + } + } + + Row { + id: fontSizeControls + spacing: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + + DankActionButton { + buttonSize: 32 + iconName: "remove" + iconSize: Theme.iconSizeSmall + enabled: SettingsData.notepadFontSize > 8 + backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) + iconColor: Theme.surfaceText + onClicked: { + var newSize = Math.max(8, SettingsData.notepadFontSize - 1) + SettingsData.notepadFontSize = newSize + } + } + + Rectangle { + width: 60 + height: 32 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 1 + + StyledText { + anchors.centerIn: parent + text: SettingsData.notepadFontSize + "px" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + } + } + + DankActionButton { + buttonSize: 32 + iconName: "add" + iconSize: Theme.iconSizeSmall + enabled: SettingsData.notepadFontSize < 48 + backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) + iconColor: Theme.surfaceText + onClicked: { + var newSize = Math.min(48, SettingsData.notepadFontSize + 1) + SettingsData.notepadFontSize = newSize + } + } + } + } + } + + StyledText { + width: parent.width + text: SettingsData.notepadUseMonospace ? + "Using global monospace font from Settings → Personalization" : + "Global fonts can be configured in Settings → Personalization" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + wrapMode: Text.WordWrap + opacity: 0.8 + } + } + } +} \ No newline at end of file diff --git a/Modules/Notepad/NotepadTabs.qml b/Modules/Notepad/NotepadTabs.qml new file mode 100644 index 00000000..ea0f8f6e --- /dev/null +++ b/Modules/Notepad/NotepadTabs.qml @@ -0,0 +1,153 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Services +import qs.Widgets + +pragma ComponentBehavior: Bound + +Column { + id: root + + property var currentTab: NotepadStorageService.tabs.length > NotepadStorageService.currentTabIndex ? NotepadStorageService.tabs[NotepadStorageService.currentTabIndex] : null + property bool contentLoaded: false + + signal tabSwitched(int tabIndex) + signal tabClosed(int tabIndex) + signal newTabRequested() + + function hasUnsavedChangesForTab(tab) { + if (!tab) return false + + if (tab.id === currentTab?.id) { + return root.parent?.hasUnsavedChanges ? root.parent.hasUnsavedChanges() : false + } + return false + } + + spacing: Theme.spacingXS + + Row { + width: parent.width + height: 36 + spacing: Theme.spacingXS + + ScrollView { + width: parent.width - newTabButton.width - Theme.spacingXS + height: parent.height + clip: true + + ScrollBar.horizontal.visible: false + ScrollBar.vertical.visible: false + + Row { + spacing: Theme.spacingXS + + Repeater { + model: NotepadStorageService.tabs + + delegate: Rectangle { + required property int index + required property var modelData + + readonly property bool isActive: NotepadStorageService.currentTabIndex === index + readonly property bool isHovered: tabMouseArea.containsMouse && !closeMouseArea.containsMouse + readonly property real calculatedWidth: { + const textWidth = tabText.paintedWidth || 100 + const closeButtonWidth = NotepadStorageService.tabs.length > 1 ? 20 : 0 + const spacing = Theme.spacingXS + const padding = Theme.spacingM * 2 + return Math.max(120, Math.min(200, textWidth + closeButtonWidth + spacing + padding)) + } + + width: calculatedWidth + height: 32 + radius: Theme.cornerRadius + color: isActive ? Theme.primaryPressed : isHovered ? Theme.primaryHoverLight : "transparent" + border.width: isActive ? 0 : 1 + border.color: Theme.outlineMedium + + MouseArea { + id: tabMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton + + onClicked: root.tabSwitched(index) + } + + Row { + id: tabContent + anchors.centerIn: parent + spacing: Theme.spacingXS + + StyledText { + id: tabText + text: { + var prefix = "" + if (hasUnsavedChangesForTab(modelData)) { + prefix = "● " + } + return prefix + (modelData.title || "Untitled") + } + font.pixelSize: Theme.fontSizeSmall + color: isActive ? Theme.primary : Theme.surfaceText + font.weight: isActive ? Font.Medium : Font.Normal + elide: Text.ElideMiddle + maximumLineCount: 1 + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + id: tabCloseButton + width: 20 + height: 20 + radius: 10 + color: closeMouseArea.containsMouse ? Theme.surfaceTextHover : "transparent" + visible: NotepadStorageService.tabs.length > 1 + anchors.verticalCenter: parent.verticalCenter + + DankIcon { + name: "close" + size: 14 + color: Theme.surfaceTextMedium + anchors.centerIn: parent + } + + MouseArea { + id: closeMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + z: 100 + + onClicked: { + root.tabClosed(index) + } + } + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + } + } + + DankActionButton { + id: newTabButton + width: 32 + height: 32 + iconName: "add" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: root.newTabRequested() + } + } +} \ No newline at end of file diff --git a/Modules/Notepad/NotepadTextEditor.qml b/Modules/Notepad/NotepadTextEditor.qml new file mode 100644 index 00000000..52ab4661 --- /dev/null +++ b/Modules/Notepad/NotepadTextEditor.qml @@ -0,0 +1,291 @@ +import QtQuick +import QtQuick.Controls +import Quickshell.Io +import qs.Common +import qs.Services +import qs.Widgets + +pragma ComponentBehavior: Bound + +Column { + id: root + + property alias text: textArea.text + property alias textArea: textArea + property bool contentLoaded: false + property string lastSavedContent: "" + property var currentTab: NotepadStorageService.tabs.length > NotepadStorageService.currentTabIndex ? NotepadStorageService.tabs[NotepadStorageService.currentTabIndex] : null + + signal saveRequested() + signal openRequested() + signal newRequested() + signal escapePressed() + signal contentChanged() + signal settingsRequested() + + function hasUnsavedChanges() { + if (!currentTab || !contentLoaded) { + return false + } + + if (currentTab.isTemporary) { + return textArea.text.length > 0 + } + return textArea.text !== lastSavedContent + } + + function loadCurrentTabContent() { + if (!currentTab) return + + contentLoaded = false + NotepadStorageService.loadTabContent( + NotepadStorageService.currentTabIndex, + (content) => { + lastSavedContent = content + textArea.text = content + contentLoaded = true + } + ) + } + + function saveCurrentTabContent() { + if (!currentTab || !contentLoaded) return + + NotepadStorageService.saveTabContent( + NotepadStorageService.currentTabIndex, + textArea.text + ) + lastSavedContent = textArea.text + } + + function autoSaveToSession() { + if (!currentTab || !contentLoaded) return + saveCurrentTabContent() + } + + spacing: Theme.spacingM + + StyledRect { + width: parent.width + height: parent.height - bottomControls.height - Theme.spacingM + color: Theme.surface + border.color: Theme.outlineMedium + border.width: 1 + radius: Theme.cornerRadius + + ScrollView { + anchors.fill: parent + anchors.margins: 1 + clip: true + + TextArea { + id: textArea + placeholderText: qsTr("Start typing your notes here...") + font.family: SettingsData.notepadUseMonospace ? SettingsData.monoFontFamily : (SettingsData.notepadFontFamily || SettingsData.fontFamily) + font.pixelSize: SettingsData.notepadFontSize * SettingsData.fontScale + color: Theme.surfaceText + selectByMouse: true + selectByKeyboard: true + wrapMode: TextArea.Wrap + focus: true + activeFocusOnTab: true + textFormat: TextEdit.PlainText + inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase + persistentSelection: true + tabStopDistance: 40 + leftPadding: Theme.spacingM + topPadding: Theme.spacingM + rightPadding: Theme.spacingM + bottomPadding: Theme.spacingM + + Component.onCompleted: { + loadCurrentTabContent() + } + + Connections { + target: NotepadStorageService + function onCurrentTabIndexChanged() { + loadCurrentTabContent() + } + function onTabsChanged() { + if (NotepadStorageService.tabs.length > 0 && !contentLoaded) { + loadCurrentTabContent() + } + } + } + + onTextChanged: { + if (contentLoaded && text !== lastSavedContent) { + autoSaveTimer.restart() + } + root.contentChanged() + } + + Keys.onEscapePressed: (event) => { + root.escapePressed() + event.accepted = true + } + + Keys.onPressed: (event) => { + if (event.modifiers & Qt.ControlModifier) { + switch (event.key) { + case Qt.Key_S: + event.accepted = true + root.saveRequested() + break + case Qt.Key_O: + event.accepted = true + root.openRequested() + break + case Qt.Key_N: + event.accepted = true + root.newRequested() + break + case Qt.Key_A: + event.accepted = true + selectAll() + break + } + } + } + + background: Rectangle { + color: "transparent" + } + } + } + } + + Column { + id: bottomControls + width: parent.width + spacing: Theme.spacingS + + Item { + width: parent.width + height: 32 + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingL + + Row { + spacing: Theme.spacingS + DankActionButton { + iconName: "save" + iconSize: Theme.iconSize - 2 + iconColor: Theme.primary + enabled: currentTab && (hasUnsavedChanges() || textArea.text.length > 0) + onClicked: root.saveRequested() + } + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: qsTr("Save") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + } + } + + Row { + spacing: Theme.spacingS + DankActionButton { + iconName: "folder_open" + iconSize: Theme.iconSize - 2 + iconColor: Theme.secondary + onClicked: root.openRequested() + } + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: qsTr("Open") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + } + } + + Row { + spacing: Theme.spacingS + DankActionButton { + iconName: "note_add" + iconSize: Theme.iconSize - 2 + iconColor: Theme.surfaceText + onClicked: root.newRequested() + } + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: qsTr("New") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + } + } + } + + DankActionButton { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + iconName: "more_horiz" + iconSize: Theme.iconSize - 2 + iconColor: Theme.surfaceText + onClicked: root.settingsRequested() + } + } + + Row { + width: parent.width + spacing: Theme.spacingL + + StyledText { + text: textArea.text.length > 0 ? qsTr("%1 characters").arg(textArea.text.length) : qsTr("Empty") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + } + + StyledText { + text: qsTr("Lines: %1").arg(textArea.lineCount) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + visible: textArea.text.length > 0 + } + + StyledText { + text: { + if (autoSaveTimer.running) { + return qsTr("Auto-saving...") + } + + if (hasUnsavedChanges()) { + if (currentTab && currentTab.isTemporary) { + return qsTr("Unsaved note...") + } else { + return qsTr("Unsaved changes") + } + } else { + return qsTr("Saved") + } + } + font.pixelSize: Theme.fontSizeSmall + color: { + if (autoSaveTimer.running) { + return Theme.primary + } + + if (hasUnsavedChanges()) { + return Theme.warning + } else { + return Theme.success + } + } + opacity: textArea.text.length > 0 ? 1 : 0 + } + } + } + + Timer { + id: autoSaveTimer + interval: 2000 + repeat: false + onTriggered: { + autoSaveToSession() + } + } +} \ No newline at end of file diff --git a/Modules/NotepadSlideout.qml b/Modules/NotepadSlideout.qml deleted file mode 100644 index de381653..00000000 --- a/Modules/NotepadSlideout.qml +++ /dev/null @@ -1,1278 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtCore -import Quickshell -import Quickshell.Wayland -import Quickshell.Io -import qs.Common -import qs.Modals.Common -import qs.Modals.FileBrowser -import qs.Services -import qs.Widgets - -pragma ComponentBehavior: Bound - -PanelWindow { - id: root - - property bool isVisible: false - property bool fileDialogOpen: false - property string currentFileName: "" - property url currentFileUrl - property var targetScreen: null - property var modelData: null - property bool confirmationDialogOpen: false - property string pendingAction: "" - property url pendingFileUrl - property string lastSavedFileContent: "" - property bool expandedWidth: false - property var currentTab: NotepadStorageService.tabs.length > NotepadStorageService.currentTabIndex ? NotepadStorageService.tabs[NotepadStorageService.currentTabIndex] : null - property string currentContent: "" - property string lastSavedContent: "" - property bool contentLoaded: false - property bool showSettingsMenu: false - property var cachedFontFamilies: [] - property var cachedMonoFamilies: [] - property bool fontsEnumerated: false - - function hasUnsavedChanges() { - if (!currentTab || !contentLoaded) { - return false - } - - // For temporary files, show unsaved if there's any content that hasn't been hard-saved - if (currentTab.isTemporary) { - return textArea.text.length > 0 - } - return textArea.text !== lastSavedContent - } - - function hasUnsavedTemporaryContent() { - return hasUnsavedChanges() - } - - function hasUnsavedChangesForTab(tab) { - if (!tab) return false - - // Only the currently active tab can show real unsaved status - if (tab.id === currentTab?.id) { - return hasUnsavedChanges() - } - return false - } - - function enumerateFonts() { - var fonts = ["Default"] - var availableFonts = Qt.fontFamilies() - var rootFamilies = [] - var seenFamilies = new Set() - for (var i = 0; i < availableFonts.length; i++) { - var fontName = availableFonts[i] - if (fontName.startsWith(".")) - continue - - if (fontName === SettingsData.defaultFontFamily) - continue - - var rootName = fontName.replace(/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i, "").replace(/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i, - "").replace(/ (UI|Display|Text|Mono|Sans|Serif)$/i, function (match, suffix) { - return match - }).trim() - if (!seenFamilies.has(rootName) && rootName !== "") { - seenFamilies.add(rootName) - rootFamilies.push(rootName) - } - } - cachedFontFamilies = fonts.concat(rootFamilies.sort()) - var monoFonts = ["Default"] - var monoFamilies = [] - var seenMonoFamilies = new Set() - for (var j = 0; j < availableFonts.length; j++) { - var fontName2 = availableFonts[j] - if (fontName2.startsWith(".")) - continue - - if (fontName2 === SettingsData.defaultMonoFontFamily) - continue - - var lowerName = fontName2.toLowerCase() - if (lowerName.includes("mono") || lowerName.includes("code") || lowerName.includes("console") || lowerName.includes("terminal") || lowerName.includes("courier") || lowerName.includes("dejavu sans mono") || lowerName.includes( - "jetbrains") || lowerName.includes("fira") || lowerName.includes("hack") || lowerName.includes("source code") || lowerName.includes("ubuntu mono") || lowerName.includes("cascadia")) { - var rootName2 = fontName2.replace(/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i, "").replace(/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i, "").trim() - if (!seenMonoFamilies.has(rootName2) && rootName2 !== "") { - seenMonoFamilies.add(rootName2) - monoFamilies.push(rootName2) - } - } - } - cachedMonoFamilies = monoFonts.concat(monoFamilies.sort()) - fontsEnumerated = true - } - - function loadCurrentTabContent() { - if (!currentTab) return - - contentLoaded = false - NotepadStorageService.loadTabContent( - NotepadStorageService.currentTabIndex, - (content) => { - currentContent = content - lastSavedContent = content - textArea.text = content - contentLoaded = true - } - ) - } - - function saveCurrentTabContent() { - if (!currentTab || !contentLoaded) return - - NotepadStorageService.saveTabContent( - NotepadStorageService.currentTabIndex, - textArea.text - ) - currentContent = textArea.text - lastSavedContent = textArea.text - } - - function autoSaveToSession() { - if (!currentTab || !contentLoaded) return - - currentContent = textArea.text - saveCurrentTabContent() - } - - function createNewTab() { - performCreateNewTab() - } - - function performCreateNewTab() { - NotepadStorageService.createNewTab() - textArea.text = "" - currentContent = "" - lastSavedContent = "" - contentLoaded = true - textArea.forceActiveFocus() - } - - function closeTab(tabIndex) { - if (tabIndex === NotepadStorageService.currentTabIndex && hasUnsavedChanges()) { - root.pendingAction = "close_tab_" + tabIndex - root.confirmationDialogOpen = true - confirmationDialog.open() - } else { - performCloseTab(tabIndex) - } - } - - function performCloseTab(tabIndex) { - NotepadStorageService.closeTab(tabIndex) - Qt.callLater(() => { - loadCurrentTabContent() - }) - } - - function switchToTab(tabIndex) { - if (tabIndex < 0 || tabIndex >= NotepadStorageService.tabs.length) return - - if (contentLoaded) { - autoSaveToSession() - } - - NotepadStorageService.switchToTab(tabIndex) - Qt.callLater(() => { - loadCurrentTabContent() - if (currentTab) { - root.currentFileName = currentTab.fileName || "" - root.currentFileUrl = currentTab.fileUrl || "" - } - }) - } - - function show() { - visible = true - isVisible = true - textArea.forceActiveFocus() - } - - function hide() { - isVisible = false - } - - function toggle() { - if (isVisible) { - hide() - } else { - show() - } - } - - visible: isVisible - screen: modelData - - anchors.top: true - anchors.bottom: true - anchors.right: true - - implicitWidth: 960 - implicitHeight: modelData ? modelData.height : 800 - - color: "transparent" - - WlrLayershell.layer: WlrLayershell.Top - WlrLayershell.exclusiveZone: 0 - WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None - - Component.onCompleted: { - if (!fontsEnumerated) { - enumerateFonts() - } - } - - StyledRect { - id: contentRect - - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right - width: expandedWidth ? 960 : 480 - color: Theme.surfaceContainer - border.color: Theme.outlineMedium - border.width: 1 - opacity: isVisible ? SettingsData.popupTransparency : 0 - - Behavior on opacity { - NumberAnimation { - duration: 700 - easing.type: Easing.OutCubic - } - } - - transform: Translate { - id: slideTransform - x: isVisible ? 0 : contentRect.width - - Behavior on x { - NumberAnimation { - id: slideAnimation - duration: 450 - easing.type: Easing.OutCubic - - onRunningChanged: { - if (!running && !isVisible) { - root.visible = false - } - } - } - } - } - - Behavior on width { - NumberAnimation { - duration: 250 - easing.type: Easing.OutCubic - } - } - - Column { - anchors.fill: parent - anchors.margins: Theme.spacingL - spacing: Theme.spacingM - - // Header - Column { - width: parent.width - spacing: Theme.spacingXS - - // Title row - Row { - width: parent.width - height: 32 - - Column { - width: parent.width - buttonRow.width - spacing: Theme.spacingXS - anchors.verticalCenter: parent.verticalCenter - - StyledText { - text: qsTr("Notepad") - font.pixelSize: Theme.fontSizeLarge - color: Theme.surfaceText - font.weight: Font.Medium - } - } - - Row { - id: buttonRow - spacing: Theme.spacingXS - - DankActionButton { - id: expandButton - iconName: root.expandedWidth ? "unfold_less" : "unfold_more" - iconSize: Theme.iconSize - 4 - iconColor: Theme.surfaceText - onClicked: root.expandedWidth = !root.expandedWidth - - transform: Rotation { - angle: 90 - origin.x: expandButton.width / 2 - origin.y: expandButton.height / 2 - } - } - - DankActionButton { - id: closeButton - iconName: "close" - iconSize: Theme.iconSize - 4 - iconColor: Theme.surfaceText - onClicked: root.hide() - } - } - } - - // Tab bar - Row { - width: parent.width - height: 36 - spacing: Theme.spacingXS - - ScrollView { - width: parent.width - newTabButton.width - Theme.spacingXS - height: parent.height - clip: true - - ScrollBar.horizontal.visible: false - ScrollBar.vertical.visible: false - - Row { - spacing: Theme.spacingXS - - Repeater { - model: NotepadStorageService.tabs - - delegate: Rectangle { - required property int index - required property var modelData - - readonly property bool isActive: NotepadStorageService.currentTabIndex === index - readonly property bool isHovered: tabMouseArea.containsMouse && !closeMouseArea.containsMouse - readonly property real calculatedWidth: { - const textWidth = tabText.paintedWidth || 100 - const closeButtonWidth = NotepadStorageService.tabs.length > 1 ? 20 : 0 - const spacing = Theme.spacingXS - const padding = Theme.spacingM * 2 - return Math.max(120, Math.min(200, textWidth + closeButtonWidth + spacing + padding)) - } - - width: calculatedWidth - height: 32 - radius: Theme.cornerRadius - color: isActive ? Theme.primaryPressed : isHovered ? Theme.primaryHoverLight : "transparent" - border.width: isActive ? 0 : 1 - border.color: Theme.outlineMedium - - MouseArea { - id: tabMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton - - onClicked: switchToTab(index) - } - - Row { - id: tabContent - anchors.centerIn: parent - spacing: Theme.spacingXS - - StyledText { - id: tabText - text: { - var prefix = "" - if (hasUnsavedChangesForTab(modelData)) { - prefix = "● " - } - return prefix + (modelData.title || "Untitled") - } - font.pixelSize: Theme.fontSizeSmall - color: isActive ? Theme.primary : Theme.surfaceText - font.weight: isActive ? Font.Medium : Font.Normal - elide: Text.ElideMiddle - maximumLineCount: 1 - anchors.verticalCenter: parent.verticalCenter - } - - Rectangle { - id: tabCloseButton - width: 20 - height: 20 - radius: 10 - color: closeMouseArea.containsMouse ? Theme.surfaceTextHover : "transparent" - visible: NotepadStorageService.tabs.length > 1 - anchors.verticalCenter: parent.verticalCenter - - DankIcon { - name: "close" - size: 14 - color: Theme.surfaceTextMedium - anchors.centerIn: parent - } - - MouseArea { - id: closeMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - z: 100 - - onClicked: { - closeTab(index) - } - } - } - } - - Behavior on color { - ColorAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing - } - } - } - } - } - } - - DankActionButton { - id: newTabButton - width: 32 - height: 32 - iconName: "add" - iconSize: Theme.iconSize - 4 - iconColor: Theme.surfaceText - onClicked: createNewTab() - } - } - } - - // Text area - StyledRect { - width: parent.width - height: parent.height - 180 - color: Theme.surface - border.color: Theme.outlineMedium - border.width: 1 - radius: Theme.cornerRadius - - ScrollView { - anchors.fill: parent - anchors.margins: 1 - clip: true - - TextArea { - id: textArea - placeholderText: qsTr("Start typing your notes here...") - font.family: SettingsData.notepadUseMonospace ? SettingsData.monoFontFamily : (SettingsData.notepadFontFamily || SettingsData.fontFamily) - font.pixelSize: SettingsData.notepadFontSize * SettingsData.fontScale - color: Theme.surfaceText - selectByMouse: true - selectByKeyboard: true - wrapMode: TextArea.Wrap - focus: root.isVisible - activeFocusOnTab: true - textFormat: TextEdit.PlainText - inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase - persistentSelection: true - tabStopDistance: 40 - leftPadding: Theme.spacingM - topPadding: Theme.spacingM - rightPadding: Theme.spacingM - bottomPadding: Theme.spacingM - - Component.onCompleted: { - loadCurrentTabContent() - if (currentTab) { - root.currentFileName = currentTab.fileName || "" - root.currentFileUrl = currentTab.fileUrl || "" - } - } - - Connections { - target: NotepadStorageService - function onCurrentTabIndexChanged() { - loadCurrentTabContent() - if (currentTab) { - root.currentFileName = currentTab.fileName || "" - root.currentFileUrl = currentTab.fileUrl || "" - } - } - function onTabsChanged() { - if (NotepadStorageService.tabs.length > 0 && !contentLoaded) { - loadCurrentTabContent() - if (currentTab) { - root.currentFileName = currentTab.fileName || "" - root.currentFileUrl = currentTab.fileUrl || "" - } - } - } - } - - onTextChanged: { - if (contentLoaded && text !== lastSavedContent) { - autoSaveTimer.restart() - } - } - - Keys.onEscapePressed: (event) => { - root.hide() - event.accepted = true - } - - Keys.onPressed: (event) => { - if (event.modifiers & Qt.ControlModifier) { - switch (event.key) { - case Qt.Key_S: - event.accepted = true - if (currentTab && !currentTab.isTemporary && currentTab.filePath) { - // For non-temporary tabs, save directly to the original file - var fileUrl = "file://" + currentTab.filePath - saveToFile(fileUrl) - } else { - // For temporary tabs or new files, open save dialog - root.fileDialogOpen = true - saveBrowser.open() - } - break - case Qt.Key_O: - event.accepted = true - if (hasUnsavedChanges()) { - root.pendingAction = "open" - root.confirmationDialogOpen = true - confirmationDialog.open() - } else { - root.fileDialogOpen = true - loadBrowser.open() - } - break - case Qt.Key_N: - event.accepted = true - if (hasUnsavedChanges()) { - root.pendingAction = "new" - root.confirmationDialogOpen = true - confirmationDialog.open() - } else { - createNewTab() - } - break - case Qt.Key_A: - event.accepted = true - selectAll() - break - } - } - } - - background: Rectangle { - color: "transparent" - } - } - } - } - - // Bottom controls - Column { - width: parent.width - spacing: Theme.spacingS - - Item { - width: parent.width - height: 32 - - Row { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.spacingL - - Row { - spacing: Theme.spacingS - DankActionButton { - iconName: "save" - iconSize: Theme.iconSize - 2 - iconColor: Theme.primary - enabled: currentTab && (hasUnsavedChanges() || textArea.text.length > 0) - onClicked: { - root.fileDialogOpen = true - saveBrowser.open() - } - } - StyledText { - anchors.verticalCenter: parent.verticalCenter - text: qsTr("Save") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceTextMedium - } - } - - Row { - spacing: Theme.spacingS - DankActionButton { - iconName: "folder_open" - iconSize: Theme.iconSize - 2 - iconColor: Theme.secondary - onClicked: { - if (hasUnsavedChanges()) { - root.pendingAction = "open" - root.confirmationDialogOpen = true - confirmationDialog.open() - } else { - root.fileDialogOpen = true - loadBrowser.open() - } - } - } - StyledText { - anchors.verticalCenter: parent.verticalCenter - text: qsTr("Open") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceTextMedium - } - } - - Row { - spacing: Theme.spacingS - DankActionButton { - iconName: "note_add" - iconSize: Theme.iconSize - 2 - iconColor: Theme.surfaceText - onClicked: createNewTab() - } - StyledText { - anchors.verticalCenter: parent.verticalCenter - text: qsTr("New") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceTextMedium - } - } - } - - DankActionButton { - id: settingsButton - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - iconName: "more_horiz" - iconSize: Theme.iconSize - 2 - iconColor: Theme.surfaceText - onClicked: showSettingsMenu = !showSettingsMenu - } - } - - Row { - width: parent.width - spacing: Theme.spacingL - - StyledText { - text: textArea.text.length > 0 ? qsTr("%1 characters").arg(textArea.text.length) : qsTr("Empty") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceTextMedium - } - - StyledText { - text: qsTr("Lines: %1").arg(textArea.lineCount) - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceTextMedium - visible: textArea.text.length > 0 - } - - StyledText { - text: { - if (autoSaveTimer.running) { - return qsTr("Auto-saving...") - } - - if (hasUnsavedChanges()) { - if (currentTab && currentTab.isTemporary) { - return qsTr("Unsaved note...") - } else { - return qsTr("Unsaved changes") - } - } else { - return qsTr("Saved") - } - } - font.pixelSize: Theme.fontSizeSmall - color: { - if (autoSaveTimer.running) { - return Theme.primary - } - - if (hasUnsavedChanges()) { - return Theme.warning - } else { - return Theme.success - } - } - opacity: textArea.text.length > 0 ? 1 : 0 - } - } - } - } - - MouseArea { - anchors.fill: parent - visible: showSettingsMenu - onClicked: showSettingsMenu = false - z: 50 - } - - // Settings Menu Popup - Rectangle { - id: settingsMenu - visible: showSettingsMenu - anchors.horizontalCenter: parent.horizontalCenter - y: 423 - width: 360 - height: settingsColumn.implicitHeight + Theme.spacingXL * 2 - radius: Theme.cornerRadius - color: Theme.popupBackground() - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) - border.width: 1 - z: 100 - - Rectangle { - anchors.fill: parent - anchors.topMargin: 4 - anchors.leftMargin: 2 - anchors.rightMargin: -2 - anchors.bottomMargin: -4 - radius: parent.radius - color: Qt.rgba(0, 0, 0, 0.15) - z: parent.z - 1 - } - - Column { - id: settingsColumn - width: parent.width - Theme.spacingXL * 2 - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.topMargin: Theme.spacingXL - spacing: Theme.spacingS - - Rectangle { - width: parent.width - height: 36 - color: "transparent" - - StyledText { - anchors.left: parent.left - anchors.leftMargin: -Theme.spacingXS - anchors.verticalCenter: parent.verticalCenter - text: "Notepad Font Settings" - font.pixelSize: Theme.fontSizeMedium - font.weight: Font.Medium - color: Theme.surfaceText - } - } - - Rectangle { - width: parent.width - height: 1 - color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) - } - - DankToggle { - anchors.left: parent.left - anchors.leftMargin: -Theme.spacingM - width: parent.width + Theme.spacingM - text: "Use Monospace Font" - description: "Toggle fonts" - checked: SettingsData.notepadUseMonospace - onToggled: checked => { - SettingsData.notepadUseMonospace = checked - } - } - - Rectangle { - width: parent.width - height: visible ? (fontDropdown.height + Theme.spacingS) : 0 - color: "transparent" - visible: !SettingsData.notepadUseMonospace - - DankDropdown { - id: fontDropdown - anchors.left: parent.left - anchors.leftMargin: -Theme.spacingM - width: parent.width + Theme.spacingM - text: "Font Family" - options: cachedFontFamilies - currentValue: { - if (!SettingsData.notepadFontFamily || SettingsData.notepadFontFamily === "") - return "Default (Global)" - else - return SettingsData.notepadFontFamily - } - enableFuzzySearch: true - onValueChanged: value => { - if (value && (value.startsWith("Default") || value === "Default (Global)")) { - SettingsData.notepadFontFamily = "" - } else { - SettingsData.notepadFontFamily = value - } - } - } - } - - Rectangle { - width: parent.width - height: fontSizeRow.height + Theme.spacingS - color: "transparent" - - Row { - id: fontSizeRow - width: parent.width - spacing: Theme.spacingS - - Column { - width: parent.width - fontSizeControls.width - Theme.spacingM - spacing: Theme.spacingXS - - StyledText { - text: "Font Size" - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Medium - color: Theme.surfaceText - } - - StyledText { - text: SettingsData.notepadFontSize + "px" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - width: parent.width - } - } - - Row { - id: fontSizeControls - spacing: Theme.spacingS - anchors.verticalCenter: parent.verticalCenter - - DankActionButton { - buttonSize: 32 - iconName: "remove" - iconSize: Theme.iconSizeSmall - enabled: SettingsData.notepadFontSize > 8 - backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) - iconColor: Theme.surfaceText - onClicked: { - var newSize = Math.max(8, SettingsData.notepadFontSize - 1) - SettingsData.notepadFontSize = newSize - } - } - - Rectangle { - width: 60 - height: 32 - radius: Theme.cornerRadius - color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) - border.width: 1 - - StyledText { - anchors.centerIn: parent - text: SettingsData.notepadFontSize + "px" - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Medium - color: Theme.surfaceText - } - } - - DankActionButton { - buttonSize: 32 - iconName: "add" - iconSize: Theme.iconSizeSmall - enabled: SettingsData.notepadFontSize < 48 - backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) - iconColor: Theme.surfaceText - onClicked: { - var newSize = Math.min(48, SettingsData.notepadFontSize + 1) - SettingsData.notepadFontSize = newSize - } - } - } - } - } - - StyledText { - width: parent.width - text: SettingsData.notepadUseMonospace ? - "Using global monospace font from Settings → Personalization" : - "Global fonts can be configured in Settings → Personalization" - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceTextMedium - wrapMode: Text.WordWrap - opacity: 0.8 - } - } - } - } - - Timer { - id: autoSaveTimer - interval: 2000 - repeat: false - onTriggered: { - autoSaveToSession() - } - } - - - property string pendingSaveContent: "" - - function saveToFile(fileUrl) { - if (!currentTab) return - - var content = textArea.text - var filePath = fileUrl.toString().replace(/^file:\/\//, '') - - saveFileView.path = "" - pendingSaveContent = content - saveFileView.path = filePath - - Qt.callLater(() => { - saveFileView.setText(pendingSaveContent) - }) - } - - function loadFromFile(fileUrl) { - if (hasUnsavedTemporaryContent()) { - root.pendingFileUrl = fileUrl - root.pendingAction = "load_file" - root.confirmationDialogOpen = true - confirmationDialog.open() - } else { - performLoadFromFile(fileUrl) - } - } - - function performLoadFromFile(fileUrl) { - const filePath = fileUrl.toString().replace(/^file:\/\//, '') - const fileName = filePath.split('/').pop() - - loadFileView.path = "" - loadFileView.path = filePath - - // Wait for the file to be loaded before reading - if (loadFileView.waitForJob()) { - Qt.callLater(() => { - var content = loadFileView.text() - if (currentTab && content !== undefined && content !== null) { - textArea.text = content - currentContent = content - lastSavedContent = content - contentLoaded = true - root.lastSavedFileContent = content - - NotepadStorageService.updateTabMetadata(NotepadStorageService.currentTabIndex, { - title: fileName, - filePath: filePath, - isTemporary: false - }) - - root.currentFileName = fileName - root.currentFileUrl = fileUrl - saveCurrentTabContent() - } - }) - } - } - - FileView { - id: saveFileView - blockWrites: true - preload: false - atomicWrites: true - printErrors: true - - onSaved: { - if (currentTab && saveFileView.path && pendingSaveContent) { - NotepadStorageService.updateTabMetadata(NotepadStorageService.currentTabIndex, { - hasUnsavedChanges: false, - lastSavedContent: pendingSaveContent - }) - root.lastSavedFileContent = pendingSaveContent - pendingSaveContent = "" - } - } - - onSaveFailed: (error) => { - pendingSaveContent = "" - } - } - - FileView { - id: loadFileView - blockLoading: true - preload: true - atomicWrites: true - printErrors: true - - onLoadFailed: (error) => { - } - } - - FileBrowserModal { - id: saveBrowser - - browserTitle: qsTr("Save Notepad File") - browserIcon: "save" - browserType: "notepad_save" - fileExtensions: ["*.txt", "*.md", "*.*"] - allowStacking: true - saveMode: true - defaultFileName: { - if (currentTab && currentTab.title && currentTab.title !== "Untitled") { - return currentTab.title - } else if (currentTab && !currentTab.isTemporary && currentTab.filePath) { - return currentTab.filePath.split('/').pop() - } else { - return "note.txt" - } - } - - WlrLayershell.layer: WlrLayershell.Overlay - - onFileSelected: (path) => { - root.fileDialogOpen = false - const cleanPath = path.toString().replace(/^file:\/\//, '') - const fileName = cleanPath.split('/').pop() - const fileUrl = "file://" + cleanPath - - root.currentFileName = fileName - root.currentFileUrl = fileUrl - - if (currentTab) { - NotepadStorageService.saveTabAs( - NotepadStorageService.currentTabIndex, - cleanPath - ) - } - - saveToFile(fileUrl) - - if (root.pendingAction === "new") { - Qt.callLater(() => { - createNewTab() - }) - } else if (root.pendingAction === "open") { - Qt.callLater(() => { - root.fileDialogOpen = true - loadBrowser.open() - }) - } else if (root.pendingAction.startsWith("close_tab_")) { - Qt.callLater(() => { - var tabIndex = parseInt(root.pendingAction.split("_")[2]) - performCloseTab(tabIndex) - }) - } - root.pendingAction = "" - - close() - } - - onDialogClosed: { - root.fileDialogOpen = false - } - } - - FileBrowserModal { - id: loadBrowser - - browserTitle: qsTr("Open Notepad File") - browserIcon: "folder_open" - browserType: "notepad_load" - fileExtensions: ["*.txt", "*.md", "*.*"] - allowStacking: true - - WlrLayershell.layer: WlrLayershell.Overlay - - onFileSelected: (path) => { - root.fileDialogOpen = false - const cleanPath = path.toString().replace(/^file:\/\//, '') - const fileName = cleanPath.split('/').pop() - const fileUrl = "file://" + cleanPath - - root.currentFileName = fileName - root.currentFileUrl = fileUrl - - loadFromFile(fileUrl) - close() - } - - onDialogClosed: { - root.fileDialogOpen = false - } - } - - DankModal { - id: confirmationDialog - - width: 400 - height: 180 - shouldBeVisible: false - allowStacking: true - - onBackgroundClicked: { - close() - root.confirmationDialogOpen = false - } - - content: Component { - FocusScope { - anchors.fill: parent - focus: true - - Keys.onEscapePressed: event => { - confirmationDialog.close() - root.confirmationDialogOpen = false - event.accepted = true - } - - Column { - anchors.centerIn: parent - width: parent.width - Theme.spacingM * 2 - spacing: Theme.spacingM - - Row { - width: parent.width - - Column { - width: parent.width - 40 - spacing: Theme.spacingXS - - StyledText { - text: qsTr("Unsaved Changes") - font.pixelSize: Theme.fontSizeLarge - color: Theme.surfaceText - font.weight: Font.Medium - } - - StyledText { - text: root.pendingAction === "new" ? - qsTr("You have unsaved changes. Save before creating a new file?") : - root.pendingAction.startsWith("close_tab_") ? - qsTr("You have unsaved changes. Save before closing this tab?") : - root.pendingAction === "load_file" || root.pendingAction === "open" ? - qsTr("You have unsaved changes. Save before opening a file?") : - qsTr("You have unsaved changes. Save before continuing?") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceTextMedium - width: parent.width - wrapMode: Text.Wrap - } - } - - DankActionButton { - iconName: "close" - iconSize: Theme.iconSize - 4 - iconColor: Theme.surfaceText - onClicked: { - confirmationDialog.close() - root.confirmationDialogOpen = false - } - } - } - - Item { - width: parent.width - height: 40 - - Row { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.spacingM - - Rectangle { - width: Math.max(80, discardText.contentWidth + Theme.spacingM * 2) - height: 36 - radius: Theme.cornerRadius - color: discardArea.containsMouse ? Theme.surfaceTextHover : "transparent" - border.color: Theme.surfaceVariantAlpha - border.width: 1 - - StyledText { - id: discardText - anchors.centerIn: parent - text: qsTr("Don't Save") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - font.weight: Font.Medium - } - - MouseArea { - id: discardArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - confirmationDialog.close() - root.confirmationDialogOpen = false - if (root.pendingAction === "new") { - createNewTab() - } else if (root.pendingAction === "open") { - root.fileDialogOpen = true - loadBrowser.open() - } else if (root.pendingAction === "load_file") { - performLoadFromFile(root.pendingFileUrl) - } else if (root.pendingAction.startsWith("close_tab_")) { - var tabIndex = parseInt(root.pendingAction.split("_")[2]) - performCloseTab(tabIndex) - } - root.pendingAction = "" - root.pendingFileUrl = "" - } - } - } - - Rectangle { - width: Math.max(70, saveAsText.contentWidth + Theme.spacingM * 2) - height: 36 - radius: Theme.cornerRadius - color: saveAsArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary - - StyledText { - id: saveAsText - anchors.centerIn: parent - text: qsTr("Save") - font.pixelSize: Theme.fontSizeMedium - color: Theme.background - font.weight: Font.Medium - } - - MouseArea { - id: saveAsArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - confirmationDialog.close() - root.confirmationDialogOpen = false - root.fileDialogOpen = true - saveBrowser.open() - } - } - - 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 5a89cc10..78e15251 100644 --- a/Modules/TopBar/TopBar.qml +++ b/Modules/TopBar/TopBar.qml @@ -33,11 +33,11 @@ PanelWindow { function getNotepadInstanceForScreen() { if (!notepadVariants || !notepadVariants.instances) return null - + for (var i = 0; i < notepadVariants.instances.length; i++) { - var loader = notepadVariants.instances[i] - if (loader.modelData && loader.modelData.name === root.screen?.name) { - return loader.ensureLoaded() + var slideout = notepadVariants.instances[i] + if (slideout.modelData && slideout.modelData.name === root.screen?.name) { + return slideout } } return null @@ -157,7 +157,7 @@ PanelWindow { } property var notepadInstance: null - property bool notepadInstanceVisible: notepadInstance?.notepadVisible ?? false + property bool notepadInstanceVisible: notepadInstance?.isVisible ?? false readonly property bool hasActivePopout: { const loaders = [{ @@ -1065,7 +1065,7 @@ PanelWindow { NotepadButton { property var notepadInstance: topBarCore.notepadInstance - isActive: notepadInstance?.notepadVisible ?? false + isActive: notepadInstance?.isVisible ?? false widgetHeight: root.widgetHeight barHeight: root.effectiveBarHeight section: topBarContent.getWidgetSection(parent) || "right" diff --git a/Widgets/DankSlideout.qml b/Widgets/DankSlideout.qml new file mode 100644 index 00000000..191a3920 --- /dev/null +++ b/Widgets/DankSlideout.qml @@ -0,0 +1,175 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import qs.Common +import qs.Widgets + +pragma ComponentBehavior: Bound + +PanelWindow { + id: root + + property bool isVisible: false + property var targetScreen: null + property var modelData: null + property real slideoutWidth: 480 + property bool expandable: false + property bool expandedWidth: false + property real expandedWidthValue: 960 + property Component content: null + property string title: "" + property alias container: contentContainer + + function show() { + visible = true + isVisible = true + } + + function hide() { + isVisible = false + } + + function toggle() { + if (isVisible) { + hide() + } else { + show() + } + } + + visible: isVisible + screen: modelData + + anchors.top: true + anchors.bottom: true + anchors.right: true + + implicitWidth: expandable ? expandedWidthValue : slideoutWidth + implicitHeight: modelData ? modelData.height : 800 + + color: "transparent" + + WlrLayershell.layer: WlrLayershell.Top + WlrLayershell.exclusiveZone: 0 + WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None + + StyledRect { + id: contentRect + + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + width: expandable && expandedWidth ? expandedWidthValue : slideoutWidth + color: Theme.surfaceContainer + border.color: Theme.outlineMedium + border.width: 1 + opacity: isVisible ? SettingsData.popupTransparency : 0 + + Behavior on opacity { + NumberAnimation { + duration: 700 + easing.type: Easing.OutCubic + } + } + + transform: Translate { + id: slideTransform + x: isVisible ? 0 : contentRect.width + + Behavior on x { + NumberAnimation { + id: slideAnimation + duration: 450 + easing.type: Easing.OutCubic + + onRunningChanged: { + if (!running && !isVisible) { + root.visible = false + } + } + } + } + } + + Behavior on width { + NumberAnimation { + duration: 250 + easing.type: Easing.OutCubic + } + } + + Column { + id: headerColumn + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + visible: root.title !== "" + + Row { + width: parent.width + height: 32 + + Column { + width: parent.width - buttonRow.width + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: root.title + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + } + } + + Row { + id: buttonRow + spacing: Theme.spacingXS + + DankActionButton { + id: expandButton + iconName: root.expandedWidth ? "unfold_less" : "unfold_more" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + visible: root.expandable + onClicked: root.expandedWidth = !root.expandedWidth + + transform: Rotation { + angle: 90 + origin.x: expandButton.width / 2 + origin.y: expandButton.height / 2 + } + } + + DankActionButton { + id: closeButton + iconName: "close" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: root.hide() + } + } + } + } + + Item { + id: contentContainer + anchors.top: root.title !== "" ? headerColumn.bottom : parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.topMargin: root.title !== "" ? 0 : Theme.spacingL + anchors.leftMargin: Theme.spacingL + anchors.rightMargin: Theme.spacingL + anchors.bottomMargin: Theme.spacingL + + Loader { + anchors.fill: parent + sourceComponent: root.content + } + } + } +} \ No newline at end of file diff --git a/shell.qml b/shell.qml index 8285a23e..a5837381 100644 --- a/shell.qml +++ b/shell.qml @@ -18,6 +18,8 @@ import qs.Modules.ControlCenter import qs.Modules.Dock import qs.Modules.Lock import qs.Modules.Notifications.Center +import qs.Widgets +import "./Modules/Notepad" import qs.Modules.Notifications.Popup import qs.Modules.OSD import qs.Modules.ProcessList @@ -287,29 +289,28 @@ ShellRoot { id: notepadSlideoutVariants model: SettingsData.getFilteredScreens("notepad") - delegate: Loader { - id: notepadLoader - property var modelData: item - active: false - - sourceComponent: Component { - NotepadSlideout { - id: notepadSlideout - modelData: notepadLoader.modelData - - Component.onCompleted: { - notepadLoader.loaded = true + delegate: DankSlideout { + id: notepadSlideout + modelData: item + title: qsTr("Notepad") + slideoutWidth: 480 + expandable: true + expandedWidthValue: 960 + + content: Component { + Notepad { + onHideRequested: { + notepadSlideout.hide() } } } - - property bool loaded: false - - function ensureLoaded() { - if (!active) { - active = true + + function toggle() { + if (isVisible) { + hide() + } else { + show() } - return item } } } @@ -474,30 +475,29 @@ ShellRoot { if (notepadSlideoutVariants.instances.length === 0) { return null } - + if (notepadSlideoutVariants.instances.length === 1) { - return notepadSlideoutVariants.instances[0].ensureLoaded() + return notepadSlideoutVariants.instances[0] } - + var focusedScreen = getFocusedScreenName() if (focusedScreen && notepadSlideoutVariants.instances.length > 0) { for (var i = 0; i < notepadSlideoutVariants.instances.length; i++) { - var loader = notepadSlideoutVariants.instances[i] - if (loader.modelData && loader.modelData.name === focusedScreen) { - loader.ensureLoaded() - return loader.item + var slideout = notepadSlideoutVariants.instances[i] + if (slideout.modelData && slideout.modelData.name === focusedScreen) { + return slideout } } } - + for (var i = 0; i < notepadSlideoutVariants.instances.length; i++) { - var loader = notepadSlideoutVariants.instances[i] - if (loader.active && loader.item && loader.item.notepadVisible) { - return loader.item + var slideout = notepadSlideoutVariants.instances[i] + if (slideout.isVisible) { + return slideout } } - - return notepadSlideoutVariants.instances[0].ensureLoaded() + + return notepadSlideoutVariants.instances[0] } function open(): string {