summaryrefslogtreecommitdiff
path: root/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Notepad
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Notepad')
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Notepad/Notepad.qml531
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Notepad/NotepadSettings.qml367
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Notepad/NotepadTabs.qml285
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/Notepad/NotepadTextEditor.qml924
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", "")
+ }
+ }
+ }
+}