1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-15 15:45:20 -04:00

feat(Notepad): Complete refactor - New popout mode & settings

- Add a full popout Notepad experience w/new layout settings
- New ability to choose the left or right side of the screen
- Add more safegaurds to preserve user data throughout
- New banner to show file reload/conflict handling
- New extensionless file support
- Polish settings with gap controls, compact buttons, and shortcuts help
This commit is contained in:
purian23
2026-06-14 20:20:36 -04:00
parent 9d1a81c93c
commit db56c8d74d
13 changed files with 1301 additions and 303 deletions
+268 -9
View File
@@ -1,5 +1,6 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Common
@@ -21,11 +22,24 @@ Item {
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
}
@@ -36,6 +50,37 @@ Item {
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() {
@@ -51,10 +96,14 @@ Item {
}
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();
}
@@ -86,7 +135,6 @@ Item {
NotepadStorageService.switchToTab(tabIndex);
Qt.callLater(() => {
textEditor.loadCurrentTabContent();
if (currentTab) {
root.currentFileName = currentTab.fileName || "";
root.currentFileUrl = currentTab.fileUrl || "";
@@ -100,6 +148,7 @@ Item {
var content = textEditor.text;
var filePath = fileUrl.toString().replace(/^file:\/\//, '');
textEditor.externalWatchPaused = true;
saveFileView.path = "";
pendingSaveContent = content;
saveFileView.path = filePath;
@@ -109,6 +158,53 @@ Item {
});
}
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;
@@ -146,14 +242,151 @@ Item {
root.currentFileName = fileName;
root.currentFileUrl = fileUrl;
textEditor.saveCurrentTabContent();
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()
}
}
RowLayout {
id: bannerActions
Layout.fillWidth: true
Layout.alignment: Qt.AlignRight
spacing: Theme.spacingS
StyledRect {
readonly property real actionWidth: Math.min(keepText.implicitWidth + Theme.spacingM * 2, Math.max(104, (bannerActions.width - bannerActions.spacing) / 2))
Layout.preferredWidth: actionWidth
Layout.preferredHeight: 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 {
readonly property real actionWidth: Math.min(reloadText.implicitWidth + Theme.spacingM * 2, Math.max(116, (bannerActions.width - bannerActions.spacing) / 2))
Layout.preferredWidth: actionWidth
Layout.preferredHeight: 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.fill: parent
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 {
@@ -178,11 +411,12 @@ Item {
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) {
var fileUrl = "file://" + currentTab.filePath;
saveToFile(fileUrl);
root.saveExternalWithFreshnessCheck();
} else {
root.fileDialogOpen = true;
saveBrowserLoader.active = true;
@@ -214,12 +448,28 @@ Item {
onEscapePressed: {
textEditor.autoSaveToSession();
root.hideRequested();
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()
}
}
@@ -242,17 +492,24 @@ Item {
printErrors: true
onSaved: {
if (currentTab && saveFileView.path && pendingSaveContent) {
if (currentTab && saveFileView.path) {
NotepadStorageService.updateTabMetadata(NotepadStorageService.currentTabIndex, {
hasUnsavedChanges: false,
lastSavedContent: pendingSaveContent
});
root.lastSavedFileContent = pendingSaveContent;
pendingSaveContent = "";
textEditor.lastSavedContent = pendingSaveContent;
textEditor.ignoreNextExternalChange = true;
textEditor.commitLiveBuffer();
if (root.conflictBannerVisible)
NotepadStorageService.clearConflict();
}
textEditor.externalWatchPaused = false;
pendingSaveContent = "";
}
onSaveFailed: error => {
textEditor.externalWatchPaused = false;
pendingSaveContent = "";
}
}
@@ -298,6 +555,7 @@ Item {
root.currentFileName = fileName;
root.currentFileUrl = fileUrl;
textEditor.externalWatchPaused = true;
if (currentTab) {
NotepadStorageService.saveTabAs(NotepadStorageService.currentTabIndex, cleanPath);
@@ -343,7 +601,7 @@ Item {
browserTitle: I18n.tr("Open Notepad File")
browserIcon: "folder_open"
browserType: "notepad_load"
fileExtensions: ["*.txt", "*.md", "*.*"]
fileExtensions: ["*"]
allowStacking: true
onFileSelected: path => {
@@ -376,6 +634,7 @@ Item {
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 180
shouldBeVisible: false
allowStacking: true
useOverlayLayer: true
onBackgroundClicked: {
close();
@@ -0,0 +1,134 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Notepad
FloatingWindow {
id: win
property alias shouldBeVisible: win.visible
function show() {
visible = true;
}
function hide() {
visible = false;
}
function toggle() {
visible = !visible;
}
title: I18n.tr("Notepad")
minimumSize: Qt.size(360, 320)
implicitWidth: 640
implicitHeight: 760
color: Theme.surfaceContainer
visible: false
onVisibleChanged: {
if (visible) {
Qt.callLater(notepad.externalSync);
} else {
notepad.flushAutoSave();
}
}
Item {
anchors.fill: parent
Item {
id: titleBar
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: 44
z: 10
MouseArea {
anchors.fill: parent
onPressed: windowControls.tryStartMove()
onDoubleClicked: windowControls.tryToggleMaximize()
}
Rectangle {
anchors.fill: parent
color: Theme.surfaceContainerHigh
opacity: 0.5
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "edit_note"
size: Theme.iconSize - 2
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Notepad")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
DankActionButton {
visible: windowControls.canMaximize
circular: false
iconName: win.maximized ? "fullscreen_exit" : "fullscreen"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: windowControls.tryToggleMaximize()
}
DankActionButton {
circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: win.hide()
}
}
}
Notepad {
id: notepad
anchors.top: titleBar.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.topMargin: Theme.spacingM
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.bottomMargin: Theme.spacingM
inPopout: true
surfaceVisible: win.visible
onHideRequested: win.hide()
onDockRequested: {
win.hide();
PopoutService.openNotepadSlideout();
}
}
}
FloatingWindowControls {
id: windowControls
targetWindow: win
}
}
+433 -236
View File
@@ -10,6 +10,7 @@ Item {
property var cachedFontFamilies: []
property var cachedMonoFamilies: []
property bool fontsEnumerated: false
property bool shortcutsExpanded: false
signal settingsRequested
signal findRequested
@@ -62,11 +63,23 @@ Item {
}
}
MouseArea {
Rectangle {
anchors.fill: parent
visible: root.isVisible
onClicked: root.settingsRequested()
z: 50
color: Qt.rgba(Theme.surface.r, Theme.surface.g, Theme.surface.b, 0.85)
WheelHandler {
// Hold scroll so the editor beneath doesn't move while settings are open.
onWheel: event => {
event.accepted = true;
}
}
MouseArea {
anchors.fill: parent
onClicked: root.settingsRequested()
}
}
Rectangle {
@@ -74,8 +87,8 @@ Item {
visible: root.isVisible
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
width: 360
height: settingsColumn.implicitHeight + Theme.spacingXL * 2
width: Math.min(360, root.width - Theme.spacingL * 2)
height: Math.min(settingsColumn.implicitHeight + Theme.spacingXL * 2, root.height - Theme.spacingL * 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)
@@ -93,274 +106,458 @@ Item {
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
DankFlickable {
id: settingsFlickable
anchors.fill: parent
clip: true
contentWidth: width
contentHeight: settingsColumn.implicitHeight + Theme.spacingXL * 2
Rectangle {
width: parent.width
height: 36
color: "transparent"
Column {
id: settingsColumn
x: Theme.spacingXL
y: Theme.spacingXL
width: settingsFlickable.width - Theme.spacingXL * 2
spacing: Theme.spacingS
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: I18n.tr("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: I18n.tr("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 I18n.tr("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
Rectangle {
width: parent.width
spacing: Theme.spacingS
height: 36
color: "transparent"
Column {
width: parent.width - fontSizeControls.width - Theme.spacingM
spacing: Theme.spacingXS
StyledText {
anchors.left: parent.left
anchors.leftMargin: -Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Notepad Settings")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
}
StyledText {
text: I18n.tr("Font Size")
font.pixelSize: Theme.fontSizeSmall
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)
}
StyledText {
text: SettingsData.notepadFontSize + "px"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
}
DankToggle {
anchors.left: parent.left
anchors.leftMargin: -Theme.spacingM
width: parent.width + Theme.spacingM
text: I18n.tr("Use Monospace Font")
description: I18n.tr("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: I18n.tr("Display line numbers in editor")
checked: SettingsData.notepadShowLineNumbers
onToggled: checked => {
SettingsData.notepadShowLineNumbers = checked;
}
}
DankToggle {
anchors.left: parent.left
anchors.leftMargin: -Theme.spacingM
width: parent.width + Theme.spacingM
text: I18n.tr("Auto-save to disk")
description: I18n.tr("Automatically save changes to opened files as you type")
checked: SettingsData.notepadAutoSave
onToggled: checked => {
SettingsData.notepadAutoSave = 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 {
id: fontSizeControls
spacing: Theme.spacingS
anchors.left: parent.left
anchors.leftMargin: -Theme.spacingM
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
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;
}
DankIcon {
name: "search"
size: Theme.iconSize - 2
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
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
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
StyledText {
anchors.centerIn: parent
text: SettingsData.notepadFontSize + "px"
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 I18n.tr("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
}
}
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;
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
Rectangle {
width: parent.width
spacing: Theme.spacingS
height: transparencySliderColumn.height + Theme.spacingS
color: "transparent"
DankToggle {
anchors.left: parent.left
anchors.leftMargin: -Theme.spacingM
width: parent.width + Theme.spacingM
text: I18n.tr("Custom Transparency")
description: I18n.tr("Override global transparency for Notepad")
checked: SettingsData.notepadTransparencyOverride >= 0
onToggled: checked => {
if (checked) {
SettingsData.notepadTransparencyOverride = SettingsData.notepadLastCustomTransparency;
} else {
SettingsData.notepadTransparencyOverride = -1;
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("Surface Opacity")
description: I18n.tr("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;
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 ? I18n.tr("Using global monospace font from Settings → Personalization") : I18n.tr("Global fonts can be configured in Settings → Personalization")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
wrapMode: Text.WordWrap
opacity: 0.8
Rectangle {
width: parent.width
height: gapColumn.height + Theme.spacingS
color: "transparent"
Column {
id: gapColumn
width: parent.width
spacing: Theme.spacingS
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Default Mode")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
DankButtonGroup {
model: [I18n.tr("Slideout"), I18n.tr("Popout")]
size: "small"
currentIndex: SettingsData.notepadDefaultMode === "popout" ? 1 : 0
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.notepadDefaultMode = index === 1 ? "popout" : "slideout";
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
visible: SettingsData.notepadDefaultMode !== "popout"
StyledText {
text: I18n.tr("Open From")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
DankButtonGroup {
model: [I18n.tr("Right"), I18n.tr("Left")]
size: "small"
currentIndex: SettingsData.notepadSlideoutSide === "left" ? 1 : 0
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.notepadSlideoutSide = index === 1 ? "left" : "right";
}
}
}
DankToggle {
anchors.left: parent.left
anchors.leftMargin: -Theme.spacingM
width: parent.width + Theme.spacingM
text: I18n.tr("Auto Compositor Gaps")
description: I18n.tr("Inset the Notepad from screen edges using the compositor's configured gaps")
checked: SettingsData.notepadUseCompositorGap
onToggled: checked => {
SettingsData.notepadUseCompositorGap = checked;
}
}
StyledText {
visible: !SettingsData.notepadUseCompositorGap
text: I18n.tr("Manual Gaps")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
DankSlider {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingXS
width: parent.width - Theme.spacingXS * 2
height: 24
visible: !SettingsData.notepadUseCompositorGap
value: SettingsData.notepadEdgeGap
minimum: 0
maximum: 64
unit: "px"
showValue: true
wheelEnabled: false
onSliderValueChanged: newValue => {
SettingsData.notepadEdgeGap = newValue;
}
}
}
}
StyledText {
width: parent.width
text: SettingsData.notepadUseMonospace ? I18n.tr("Using global monospace font from Settings → Personalization") : I18n.tr("Global fonts can be configured in Settings → Personalization")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
wrapMode: Text.WordWrap
opacity: 0.8
}
StyledRect {
width: parent.width
implicitHeight: shortcutsHeader.height + (root.shortcutsExpanded ? shortcutsColumn.implicitHeight + Theme.spacingM : 0)
radius: Theme.cornerRadius
color: root.shortcutsExpanded ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : "transparent"
border.color: root.shortcutsExpanded ? Theme.primary : Theme.outlineMedium
border.width: root.shortcutsExpanded ? 2 : 1
StateLayer {
anchors.fill: parent
stateColor: Theme.primary
cornerRadius: parent.radius
onClicked: root.shortcutsExpanded = !root.shortcutsExpanded
}
Row {
id: shortcutsHeader
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingS
height: 36
spacing: Theme.spacingS
DankIcon {
name: root.shortcutsExpanded ? "expand_less" : "expand_more"
size: Theme.iconSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Keyboard Shortcuts")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
id: shortcutsColumn
visible: root.shortcutsExpanded
width: parent.width - Theme.spacingL * 2
anchors.top: shortcutsHeader.bottom
anchors.horizontalCenter: parent.horizontalCenter
spacing: 2
StyledText {
width: parent.width
text: I18n.tr("Ctrl+S: Save • Ctrl+O: Open • Ctrl+N: New • Ctrl+F: Find")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
}
StyledText {
width: parent.width
text: I18n.tr("Ctrl+A: Select All • Ctrl+P: Preview • Enter/Shift+Enter: Find Next/Previous • Esc: Close")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
}
}
}
}
}
}
+267 -32
View File
@@ -32,6 +32,23 @@ Column {
property string pluginHighlightedHtml: ""
property string lastPluginContent: ""
property int loadRequestId: 0
property bool ignoreNextExternalChange: false
property bool watcherReloadPending: false
property bool externalWatchPaused: false
property bool inPopout: false
property bool surfaceVisible: true
// Tab ids are Date.now() timestamps (~1.78e12) which overflow a 32-bit `int`,
// corrupting the value (e.g. -946062153) and breaking buffer keying. `var`
// holds the full JS-safe integer.
property var loadedTabId: -1
property bool applyingShared: false
property bool showPathInfo: false
function currentFilePath() {
if (!currentTab)
return "";
return currentTab.isTemporary ? (NotepadStorageService.baseDir + "/" + currentTab.filePath) : currentTab.filePath;
}
signal saveRequested
signal openRequested
@@ -40,6 +57,10 @@ Column {
signal escapePressed
signal contentChanged
signal settingsRequested
signal popoutRequested
signal dockRequested
signal conflictDetected(string diskContent)
signal autoSaveRequested
function hasUnsavedChanges() {
if (!currentTab || !contentLoaded) {
@@ -52,6 +73,12 @@ Column {
return textArea.text !== lastSavedContent;
}
function commitLiveBuffer() {
if (loadedTabId < 0 || !contentLoaded)
return;
NotepadStorageService.setSessionBuffer(loadedTabId, textArea.text, lastSavedContent);
}
function loadCurrentTabContent() {
if (!currentTab)
return;
@@ -62,8 +89,25 @@ Column {
const activeTab = NotepadStorageService.tabs.length > NotepadStorageService.currentTabIndex ? NotepadStorageService.tabs[NotepadStorageService.currentTabIndex] : null;
if (requestId !== loadRequestId || !activeTab || activeTab.id !== requestedTabId)
return;
const buffer = NotepadStorageService.getSessionBuffer(requestedTabId);
if (buffer !== undefined) {
applyingShared = true;
lastSavedContent = buffer.baseline;
textArea.text = buffer.content;
applyingShared = false;
loadedTabId = requestedTabId;
contentLoaded = true;
syncContentToPlugin();
applyDiskContent(content);
return;
}
applyingShared = true;
lastSavedContent = content;
textArea.text = content;
applyingShared = false;
loadedTabId = requestedTabId;
contentLoaded = true;
syncContentToPlugin();
});
@@ -72,14 +116,56 @@ Column {
function saveCurrentTabContent() {
if (!currentTab || !contentLoaded)
return;
if (!currentTab.isTemporary)
return;
NotepadStorageService.saveTabContent(NotepadStorageService.currentTabIndex, textArea.text);
lastSavedContent = textArea.text;
NotepadStorageService.clearSessionBuffer(loadedTabId);
}
function autoSaveToSession() {
commitLiveBuffer();
if (!currentTab || !contentLoaded)
return;
saveCurrentTabContent();
if (currentTab.isTemporary) {
saveCurrentTabContent();
} else if (SettingsData.notepadAutoSave) {
root.autoSaveRequested();
}
}
function syncFromDisk() {
if (!currentTab)
return;
loadCurrentTabContent();
}
function applyDiskContent(diskContent) {
if (diskContent === undefined || diskContent === null)
return;
if (diskContent === textArea.text) {
lastSavedContent = diskContent;
return;
}
if (diskContent === lastSavedContent) {
return;
}
if (textArea.text === lastSavedContent) {
reloadFromDisk(diskContent);
} else if (surfaceVisible) {
conflictDetected(diskContent);
}
}
function reloadFromDisk(diskContent) {
applyingShared = true;
contentLoaded = false;
textArea.text = diskContent;
lastSavedContent = diskContent;
contentLoaded = true;
applyingShared = false;
NotepadStorageService.clearSessionBuffer(loadedTabId);
syncContentToPlugin();
}
function setTextDocumentLineHeight() {
@@ -202,7 +288,8 @@ Column {
if (!currentTab)
return;
const filePath = currentTab?.filePath || "";
const ext = filePath.split('.').pop().toLowerCase();
const baseName = filePath.split('/').pop();
const ext = baseName.includes('.') ? baseName.split('.').pop().toLowerCase() : "";
const content = textArea.text;
if (content === lastPluginContent && SettingsData.getBuiltInPluginSetting("dankNotepadModule", "previewActive", false) === inlinePreviewVisible) {
@@ -550,6 +637,7 @@ Column {
Connections {
target: NotepadStorageService
function onCurrentTabIndexChanged() {
root.commitLiveBuffer();
loadCurrentTabContent();
Qt.callLater(() => {
textArea.forceActiveFocus();
@@ -570,7 +658,9 @@ Column {
}
onTextChanged: {
if (contentLoaded && text !== lastSavedContent) {
// Debounced flush to the shared buffer (+ optional disk
// autosave) for every loaded tab, not just scratch notes.
if (contentLoaded && !applyingShared) {
autoSaveTimer.restart();
}
root.contentChanged();
@@ -744,6 +834,7 @@ Column {
spacing: Theme.spacingS
Item {
id: buttonBarItem
width: parent.width
height: 32
@@ -820,17 +911,98 @@ Column {
}
}
DankActionButton {
Row {
id: rightButtonRow
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
iconName: "more_horiz"
iconSize: Theme.iconSize - 2
iconColor: Theme.surfaceText
onClicked: root.settingsRequested()
spacing: Theme.spacingS
DankActionButton {
visible: !root.inPopout
iconName: "open_in_new"
iconSize: Theme.iconSize - 2
iconColor: Theme.surfaceText
onClicked: root.popoutRequested()
}
DankActionButton {
visible: root.inPopout
iconName: "dock_to_right"
iconSize: Theme.iconSize - 2
iconColor: Theme.surfaceText
onClicked: root.dockRequested()
}
DankActionButton {
iconName: "more_horiz"
iconSize: Theme.iconSize - 2
iconColor: Theme.surfaceText
onClicked: root.settingsRequested()
}
}
StyledRect {
id: pathInfoPopup
visible: root.showPathInfo
anchors.right: parent.right
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingS
width: Math.min(root.width, 360)
height: pathInfoRow.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Theme.outlineMedium
border.width: 1
z: 10
Row {
id: pathInfoRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: currentTab && currentTab.isTemporary ? "draft" : "description"
size: Theme.iconSize - 4
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
width: pathInfoRow.width - (Theme.iconSize - 4) - copyPathButton.width - Theme.spacingS * 2
text: root.currentFilePath()
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
elide: Text.ElideMiddle
anchors.verticalCenter: parent.verticalCenter
}
DankActionButton {
id: copyPathButton
iconName: "content_copy"
iconSize: Theme.iconSize - 6
iconColor: Theme.surfaceTextMedium
anchors.verticalCenter: parent.verticalCenter
onClicked: {
const proc = clipboardCopyProcComp.createObject(root, {
content: root.currentFilePath(),
running: true
});
proc.exited.connect(() => {
ToastService.showInfo(I18n.tr("Path copied to clipboard"));
proc.destroy();
});
}
}
}
}
}
Row {
id: statusRow
width: parent.width
spacing: Theme.spacingL
@@ -853,35 +1025,46 @@ Column {
opacity: 1.0
}
StyledText {
text: {
if (autoSaveTimer.running) {
return I18n.tr("Auto-saving...");
}
Row {
visible: textArea.text.length > 0
spacing: Theme.spacingXS
if (hasUnsavedChanges()) {
if (currentTab && currentTab.isTemporary) {
return I18n.tr("Unsaved note...");
} else {
return I18n.tr("Unsaved changes");
StyledText {
anchors.verticalCenter: parent.verticalCenter
readonly property bool savingToDisk: autoSaveTimer.running && currentTab && (currentTab.isTemporary || SettingsData.notepadAutoSave)
text: {
if (savingToDisk) {
return I18n.tr("Saving...");
}
} 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;
if (currentTab && currentTab.isTemporary) {
return I18n.tr("Auto saved");
}
return hasUnsavedChanges() ? I18n.tr("Unsaved changes") : I18n.tr("Saved");
}
font.pixelSize: Theme.fontSizeSmall
color: {
if (savingToDisk) {
return Theme.primary;
}
if (currentTab && currentTab.isTemporary) {
return Theme.success;
}
return hasUnsavedChanges() ? Theme.warning : Theme.success;
}
}
opacity: textArea.text.length > 0 ? 1.0 : 0.0
DankActionButton {
anchors.verticalCenter: parent.verticalCenter
iconName: "info"
iconSize: Theme.iconSizeSmall
iconColor: root.showPathInfo ? Theme.primary : Theme.surfaceTextMedium
buttonSize: 20
onClicked: root.showPathInfo = !root.showPathInfo
}
}
}
}
@@ -902,6 +1085,38 @@ Column {
onTriggered: syncContentToPlugin()
}
FileView {
id: externalWatch
path: (!root.externalWatchPaused && currentTab && !currentTab.isTemporary && currentTab.filePath) ? currentTab.filePath : ""
blockLoading: true
preload: true
watchChanges: true
onFileChanged: {
root.watcherReloadPending = true;
reload();
}
onLoaded: {
if (root.ignoreNextExternalChange) {
root.ignoreNextExternalChange = false;
root.lastSavedContent = externalWatch.text();
root.watcherReloadPending = false;
return;
}
if (!root.watcherReloadPending)
return;
root.watcherReloadPending = false;
if (!root.contentLoaded || !root.currentTab || root.currentTab.isTemporary)
return;
if (!root.surfaceVisible)
return;
root.applyDiskContent(externalWatch.text());
}
onLoadFailed: error => {}
}
Connections {
target: SettingsData
function onBuiltInPluginSettingsChanged() {
@@ -910,4 +1125,24 @@ Column {
}
}
}
Connections {
target: NotepadStorageService
function onSessionBufferRevisionChanged() {
if (applyingShared || !contentLoaded || loadedTabId < 0)
return;
if (textArea.activeFocus)
return;
var buffer = NotepadStorageService.getSessionBuffer(loadedTabId);
if (buffer === undefined || buffer.content === textArea.text)
return;
if (textArea.text === lastSavedContent) {
applyingShared = true;
lastSavedContent = buffer.baseline;
textArea.text = buffer.content;
applyingShared = false;
syncContentToPlugin();
}
}
}
}