diff options
Diffstat (limited to 'raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Notepad')
4 files changed, 2107 insertions, 0 deletions
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Notepad/Notepad.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Notepad/Notepad.qml new file mode 100644 index 0000000..e294695 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Notepad/Notepad.qml @@ -0,0 +1,531 @@ +pragma ComponentBehavior: Bound +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Common +import qs.Modals.Common +import qs.Modals.FileBrowser +import qs.Services +import qs.Widgets + +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: "" + property var slideout: null + + signal hideRequested + signal previewRequested(string content) + + Ref { + service: NotepadStorageService + } + + Connections { + target: slideout + enabled: slideout !== null + function onAboutToHide() { + textEditor.autoSaveToSession() + } + } + + 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; + confirmationDialogLoader.active = true; + if (confirmationDialogLoader.item) + confirmationDialogLoader.item.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; + confirmationDialogLoader.active = true; + if (confirmationDialogLoader.item) + confirmationDialogLoader.item.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; + saveBrowserLoader.active = true; + if (saveBrowserLoader.item) + saveBrowserLoader.item.open(); + } + } + + onOpenRequested: { + textEditor.autoSaveToSession(); + if (textEditor.text.length > 0) { + createNewTab(); + } + + root.fileDialogOpen = true; + loadBrowserLoader.active = true; + if (loadBrowserLoader.item) + loadBrowserLoader.item.open(); + } + + onNewRequested: { + textEditor.autoSaveToSession(); + createNewTab(); + } + + onPreviewRequested: { + textEditor.togglePreview(); + } + + onEscapePressed: { + textEditor.autoSaveToSession() + root.hideRequested() + } + + onSettingsRequested: { + showSettingsMenu = !showSettingsMenu; + } + } + } + + NotepadSettings { + id: notepadSettings + anchors.fill: parent + isVisible: showSettingsMenu + onSettingsRequested: showSettingsMenu = !showSettingsMenu + onFindRequested: { + showSettingsMenu = false; + textEditor.showSearch(); + } + } + + 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 => {} + } + + LazyLoader { + id: saveBrowserLoader + active: false + + FileBrowserSurfaceModal { + id: saveBrowser + + browserTitle: I18n.tr("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"; + } + } + + onFileSelected: path => { + root.fileDialogOpen = false; + const cleanPath = decodeURI(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; + loadBrowserLoader.active = true; + if (loadBrowserLoader.item) + loadBrowserLoader.item.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; + } + } + } + + LazyLoader { + id: loadBrowserLoader + active: false + + FileBrowserSurfaceModal { + id: loadBrowser + + browserTitle: I18n.tr("Open Notepad File") + browserIcon: "folder_open" + browserType: "notepad_load" + fileExtensions: ["*.txt", "*.md", "*.*"] + allowStacking: true + + 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; + } + } + } + + LazyLoader { + id: confirmationDialogLoader + active: false + + DankModal { + id: confirmationDialog + + modalWidth: 400 + modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 180 + shouldBeVisible: false + allowStacking: true + + onBackgroundClicked: { + close(); + root.confirmationDialogOpen = false; + } + + content: Component { + FocusScope { + anchors.fill: parent + focus: true + implicitHeight: contentColumn.implicitHeight + + Keys.onEscapePressed: event => { + confirmationDialog.close(); + root.confirmationDialogOpen = false; + event.accepted = true; + } + + Column { + id: contentColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Theme.spacingM + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Unsaved Changes") + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + } + + StyledText { + text: root.pendingAction === "new" ? I18n.tr("You have unsaved changes. Save before creating a new file?") : root.pendingAction.startsWith("close_tab_") ? I18n.tr("You have unsaved changes. Save before closing this tab?") : root.pendingAction === "load_file" || root.pendingAction === "open" ? I18n.tr("You have unsaved changes. Save before opening a file?") : I18n.tr("You have unsaved changes. Save before continuing?") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceTextMedium + width: parent.width + wrapMode: Text.Wrap + } + + Item { + width: parent.width + height: 36 + + 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: I18n.tr("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; + loadBrowserLoader.active = true; + if (loadBrowserLoader.item) + loadBrowserLoader.item.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: I18n.tr("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; + saveBrowserLoader.active = true; + if (saveBrowserLoader.item) + saveBrowserLoader.item.open(); + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + } + } + + DankActionButton { + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + iconName: "close" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: { + confirmationDialog.close(); + root.confirmationDialogOpen = false; + } + } + } + } + } + } +} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Notepad/NotepadSettings.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Notepad/NotepadSettings.qml new file mode 100644 index 0000000..9265ddf --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Notepad/NotepadSettings.qml @@ -0,0 +1,367 @@ +pragma ComponentBehavior: Bound +import QtQuick +import qs.Common +import qs.Widgets + +Item { + id: root + + property bool isVisible: false + property var cachedFontFamilies: [] + property var cachedMonoFamilies: [] + property bool fontsEnumerated: false + + signal settingsRequested + signal findRequested + + 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 === Theme.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: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, Theme.notepadTransparency) + 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: I18n.tr("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: I18n.tr("Use Monospace Font") + description: "Toggle fonts" + checked: SettingsData.notepadUseMonospace + onToggled: checked => { + SettingsData.notepadUseMonospace = checked; + } + } + + DankToggle { + anchors.left: parent.left + anchors.leftMargin: -Theme.spacingM + width: parent.width + Theme.spacingM + text: I18n.tr("Show Line Numbers") + description: "Display line numbers in editor" + checked: SettingsData.notepadShowLineNumbers + onToggled: checked => { + SettingsData.notepadShowLineNumbers = checked; + } + } + + StyledRect { + width: parent.width + height: 60 + radius: Theme.cornerRadius + color: "transparent" + + StateLayer { + anchors.fill: parent + anchors.leftMargin: -Theme.spacingM + width: parent.width + Theme.spacingM + stateColor: Theme.primary + cornerRadius: parent.radius + onClicked: root.findRequested() + } + + Row { + anchors.left: parent.left + anchors.leftMargin: -Theme.spacingM + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + DankIcon { + name: "search" + size: Theme.iconSize - 2 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingXS + + StyledText { + text: I18n.tr("Find in Text") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + text: I18n.tr("Open search bar to find text") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + } + } + + 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: I18n.tr("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: I18n.tr("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; + } + } + } + } + } + + Rectangle { + width: parent.width + height: transparencySliderColumn.height + Theme.spacingS + color: "transparent" + + Column { + id: transparencySliderColumn + width: parent.width + spacing: Theme.spacingS + + DankToggle { + anchors.left: parent.left + anchors.leftMargin: -Theme.spacingM + width: parent.width + Theme.spacingM + text: I18n.tr("Custom Transparency") + description: "Override global transparency for Notepad" + checked: SettingsData.notepadTransparencyOverride >= 0 + onToggled: checked => { + if (checked) { + SettingsData.notepadTransparencyOverride = SettingsData.notepadLastCustomTransparency; + } else { + SettingsData.notepadTransparencyOverride = -1; + } + } + } + + DankSlider { + anchors.left: parent.left + anchors.leftMargin: -Theme.spacingM + width: parent.width + Theme.spacingM + height: 24 + visible: SettingsData.notepadTransparencyOverride >= 0 + value: Math.round((SettingsData.notepadTransparencyOverride >= 0 ? SettingsData.notepadTransparencyOverride : SettingsData.popupTransparency) * 100) + minimum: 0 + maximum: 100 + unit: "" + showValue: true + wheelEnabled: false + onSliderValueChanged: newValue => { + if (SettingsData.notepadTransparencyOverride >= 0) { + SettingsData.notepadTransparencyOverride = newValue / 100; + } + } + } + } + } + + 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 + } + } + } +} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Notepad/NotepadTabs.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Notepad/NotepadTabs.qml new file mode 100644 index 0000000..05a21d1 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Notepad/NotepadTabs.qml @@ -0,0 +1,285 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Services +import qs.Widgets + +Column { + id: root + + property var currentTab: NotepadStorageService.tabs.length > NotepadStorageService.currentTabIndex ? NotepadStorageService.tabs[NotepadStorageService.currentTabIndex] : null + property bool contentLoaded: false + property int draggedIndex: -1 + property int dropTargetIndex: -1 + property bool suppressShiftAnimation: false + readonly property real tabItemSize: 128 + Theme.spacingXS + + 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: Item { + id: delegateItem + 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 tabWidth: 128 + property bool longPressing: false + property bool dragging: false + property point dragStartPos: Qt.point(0, 0) + property int targetIndex: -1 + property int originalIndex: -1 + property real dragAxisOffset: 0 + + Timer { + id: longPressTimer + interval: 200 + repeat: false + onTriggered: { + if (NotepadStorageService.tabs.length > 1) { + delegateItem.longPressing = true + } + } + } + + readonly property real shiftOffset: { + if (root.draggedIndex < 0) + return 0 + if (index === root.draggedIndex) + return 0 + var dragIdx = root.draggedIndex + var dropIdx = root.dropTargetIndex + var myIdx = index + var shiftAmount = root.tabItemSize + if (dropIdx < 0) + return 0 + if (dragIdx < dropIdx && myIdx > dragIdx && myIdx <= dropIdx) + return -shiftAmount + if (dragIdx > dropIdx && myIdx >= dropIdx && myIdx < dragIdx) + return shiftAmount + return 0 + } + + width: tabWidth + height: 32 + z: dragging ? 100 : 0 + + transform: Translate { + x: shiftOffset + Behavior on x { + enabled: !root.suppressShiftAnimation + NumberAnimation { + duration: 150 + easing.type: Easing.OutCubic + } + } + } + + Item { + id: tabVisual + anchors.fill: parent + z: 1 + layer.enabled: dragging + layer.smooth: true + + transform: Translate { + x: dragging ? dragAxisOffset : 0 + } + + Rectangle { + id: tabRect + anchors.fill: parent + radius: Theme.cornerRadius + color: isActive ? Theme.primaryPressed : isHovered ? Theme.primaryHoverLight : Theme.withAlpha(Theme.primaryPressed, 0) + border.width: isActive || dragging ? 0 : 1 + border.color: dragging ? Theme.primary : Theme.outlineMedium + clip: true + + Row { + id: tabContent + anchors.fill: parent + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + spacing: Theme.spacingXS + + StyledText { + id: tabText + width: parent.width - (tabCloseButton.visible ? tabCloseButton.width + Theme.spacingXS : 0) + 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 + wrapMode: Text.NoWrap + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + id: tabCloseButton + width: 20 + height: 20 + radius: Theme.cornerRadius + color: closeMouseArea.containsMouse ? Theme.surfaceTextHover : Theme.withAlpha(Theme.surfaceTextHover, 0) + 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 + } + } + } + } + + MouseArea { + id: tabMouseArea + anchors.fill: parent + hoverEnabled: true + preventStealing: dragging || longPressing + cursorShape: dragging || longPressing ? Qt.ClosedHandCursor : Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton + + onPressed: mouse => { + if (mouse.button === Qt.LeftButton && NotepadStorageService.tabs.length > 1) { + delegateItem.dragStartPos = Qt.point(mouse.x, mouse.y) + longPressTimer.start() + } + } + + onReleased: mouse => { + longPressTimer.stop() + var wasDragging = delegateItem.dragging + var didReorder = wasDragging && delegateItem.targetIndex >= 0 && delegateItem.targetIndex !== delegateItem.originalIndex + + if (didReorder) { + root.suppressShiftAnimation = true + NotepadStorageService.reorderTab(delegateItem.originalIndex, delegateItem.targetIndex) + } + + delegateItem.longPressing = false + delegateItem.dragging = false + delegateItem.dragAxisOffset = 0 + delegateItem.targetIndex = -1 + delegateItem.originalIndex = -1 + root.draggedIndex = -1 + root.dropTargetIndex = -1 + if (didReorder) { + Qt.callLater(() => { + root.suppressShiftAnimation = false + }) + } + + if (wasDragging || mouse.button !== Qt.LeftButton) + return + root.tabSwitched(index) + } + + onPositionChanged: mouse => { + if (delegateItem.longPressing && !delegateItem.dragging) { + var distance = Math.sqrt(Math.pow(mouse.x - delegateItem.dragStartPos.x, 2) + Math.pow(mouse.y - delegateItem.dragStartPos.y, 2)) + if (distance > 5) { + delegateItem.dragging = true + delegateItem.targetIndex = index + delegateItem.originalIndex = index + root.draggedIndex = index + root.dropTargetIndex = index + } + } + + if (!delegateItem.dragging) + return + + var axisOffset = mouse.x - delegateItem.dragStartPos.x + delegateItem.dragAxisOffset = axisOffset + + var itemSize = root.tabItemSize + var rawSlot = axisOffset / itemSize + var slotOffset = rawSlot >= 0 + ? Math.floor(rawSlot + 0.4) + : Math.ceil(rawSlot - 0.4) + var tabCount = NotepadStorageService.tabs.length + var newTargetIndex = Math.max(0, Math.min(tabCount - 1, delegateItem.originalIndex + slotOffset)) + + if (newTargetIndex !== delegateItem.targetIndex) { + delegateItem.targetIndex = newTargetIndex + root.dropTargetIndex = newTargetIndex + } + } + } + } + } + } + } + + DankActionButton { + id: newTabButton + width: 32 + height: 32 + iconName: "add" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + onClicked: root.newTabRequested() + } + } +} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Notepad/NotepadTextEditor.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Notepad/NotepadTextEditor.qml new file mode 100644 index 0000000..7ea0b27 --- /dev/null +++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Notepad/NotepadTextEditor.qml @@ -0,0 +1,924 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell.Io +import qs.Common +import qs.Services +import qs.Widgets + +pragma ComponentBehavior: Bound + +Column { + id: root + + Component.onCompleted: { + if (PluginService.isPluginLoaded("dankNotepadModule")) { + pluginHighlightedHtml = SettingsData.getBuiltInPluginSetting("dankNotepadModule", "highlightedHtml", "") + } + } + + 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 + property bool searchVisible: false + property string searchQuery: "" + property var searchMatches: [] + property int currentMatchIndex: -1 + property int matchCount: 0 + property bool inlinePreviewVisible: false + property string previewMode: "split" // split | full + property string pluginHighlightedHtml: "" + property string lastPluginContent: "" + property int loadRequestId: 0 + + signal saveRequested() + signal openRequested() + signal newRequested() + signal previewRequested() + 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 + + const requestedTabId = currentTab.id + const requestId = ++loadRequestId + contentLoaded = false + NotepadStorageService.loadTabContent( + NotepadStorageService.currentTabIndex, + (content) => { + const activeTab = NotepadStorageService.tabs.length > NotepadStorageService.currentTabIndex + ? NotepadStorageService.tabs[NotepadStorageService.currentTabIndex] + : null + if (requestId !== loadRequestId || !activeTab || activeTab.id !== requestedTabId) + return + + lastSavedContent = content + textArea.text = content + contentLoaded = true + syncContentToPlugin() + } + ) + } + + function saveCurrentTabContent() { + if (!currentTab || !contentLoaded) return + + NotepadStorageService.saveTabContent( + NotepadStorageService.currentTabIndex, + textArea.text + ) + lastSavedContent = textArea.text + } + + function autoSaveToSession() { + if (!currentTab || !contentLoaded) return + saveCurrentTabContent() + } + + function setTextDocumentLineHeight() { + return + } + + property string lastTextForLineModel: "" + property var lineModel: [] + + function updateLineModel() { + if (!SettingsData.notepadShowLineNumbers) { + lineModel = [] + lastTextForLineModel = "" + return + } + + if (textArea.text !== lastTextForLineModel || lineModel.length === 0) { + lastTextForLineModel = textArea.text + lineModel = textArea.text.split('\n') + } + } + + function performSearch() { + let matches = [] + currentMatchIndex = -1 + + if (!searchQuery || searchQuery.length === 0) { + searchMatches = [] + matchCount = 0 + textArea.select(0, 0) + return + } + + const text = textArea.text + const query = searchQuery.toLowerCase() + let index = 0 + + while (index < text.length) { + const foundIndex = text.toLowerCase().indexOf(query, index) + if (foundIndex === -1) break + + matches.push({ + start: foundIndex, + end: foundIndex + searchQuery.length + }) + index = foundIndex + 1 + } + + searchMatches = matches + matchCount = matches.length + + if (matchCount > 0) { + currentMatchIndex = 0 + highlightCurrentMatch() + } else { + textArea.select(0, 0) + } + } + + function highlightCurrentMatch() { + if (currentMatchIndex >= 0 && currentMatchIndex < searchMatches.length) { + const match = searchMatches[currentMatchIndex] + + textArea.cursorPosition = match.start + textArea.moveCursorSelection(match.end, TextEdit.SelectCharacters) + + const flickable = textArea.parent + if (flickable && flickable.contentY !== undefined) { + const lineHeight = textArea.font.pixelSize * 1.5 + const approxLine = textArea.text.substring(0, match.start).split('\n').length + const targetY = approxLine * lineHeight - flickable.height / 2 + flickable.contentY = Math.max(0, Math.min(targetY, flickable.contentHeight - flickable.height)) + } + } + } + + function findNext() { + if (matchCount === 0 || searchMatches.length === 0) return + + currentMatchIndex = (currentMatchIndex + 1) % matchCount + highlightCurrentMatch() + } + + function findPrevious() { + if (matchCount === 0 || searchMatches.length === 0) return + + currentMatchIndex = currentMatchIndex <= 0 ? matchCount - 1 : currentMatchIndex - 1 + highlightCurrentMatch() + } + + function showSearch() { + searchVisible = true + Qt.callLater(() => { + searchField.forceActiveFocus() + }) + } + + function togglePreview() { + if (!inlinePreviewVisible) { + inlinePreviewVisible = true + previewMode = "split" + } else if (previewMode === "split") { + previewMode = "full" + } else { + inlinePreviewVisible = false + previewMode = "split" + } + syncContentToPlugin() + } + + function renderPreviewHtml() { + if (!inlinePreviewVisible) return "" + return pluginHighlightedHtml.length > 0 ? pluginHighlightedHtml : "<p><i>Rendering preview…</i></p>" + } + + function syncContentToPlugin() { + if (!PluginService.isPluginLoaded("dankNotepadModule")) + return + + if (!currentTab) + return + + const filePath = currentTab?.filePath || "" + const ext = filePath.split('.').pop().toLowerCase() + const content = textArea.text + + if (content === lastPluginContent && SettingsData.getBuiltInPluginSetting("dankNotepadModule", "previewActive", false) === inlinePreviewVisible) { + return + } + + lastPluginContent = content + SettingsData.setBuiltInPluginSetting("dankNotepadModule", "previewActive", inlinePreviewVisible) + SettingsData.setBuiltInPluginSetting("dankNotepadModule", "currentFilePath", filePath) + SettingsData.setBuiltInPluginSetting("dankNotepadModule", "currentFileExtension", ext) + SettingsData.setBuiltInPluginSetting("dankNotepadModule", "sourceContent", content) + SettingsData.setBuiltInPluginSetting("dankNotepadModule", "updatedAt", Date.now()) + } + + function hideSearch() { + searchVisible = false + searchQuery = "" + searchMatches = [] + matchCount = 0 + currentMatchIndex = -1 + textArea.select(0, 0) + textArea.forceActiveFocus() + } + + function copyPlainTextToClipboard() { + if (!inlinePreviewVisible || !textArea.text) return + + const content = textArea.text + if (content.length > 0) { + const proc = Qt.createQmlObject(` + import QtQuick + import Quickshell.Io + Process { + property string content: "" + command: ["sh", "-c", "printf '%s' \\"$CONTENT\\" | dms clipboard copy"] + environment: { "CONTENT": content } + running: false + }`, + root, + "copyProc" + ) + proc.content = content + proc.running = true + proc.exited.connect(() => { + ToastService.showInfo(I18n.tr("Copied to clipboard")) + proc.destroy() + }) + } + } + + function copyHtmlToClipboard() { + if (!inlinePreviewVisible || !pluginHighlightedHtml) return + + if (pluginHighlightedHtml.length > 0) { + const proc = Qt.createQmlObject(` + import QtQuick + import Quickshell.Io + Process { + property string content: "" + command: ["sh", "-c", "printf '%s' \\"$CONTENT\\" | dms clipboard copy"] + environment: { "CONTENT": content } + running: false + }`, + root, + "copyProcHtml" + ) + proc.content = pluginHighlightedHtml + proc.running = true + proc.exited.connect(() => { + ToastService.showInfo(I18n.tr("HTML copied to clipboard")) + proc.destroy() + }) + } + } + + spacing: Theme.spacingM + + StyledRect { + id: searchBar + width: parent.width + height: 48 + visible: searchVisible + opacity: searchVisible ? 1 : 0 + color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + border.color: searchField.activeFocus ? Theme.primary : Theme.outlineMedium + border.width: searchField.activeFocus ? 2 : 1 + radius: Theme.cornerRadius + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + spacing: Theme.spacingS + + // Search icon + DankIcon { + Layout.alignment: Qt.AlignVCenter + name: "search" + size: Theme.iconSize - 2 + color: searchField.activeFocus ? Theme.primary : Theme.surfaceVariantText + } + + // Search input field + TextInput { + id: searchField + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + height: 32 + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + verticalAlignment: TextInput.AlignVCenter + selectByMouse: true + clip: true + + Component.onCompleted: { + text = root.searchQuery + } + + Connections { + target: root + function onSearchQueryChanged() { + if (searchField.text !== root.searchQuery) { + searchField.text = root.searchQuery + } + } + } + + onTextChanged: { + if (root.searchQuery !== text) { + root.searchQuery = text + root.performSearch() + } + } + Keys.onEscapePressed: event => { + root.hideSearch() + event.accepted = true + } + Keys.onReturnPressed: event => { + if (event.modifiers & Qt.ShiftModifier) { + root.findPrevious() + } else { + root.findNext() + } + event.accepted = true + } + Keys.onEnterPressed: event => { + if (event.modifiers & Qt.ShiftModifier) { + root.findPrevious() + } else { + root.findNext() + } + event.accepted = true + } + } + + // Placeholder text + StyledText { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + text: I18n.tr("Find in note...") + font: searchField.font + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) + visible: searchField.text.length === 0 && !searchField.activeFocus + Layout.leftMargin: -(searchField.width - 20) // Position over the input field + } + + // Match count display + StyledText { + Layout.alignment: Qt.AlignVCenter + text: matchCount > 0 ? "%1/%2".arg(currentMatchIndex + 1).arg(matchCount) : searchQuery.length > 0 ? I18n.tr("No matches") : "" + font.pixelSize: Theme.fontSizeSmall + color: matchCount > 0 ? Theme.primary : Theme.surfaceTextMedium + visible: searchQuery.length > 0 + Layout.rightMargin: Theme.spacingS + } + + // Navigation buttons + DankActionButton { + id: prevButton + Layout.alignment: Qt.AlignVCenter + iconName: "keyboard_arrow_up" + iconSize: Theme.iconSize + iconColor: matchCount > 0 ? Theme.surfaceText : Theme.surfaceTextAlpha + enabled: matchCount > 0 + onClicked: root.findPrevious() + } + + DankActionButton { + id: nextButton + Layout.alignment: Qt.AlignVCenter + iconName: "keyboard_arrow_down" + iconSize: Theme.iconSize + iconColor: matchCount > 0 ? Theme.surfaceText : Theme.surfaceTextAlpha + enabled: matchCount > 0 + onClicked: root.findNext() + } + + DankActionButton { + id: closeSearchButton + Layout.alignment: Qt.AlignVCenter + iconName: "close" + iconSize: Theme.iconSize - 2 + iconColor: Theme.surfaceText + onClicked: root.hideSearch() + } + } + } + + StyledRect { + width: parent.width + height: parent.height - bottomControls.height - Theme.spacingM - (searchVisible ? searchBar.height + Theme.spacingM : 0) + color: Qt.rgba(Theme.surface.r, Theme.surface.g, Theme.surface.b, Theme.notepadTransparency) + border.color: Theme.outlineMedium + border.width: 1 + radius: Theme.cornerRadius + + RowLayout { + id: editorPreviewRow + anchors.fill: parent + anchors.margins: 1 + spacing: Theme.spacingM + + Item { + id: editorPane + visible: !inlinePreviewVisible || previewMode === "split" + Layout.fillHeight: true + Layout.fillWidth: !inlinePreviewVisible || previewMode === "split" + Layout.preferredWidth: inlinePreviewVisible ? parent.width * 0.55 : parent.width + clip: true + + DankFlickable { + id: flickable + anchors.fill: parent + clip: true + contentWidth: width - 11 + + Rectangle { + id: lineNumberArea + anchors.left: parent.left + anchors.top: parent.top + width: SettingsData.notepadShowLineNumbers ? Math.max(30, 32 + Theme.spacingXS) : 0 + height: textArea.contentHeight + textArea.topPadding + textArea.bottomPadding + color: "transparent" + visible: SettingsData.notepadShowLineNumbers + + ListView { + id: lineNumberList + anchors.top: parent.top + anchors.topMargin: textArea.topPadding + anchors.right: parent.right + anchors.rightMargin: 2 + width: 32 + height: textArea.contentHeight + model: SettingsData.notepadShowLineNumbers ? root.lineModel : [] + interactive: false + spacing: 0 + + delegate: Item { + id: lineDelegate + required property int index + required property string modelData + width: 32 + height: measuringText.contentHeight + + Text { + id: measuringText + width: textArea.width - textArea.leftPadding - textArea.rightPadding + text: modelData || " " + font: textArea.font + wrapMode: Text.Wrap + visible: false + } + + StyledText { + anchors.right: parent.right + anchors.rightMargin: 4 + anchors.top: parent.top + text: index + 1 + font.family: textArea.font.family + font.pixelSize: textArea.font.pixelSize + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4) + horizontalAlignment: Text.AlignRight + } + } + } + } + + TextArea.flickable: TextArea { + id: textArea + placeholderText: "" + placeholderTextColor: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) + font.family: SettingsData.notepadUseMonospace ? SettingsData.monoFontFamily : (SettingsData.notepadFontFamily || SettingsData.fontFamily) + font.pixelSize: SettingsData.notepadFontSize * SettingsData.fontScale + font.letterSpacing: 0 + color: Theme.surfaceText + selectedTextColor: Theme.background + selectionColor: Theme.primary + selectByMouse: true + selectByKeyboard: true + wrapMode: TextArea.Wrap + focus: true + activeFocusOnTab: true + textFormat: TextEdit.PlainText + inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase + persistentSelection: true + tabStopDistance: 40 + leftPadding: (SettingsData.notepadShowLineNumbers ? lineNumberArea.width + Theme.spacingXS : Theme.spacingM) + topPadding: Theme.spacingM + rightPadding: Theme.spacingM + bottomPadding: Theme.spacingM + cursorDelegate: Rectangle { + width: 1.5 + radius: 1 + color: Theme.surfaceText + x: textArea.cursorRectangle.x + y: textArea.cursorRectangle.y + height: textArea.cursorRectangle.height + opacity: 1.0 + + SequentialAnimation on opacity { + running: textArea.activeFocus + loops: Animation.Infinite + PropertyAnimation { from: 1.0; to: 0.0; duration: 650; easing.type: Easing.InOutQuad } + PropertyAnimation { from: 0.0; to: 1.0; duration: 650; easing.type: Easing.InOutQuad } + } + } + + Component.onCompleted: { + loadCurrentTabContent() + setTextDocumentLineHeight() + root.updateLineModel() + Qt.callLater(() => { + textArea.forceActiveFocus() + }) + } + + Connections { + target: NotepadStorageService + function onCurrentTabIndexChanged() { + loadCurrentTabContent() + Qt.callLater(() => { + textArea.forceActiveFocus() + }) + } + function onTabsChanged() { + if (NotepadStorageService.tabs.length > 0 && !contentLoaded) { + loadCurrentTabContent() + } + } + } + + Connections { + target: SettingsData + function onNotepadShowLineNumbersChanged() { + root.updateLineModel() + } + } + + onTextChanged: { + if (contentLoaded && text !== lastSavedContent) { + autoSaveTimer.restart() + } + root.contentChanged() + root.updateLineModel() + pluginSyncTimer.restart() + } + + 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 + textArea.selectAll() + break + case Qt.Key_F: + event.accepted = true + root.showSearch() + break + case Qt.Key_P: + if (PluginService.isPluginLoaded("dankNotepadModule")) { + event.accepted = true + root.previewRequested() + } + break + } + } + } + + background: Rectangle { + color: "transparent" + } + } + + StyledText { + id: placeholderOverlay + text: I18n.tr("Start typing your notes here...") + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) + font.family: textArea.font.family + font.pixelSize: textArea.font.pixelSize + visible: textArea.text.length === 0 + anchors.left: textArea.left + anchors.top: textArea.top + anchors.leftMargin: textArea.leftPadding + anchors.topMargin: textArea.topPadding + z: textArea.z + 1 + } + } + } + + Rectangle { + id: previewDivider + visible: inlinePreviewVisible && previewMode === "split" + Layout.fillHeight: true + Layout.preferredWidth: 1 + color: Theme.outlineMedium + } + + Item { + id: previewPane + visible: inlinePreviewVisible + Layout.fillHeight: true + Layout.fillWidth: previewMode === "full" + Layout.preferredWidth: previewMode === "full" ? parent.width : parent.width * 0.45 + clip: true + + // Preview header with copy buttons + Rectangle { + id: previewHeader + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + height: 36 + color: Qt.rgba(Theme.surface.r, Theme.surface.g, Theme.surface.b, Theme.notepadTransparency) + z: 2 + + Row { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.rightMargin: Theme.spacingM + spacing: Theme.spacingS + + // Copy plain text button + DankActionButton { + iconName: "content_copy" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceTextMedium + onClicked: copyPlainTextToClipboard() + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: I18n.tr("Copy Text") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + } + + Rectangle { + width: 1 + height: 20 + color: Theme.outlineVariant + anchors.verticalCenter: parent.verticalCenter + } + + // Copy HTML button + DankActionButton { + iconName: "code" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceTextMedium + onClicked: copyHtmlToClipboard() + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: I18n.tr("Copy HTML") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + } + } + } + + DankFlickable { + id: previewFlickable + anchors.top: previewHeader.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.topMargin: Theme.spacingS + clip: true + contentWidth: width - 11 + contentHeight: previewText.paintedHeight + Theme.spacingM * 2 + + Text { + id: previewText + width: parent.width - Theme.spacingM + padding: Theme.spacingM + wrapMode: Text.WordWrap + textFormat: Text.RichText + text: inlinePreviewVisible ? renderPreviewHtml() : "" + color: Theme.surfaceText + font.family: SettingsData.notepadFontFamily || SettingsData.fontFamily + font.pixelSize: Theme.fontSizeMedium + linkColor: Theme.primary + + onLinkActivated: url => Qt.openUrlExternally(url) + } + } + } + } + } + + 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: I18n.tr("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: I18n.tr("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: I18n.tr("New") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + } + } + + Row { + spacing: Theme.spacingS + visible: PluginService.isPluginLoaded("dankNotepadModule") + DankActionButton { + iconName: inlinePreviewVisible ? "visibility" : "visibility_off" + iconSize: Theme.iconSize - 2 + iconColor: Theme.surfaceText + enabled: textArea.text.length > 0 + onClicked: root.previewRequested() + } + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: I18n.tr("Preview") + 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: { + const len = textArea.text.length; + if (len === 0) return I18n.tr("Empty"); + return len === 1 + ? I18n.tr("%1 character").arg(len) + : I18n.tr("%1 characters").arg(len); + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + } + + StyledText { + text: textArea.lineCount === 1 + ? I18n.tr("Line: %1").arg(textArea.lineCount) + : I18n.tr("Lines: %1").arg(textArea.lineCount) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + visible: textArea.text.length > 0 + opacity: 1.0 + } + + StyledText { + text: { + if (autoSaveTimer.running) { + return I18n.tr("Auto-saving...") + } + + if (hasUnsavedChanges()) { + if (currentTab && currentTab.isTemporary) { + return I18n.tr("Unsaved note...") + } else { + return I18n.tr("Unsaved changes") + } + } else { + return I18n.tr("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 : 0.0 + } + } + } + + Timer { + id: autoSaveTimer + interval: 2000 + repeat: false + onTriggered: { + autoSaveToSession() + } + } + + Timer { + id: pluginSyncTimer + interval: 350 + repeat: false + onTriggered: syncContentToPlugin() + } + + Connections { + target: SettingsData + function onBuiltInPluginSettingsChanged() { + if (PluginService.isPluginLoaded("dankNotepadModule")) { + pluginHighlightedHtml = SettingsData.getBuiltInPluginSetting("dankNotepadModule", "highlightedHtml", "") + } + } + } +} |