pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts 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: "" readonly property bool conflictBannerVisible: currentTab !== null && NotepadStorageService.conflictTabId === currentTab.id property var slideout: null property bool inPopout: false property bool surfaceVisible: slideout ? slideout.isVisible : true signal hideRequested signal popoutRequested signal dockRequested signal previewRequested(string content) function externalSync() { textEditor.syncFromDisk(); } function flushAutoSave() { textEditor.autoSaveToSession(); } Ref { service: NotepadStorageService } Connections { target: slideout enabled: slideout !== null function onAboutToHide() { textEditor.autoSaveToSession(); } function onRevealed() { textEditor.syncFromDisk(); } } function showConflictBanner(diskContent) { if (!currentTab) return; NotepadStorageService.flagConflict(currentTab.id, diskContent); } function resolveConflictKeepEdits() { if (!root.conflictBannerVisible) return; NotepadStorageService.clearConflict(); if (currentTab && currentTab.filePath && !currentTab.isTemporary) { root.saveToFile("file://" + currentTab.filePath); } } function resolveConflictReload() { if (!root.conflictBannerVisible) return; const diskContent = NotepadStorageService.conflictDiskContent; NotepadStorageService.clearConflict(); textEditor.reloadFromDisk(diskContent); } function dismissConflictBanner() { if (root.conflictBannerVisible) NotepadStorageService.clearConflict(); } function hasUnsavedChanges() { return textEditor.hasUnsavedChanges(); } function hasUnsavedTemporaryContent() { return hasUnsavedChanges(); } function createNewTab() { performCreateNewTab(); } function performCreateNewTab() { textEditor.commitLiveBuffer(); NotepadStorageService.createNewTab(); textEditor.applyingShared = true; textEditor.text = ""; textEditor.lastSavedContent = ""; textEditor.loadedTabId = -1; textEditor.contentLoaded = true; textEditor.applyingShared = false; 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(() => { 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:\/\//, ''); textEditor.externalWatchPaused = true; saveFileView.path = ""; pendingSaveContent = content; saveFileView.path = filePath; Qt.callLater(() => { saveFileView.setText(pendingSaveContent); }); } function saveExternalWithFreshnessCheck() { if (!currentTab || currentTab.isTemporary || !currentTab.filePath) return; const filePath = currentTab.filePath; loadFileView.path = ""; loadFileView.path = filePath; if (!loadFileView.waitForJob()) { saveToFile("file://" + filePath); return; } Qt.callLater(() => { if (!currentTab || currentTab.isTemporary || currentTab.filePath !== filePath) return; const diskContent = loadFileView.text(); if (diskContent !== undefined && diskContent !== null && diskContent !== textEditor.text && diskContent !== textEditor.lastSavedContent) { root.showConflictBanner(diskContent); return; } saveToFile("file://" + filePath); }); } function autoSaveExternal() { if (!SettingsData.notepadAutoSave) return; if (!currentTab || currentTab.isTemporary || !currentTab.filePath) return; if (!textEditor.hasUnsavedChanges()) return; const filePath = currentTab.filePath; loadFileView.path = ""; loadFileView.path = filePath; if (!loadFileView.waitForJob()) return; Qt.callLater(() => { if (!currentTab || currentTab.isTemporary || currentTab.filePath !== filePath) return; const diskContent = loadFileView.text(); if (diskContent === undefined || diskContent === null) return; if (diskContent !== textEditor.lastSavedContent) return; saveToFile("file://" + filePath); }); } 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.loadedTabId = currentTab.id; NotepadStorageService.clearSessionBuffer(currentTab.id); if (root.conflictBannerVisible) NotepadStorageService.clearConflict(); } }); } } Item { id: conflictBanner anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right height: root.conflictBannerVisible ? bannerRect.implicitHeight : 0 visible: height > 0 clip: true z: 5 Behavior on height { NumberAnimation { duration: Theme.shortDuration easing.type: Theme.standardEasing } } StyledRect { id: bannerRect anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top implicitHeight: bannerLayout.implicitHeight + Theme.spacingM * 2 radius: Theme.cornerRadius color: Theme.withAlpha(Theme.warning, 0.12) border.color: Theme.withAlpha(Theme.warning, 0.5) border.width: 1 ColumnLayout { id: bannerLayout anchors.fill: parent anchors.margins: Theme.spacingM spacing: Theme.spacingS RowLayout { Layout.fillWidth: true spacing: Theme.spacingM DankIcon { Layout.alignment: Qt.AlignVCenter name: "sync_problem" size: Theme.iconSize - 2 color: Theme.warning } StyledText { Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter text: I18n.tr("File changed on disk") font.pixelSize: Theme.fontSizeMedium font.weight: Font.Medium color: Theme.surfaceText wrapMode: Text.NoWrap elide: Text.ElideRight } DankActionButton { Layout.alignment: Qt.AlignVCenter iconName: "close" iconSize: Theme.iconSizeSmall iconColor: Theme.surfaceText buttonSize: 28 onClicked: root.dismissConflictBanner() } } Item { Layout.fillWidth: true Layout.preferredHeight: 32 Row { id: bannerActions anchors.right: parent.right spacing: Theme.spacingS readonly property real available: parent.width StyledRect { width: Math.min(keepText.implicitWidth + Theme.spacingM * 2, Math.max(104, (bannerActions.available - bannerActions.spacing) / 2)) height: 32 radius: Theme.cornerRadius color: "transparent" border.color: Theme.outlineMedium border.width: 1 StateLayer { anchors.fill: parent cornerRadius: parent.radius stateColor: Theme.surfaceText onClicked: root.resolveConflictKeepEdits() } StyledText { id: keepText anchors.centerIn: parent width: parent.width - Theme.spacingM text: I18n.tr("Keep My Edits") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceText horizontalAlignment: Text.AlignHCenter elide: Text.ElideRight } } StyledRect { width: Math.min(reloadText.implicitWidth + Theme.spacingM * 2, Math.max(116, (bannerActions.available - bannerActions.spacing) / 2)) height: 32 radius: Theme.cornerRadius color: Theme.primary StateLayer { anchors.fill: parent cornerRadius: parent.radius stateColor: Theme.background onClicked: root.resolveConflictReload() } StyledText { id: reloadText anchors.centerIn: parent width: parent.width - Theme.spacingM text: I18n.tr("Reload From Disk") font.pixelSize: Theme.fontSizeSmall color: Theme.background horizontalAlignment: Text.AlignHCenter elide: Text.ElideRight } } } } } } } Column { anchors.top: conflictBanner.bottom anchors.topMargin: root.conflictBannerVisible ? Theme.spacingM : 0 anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom 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 inPopout: root.inPopout surfaceVisible: root.surfaceVisible onSaveRequested: { if (currentTab && !currentTab.isTemporary && currentTab.filePath) { root.saveExternalWithFreshnessCheck(); } 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(); if (showSettingsMenu) { showSettingsMenu = false; return; } if (!root.inPopout) { root.hideRequested(); } } onSettingsRequested: { showSettingsMenu = !showSettingsMenu; } onPopoutRequested: root.popoutRequested() onDockRequested: root.dockRequested() onConflictDetected: diskContent => { root.showConflictBanner(diskContent); } onAutoSaveRequested: root.autoSaveExternal() } } 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) { NotepadStorageService.updateTabMetadata(NotepadStorageService.currentTabIndex, { hasUnsavedChanges: false, lastSavedContent: pendingSaveContent }); root.lastSavedFileContent = pendingSaveContent; textEditor.lastSavedContent = pendingSaveContent; textEditor.ignoreNextExternalChange = true; textEditor.commitLiveBuffer(); if (root.conflictBannerVisible) NotepadStorageService.clearConflict(); } textEditor.externalWatchPaused = false; pendingSaveContent = ""; } onSaveFailed: error => { textEditor.externalWatchPaused = false; 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; textEditor.externalWatchPaused = true; 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: ["*"] 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 useOverlayLayer: 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; } } } } } } }