1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-08 06:25:37 -05:00

Modularize the Notepad

This commit is contained in:
purian23
2025-09-20 14:22:13 -04:00
parent 3cac6e5eea
commit d4816bd174
8 changed files with 1430 additions and 1316 deletions

505
Modules/Notepad/Notepad.qml Normal file
View File

@@ -0,0 +1,505 @@
import QtQuick
import QtQuick.Controls
import QtCore
import Quickshell
import Quickshell.Wayland
import Quickshell.Io
import qs.Common
import qs.Modals.Common
import qs.Modals.FileBrowser
import qs.Services
import qs.Widgets
pragma ComponentBehavior: Bound
Item {
id: root
property bool fileDialogOpen: false
property string currentFileName: ""
property url currentFileUrl
property bool confirmationDialogOpen: false
property string pendingAction: ""
property url pendingFileUrl
property string lastSavedFileContent: ""
property var currentTab: NotepadStorageService.tabs.length > NotepadStorageService.currentTabIndex ? NotepadStorageService.tabs[NotepadStorageService.currentTabIndex] : null
property bool showSettingsMenu: false
property string pendingSaveContent: ""
signal hideRequested()
function hasUnsavedChanges() {
return textEditor.hasUnsavedChanges()
}
function hasUnsavedTemporaryContent() {
return hasUnsavedChanges()
}
function createNewTab() {
performCreateNewTab()
}
function performCreateNewTab() {
NotepadStorageService.createNewTab()
textEditor.text = ""
textEditor.lastSavedContent = ""
textEditor.contentLoaded = true
textEditor.textArea.forceActiveFocus()
}
function closeTab(tabIndex) {
if (tabIndex === NotepadStorageService.currentTabIndex && hasUnsavedChanges()) {
root.pendingAction = "close_tab_" + tabIndex
root.confirmationDialogOpen = true
confirmationDialog.open()
} else {
performCloseTab(tabIndex)
}
}
function performCloseTab(tabIndex) {
NotepadStorageService.closeTab(tabIndex)
Qt.callLater(() => {
textEditor.loadCurrentTabContent()
})
}
function switchToTab(tabIndex) {
if (tabIndex < 0 || tabIndex >= NotepadStorageService.tabs.length) return
if (textEditor.contentLoaded) {
textEditor.autoSaveToSession()
}
NotepadStorageService.switchToTab(tabIndex)
Qt.callLater(() => {
textEditor.loadCurrentTabContent()
if (currentTab) {
root.currentFileName = currentTab.fileName || ""
root.currentFileUrl = currentTab.fileUrl || ""
}
})
}
function saveToFile(fileUrl) {
if (!currentTab) return
var content = textEditor.text
var filePath = fileUrl.toString().replace(/^file:\/\//, '')
saveFileView.path = ""
pendingSaveContent = content
saveFileView.path = filePath
Qt.callLater(() => {
saveFileView.setText(pendingSaveContent)
})
}
function loadFromFile(fileUrl) {
if (hasUnsavedTemporaryContent()) {
root.pendingFileUrl = fileUrl
root.pendingAction = "load_file"
root.confirmationDialogOpen = true
confirmationDialog.open()
} else {
performLoadFromFile(fileUrl)
}
}
function performLoadFromFile(fileUrl) {
const filePath = fileUrl.toString().replace(/^file:\/\//, '')
const fileName = filePath.split('/').pop()
loadFileView.path = ""
loadFileView.path = filePath
if (loadFileView.waitForJob()) {
Qt.callLater(() => {
var content = loadFileView.text()
if (currentTab && content !== undefined && content !== null) {
textEditor.text = content
textEditor.lastSavedContent = content
textEditor.contentLoaded = true
root.lastSavedFileContent = content
NotepadStorageService.updateTabMetadata(NotepadStorageService.currentTabIndex, {
title: fileName,
filePath: filePath,
isTemporary: false
})
root.currentFileName = fileName
root.currentFileUrl = fileUrl
textEditor.saveCurrentTabContent()
}
})
}
}
Column {
anchors.fill: parent
spacing: Theme.spacingM
NotepadTabs {
id: tabBar
width: parent.width
contentLoaded: textEditor.contentLoaded
onTabSwitched: (tabIndex) => {
switchToTab(tabIndex)
}
onTabClosed: (tabIndex) => {
closeTab(tabIndex)
}
onNewTabRequested: {
createNewTab()
}
}
NotepadTextEditor {
id: textEditor
width: parent.width
height: parent.height - tabBar.height - Theme.spacingM * 2
onSaveRequested: {
if (currentTab && !currentTab.isTemporary && currentTab.filePath) {
var fileUrl = "file://" + currentTab.filePath
saveToFile(fileUrl)
} else {
root.fileDialogOpen = true
saveBrowser.open()
}
}
onOpenRequested: {
if (hasUnsavedChanges()) {
root.pendingAction = "open"
root.confirmationDialogOpen = true
confirmationDialog.open()
} else {
root.fileDialogOpen = true
loadBrowser.open()
}
}
onNewRequested: {
if (hasUnsavedChanges()) {
root.pendingAction = "new"
root.confirmationDialogOpen = true
confirmationDialog.open()
} else {
createNewTab()
}
}
onEscapePressed: {
root.hideRequested()
}
onSettingsRequested: {
showSettingsMenu = !showSettingsMenu
}
}
}
NotepadSettings {
id: notepadSettings
anchors.fill: parent
isVisible: showSettingsMenu
onSettingsRequested: showSettingsMenu = !showSettingsMenu
}
FileView {
id: saveFileView
blockWrites: true
preload: false
atomicWrites: true
printErrors: true
onSaved: {
if (currentTab && saveFileView.path && pendingSaveContent) {
NotepadStorageService.updateTabMetadata(NotepadStorageService.currentTabIndex, {
hasUnsavedChanges: false,
lastSavedContent: pendingSaveContent
})
root.lastSavedFileContent = pendingSaveContent
pendingSaveContent = ""
}
}
onSaveFailed: (error) => {
pendingSaveContent = ""
}
}
FileView {
id: loadFileView
blockLoading: true
preload: true
atomicWrites: true
printErrors: true
onLoadFailed: (error) => {
}
}
FileBrowserModal {
id: saveBrowser
browserTitle: qsTr("Save Notepad File")
browserIcon: "save"
browserType: "notepad_save"
fileExtensions: ["*.txt", "*.md", "*.*"]
allowStacking: true
saveMode: true
defaultFileName: {
if (currentTab && currentTab.title && currentTab.title !== "Untitled") {
return currentTab.title
} else if (currentTab && !currentTab.isTemporary && currentTab.filePath) {
return currentTab.filePath.split('/').pop()
} else {
return "note.txt"
}
}
WlrLayershell.layer: WlrLayershell.Overlay
onFileSelected: (path) => {
root.fileDialogOpen = false
const cleanPath = path.toString().replace(/^file:\/\//, '')
const fileName = cleanPath.split('/').pop()
const fileUrl = "file://" + cleanPath
root.currentFileName = fileName
root.currentFileUrl = fileUrl
if (currentTab) {
NotepadStorageService.saveTabAs(
NotepadStorageService.currentTabIndex,
cleanPath
)
}
saveToFile(fileUrl)
if (root.pendingAction === "new") {
Qt.callLater(() => {
createNewTab()
})
} else if (root.pendingAction === "open") {
Qt.callLater(() => {
root.fileDialogOpen = true
loadBrowser.open()
})
} else if (root.pendingAction.startsWith("close_tab_")) {
Qt.callLater(() => {
var tabIndex = parseInt(root.pendingAction.split("_")[2])
performCloseTab(tabIndex)
})
}
root.pendingAction = ""
close()
}
onDialogClosed: {
root.fileDialogOpen = false
}
}
FileBrowserModal {
id: loadBrowser
browserTitle: qsTr("Open Notepad File")
browserIcon: "folder_open"
browserType: "notepad_load"
fileExtensions: ["*.txt", "*.md", "*.*"]
allowStacking: true
WlrLayershell.layer: WlrLayershell.Overlay
onFileSelected: (path) => {
root.fileDialogOpen = false
const cleanPath = path.toString().replace(/^file:\/\//, '')
const fileName = cleanPath.split('/').pop()
const fileUrl = "file://" + cleanPath
root.currentFileName = fileName
root.currentFileUrl = fileUrl
loadFromFile(fileUrl)
close()
}
onDialogClosed: {
root.fileDialogOpen = false
}
}
DankModal {
id: confirmationDialog
width: 400
height: 180
shouldBeVisible: false
allowStacking: true
onBackgroundClicked: {
close()
root.confirmationDialogOpen = false
}
content: Component {
FocusScope {
anchors.fill: parent
focus: true
Keys.onEscapePressed: event => {
confirmationDialog.close()
root.confirmationDialogOpen = false
event.accepted = true
}
Column {
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
spacing: Theme.spacingM
Row {
width: parent.width
Column {
width: parent.width - 40
spacing: Theme.spacingXS
StyledText {
text: qsTr("Unsaved Changes")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: root.pendingAction === "new" ?
qsTr("You have unsaved changes. Save before creating a new file?") :
root.pendingAction.startsWith("close_tab_") ?
qsTr("You have unsaved changes. Save before closing this tab?") :
root.pendingAction === "load_file" || root.pendingAction === "open" ?
qsTr("You have unsaved changes. Save before opening a file?") :
qsTr("You have unsaved changes. Save before continuing?")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
width: parent.width
wrapMode: Text.Wrap
}
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: {
confirmationDialog.close()
root.confirmationDialogOpen = false
}
}
}
Item {
width: parent.width
height: 40
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Rectangle {
width: Math.max(80, discardText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: discardArea.containsMouse ? Theme.surfaceTextHover : "transparent"
border.color: Theme.surfaceVariantAlpha
border.width: 1
StyledText {
id: discardText
anchors.centerIn: parent
text: qsTr("Don't Save")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
MouseArea {
id: discardArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
confirmationDialog.close()
root.confirmationDialogOpen = false
if (root.pendingAction === "new") {
createNewTab()
} else if (root.pendingAction === "open") {
root.fileDialogOpen = true
loadBrowser.open()
} else if (root.pendingAction === "load_file") {
performLoadFromFile(root.pendingFileUrl)
} else if (root.pendingAction.startsWith("close_tab_")) {
var tabIndex = parseInt(root.pendingAction.split("_")[2])
performCloseTab(tabIndex)
}
root.pendingAction = ""
root.pendingFileUrl = ""
}
}
}
Rectangle {
width: Math.max(70, saveAsText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: saveAsArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
StyledText {
id: saveAsText
anchors.centerIn: parent
text: qsTr("Save")
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
}
MouseArea {
id: saveAsArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
confirmationDialog.close()
root.confirmationDialogOpen = false
root.fileDialogOpen = true
saveBrowser.open()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,268 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
pragma ComponentBehavior: Bound
Item {
id: root
property bool isVisible: false
property var cachedFontFamilies: []
property var cachedMonoFamilies: []
property bool fontsEnumerated: false
signal settingsRequested()
function enumerateFonts() {
var fonts = ["Default"]
var availableFonts = Qt.fontFamilies()
var rootFamilies = []
var seenFamilies = new Set()
for (var i = 0; i < availableFonts.length; i++) {
var fontName = availableFonts[i]
if (fontName.startsWith("."))
continue
if (fontName === SettingsData.defaultFontFamily)
continue
var rootName = fontName.replace(/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i, "").replace(/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i,
"").replace(/ (UI|Display|Text|Mono|Sans|Serif)$/i, function (match, suffix) {
return match
}).trim()
if (!seenFamilies.has(rootName) && rootName !== "") {
seenFamilies.add(rootName)
rootFamilies.push(rootName)
}
}
cachedFontFamilies = fonts.concat(rootFamilies.sort())
var monoFonts = ["Default"]
var monoFamilies = []
var seenMonoFamilies = new Set()
for (var j = 0; j < availableFonts.length; j++) {
var fontName2 = availableFonts[j]
if (fontName2.startsWith("."))
continue
if (fontName2 === SettingsData.defaultMonoFontFamily)
continue
var lowerName = fontName2.toLowerCase()
if (lowerName.includes("mono") || lowerName.includes("code") || lowerName.includes("console") || lowerName.includes("terminal") || lowerName.includes("courier") || lowerName.includes("dejavu sans mono") || lowerName.includes(
"jetbrains") || lowerName.includes("fira") || lowerName.includes("hack") || lowerName.includes("source code") || lowerName.includes("ubuntu mono") || lowerName.includes("cascadia")) {
var rootName2 = fontName2.replace(/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i, "").replace(/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i, "").trim()
if (!seenMonoFamilies.has(rootName2) && rootName2 !== "") {
seenMonoFamilies.add(rootName2)
monoFamilies.push(rootName2)
}
}
}
cachedMonoFamilies = monoFonts.concat(monoFamilies.sort())
fontsEnumerated = true
}
Component.onCompleted: {
if (!fontsEnumerated) {
enumerateFonts()
}
}
MouseArea {
anchors.fill: parent
visible: root.isVisible
onClicked: root.settingsRequested()
z: 50
}
Rectangle {
id: settingsMenu
visible: root.isVisible
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
width: 360
height: settingsColumn.implicitHeight + Theme.spacingXL * 2
radius: Theme.cornerRadius
color: Theme.popupBackground()
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
z: 100
Rectangle {
anchors.fill: parent
anchors.topMargin: 4
anchors.leftMargin: 2
anchors.rightMargin: -2
anchors.bottomMargin: -4
radius: parent.radius
color: Qt.rgba(0, 0, 0, 0.15)
z: parent.z - 1
}
Column {
id: settingsColumn
width: parent.width - Theme.spacingXL * 2
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Theme.spacingXL
spacing: Theme.spacingS
Rectangle {
width: parent.width
height: 36
color: "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: -Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
text: "Notepad Font Settings"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
DankToggle {
anchors.left: parent.left
anchors.leftMargin: -Theme.spacingM
width: parent.width + Theme.spacingM
text: "Use Monospace Font"
description: "Toggle fonts"
checked: SettingsData.notepadUseMonospace
onToggled: checked => {
SettingsData.notepadUseMonospace = checked
}
}
Rectangle {
width: parent.width
height: visible ? (fontDropdown.height + Theme.spacingS) : 0
color: "transparent"
visible: !SettingsData.notepadUseMonospace
DankDropdown {
id: fontDropdown
anchors.left: parent.left
anchors.leftMargin: -Theme.spacingM
width: parent.width + Theme.spacingM
text: "Font Family"
options: cachedFontFamilies
currentValue: {
if (!SettingsData.notepadFontFamily || SettingsData.notepadFontFamily === "")
return "Default (Global)"
else
return SettingsData.notepadFontFamily
}
enableFuzzySearch: true
onValueChanged: value => {
if (value && (value.startsWith("Default") || value === "Default (Global)")) {
SettingsData.notepadFontFamily = ""
} else {
SettingsData.notepadFontFamily = value
}
}
}
}
Rectangle {
width: parent.width
height: fontSizeRow.height + Theme.spacingS
color: "transparent"
Row {
id: fontSizeRow
width: parent.width
spacing: Theme.spacingS
Column {
width: parent.width - fontSizeControls.width - Theme.spacingM
spacing: Theme.spacingXS
StyledText {
text: "Font Size"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: SettingsData.notepadFontSize + "px"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
}
}
Row {
id: fontSizeControls
spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
DankActionButton {
buttonSize: 32
iconName: "remove"
iconSize: Theme.iconSizeSmall
enabled: SettingsData.notepadFontSize > 8
backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
iconColor: Theme.surfaceText
onClicked: {
var newSize = Math.max(8, SettingsData.notepadFontSize - 1)
SettingsData.notepadFontSize = newSize
}
}
Rectangle {
width: 60
height: 32
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1
StyledText {
anchors.centerIn: parent
text: SettingsData.notepadFontSize + "px"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
}
DankActionButton {
buttonSize: 32
iconName: "add"
iconSize: Theme.iconSizeSmall
enabled: SettingsData.notepadFontSize < 48
backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
iconColor: Theme.surfaceText
onClicked: {
var newSize = Math.min(48, SettingsData.notepadFontSize + 1)
SettingsData.notepadFontSize = newSize
}
}
}
}
}
StyledText {
width: parent.width
text: SettingsData.notepadUseMonospace ?
"Using global monospace font from Settings → Personalization" :
"Global fonts can be configured in Settings → Personalization"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
wrapMode: Text.WordWrap
opacity: 0.8
}
}
}
}

View File

@@ -0,0 +1,153 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
pragma ComponentBehavior: Bound
Column {
id: root
property var currentTab: NotepadStorageService.tabs.length > NotepadStorageService.currentTabIndex ? NotepadStorageService.tabs[NotepadStorageService.currentTabIndex] : null
property bool contentLoaded: false
signal tabSwitched(int tabIndex)
signal tabClosed(int tabIndex)
signal newTabRequested()
function hasUnsavedChangesForTab(tab) {
if (!tab) return false
if (tab.id === currentTab?.id) {
return root.parent?.hasUnsavedChanges ? root.parent.hasUnsavedChanges() : false
}
return false
}
spacing: Theme.spacingXS
Row {
width: parent.width
height: 36
spacing: Theme.spacingXS
ScrollView {
width: parent.width - newTabButton.width - Theme.spacingXS
height: parent.height
clip: true
ScrollBar.horizontal.visible: false
ScrollBar.vertical.visible: false
Row {
spacing: Theme.spacingXS
Repeater {
model: NotepadStorageService.tabs
delegate: Rectangle {
required property int index
required property var modelData
readonly property bool isActive: NotepadStorageService.currentTabIndex === index
readonly property bool isHovered: tabMouseArea.containsMouse && !closeMouseArea.containsMouse
readonly property real calculatedWidth: {
const textWidth = tabText.paintedWidth || 100
const closeButtonWidth = NotepadStorageService.tabs.length > 1 ? 20 : 0
const spacing = Theme.spacingXS
const padding = Theme.spacingM * 2
return Math.max(120, Math.min(200, textWidth + closeButtonWidth + spacing + padding))
}
width: calculatedWidth
height: 32
radius: Theme.cornerRadius
color: isActive ? Theme.primaryPressed : isHovered ? Theme.primaryHoverLight : "transparent"
border.width: isActive ? 0 : 1
border.color: Theme.outlineMedium
MouseArea {
id: tabMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onClicked: root.tabSwitched(index)
}
Row {
id: tabContent
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
id: tabText
text: {
var prefix = ""
if (hasUnsavedChangesForTab(modelData)) {
prefix = "● "
}
return prefix + (modelData.title || "Untitled")
}
font.pixelSize: Theme.fontSizeSmall
color: isActive ? Theme.primary : Theme.surfaceText
font.weight: isActive ? Font.Medium : Font.Normal
elide: Text.ElideMiddle
maximumLineCount: 1
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
id: tabCloseButton
width: 20
height: 20
radius: 10
color: closeMouseArea.containsMouse ? Theme.surfaceTextHover : "transparent"
visible: NotepadStorageService.tabs.length > 1
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "close"
size: 14
color: Theme.surfaceTextMedium
anchors.centerIn: parent
}
MouseArea {
id: closeMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
z: 100
onClicked: {
root.tabClosed(index)
}
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
DankActionButton {
id: newTabButton
width: 32
height: 32
iconName: "add"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: root.newTabRequested()
}
}
}

View File

@@ -0,0 +1,291 @@
import QtQuick
import QtQuick.Controls
import Quickshell.Io
import qs.Common
import qs.Services
import qs.Widgets
pragma ComponentBehavior: Bound
Column {
id: root
property alias text: textArea.text
property alias textArea: textArea
property bool contentLoaded: false
property string lastSavedContent: ""
property var currentTab: NotepadStorageService.tabs.length > NotepadStorageService.currentTabIndex ? NotepadStorageService.tabs[NotepadStorageService.currentTabIndex] : null
signal saveRequested()
signal openRequested()
signal newRequested()
signal escapePressed()
signal contentChanged()
signal settingsRequested()
function hasUnsavedChanges() {
if (!currentTab || !contentLoaded) {
return false
}
if (currentTab.isTemporary) {
return textArea.text.length > 0
}
return textArea.text !== lastSavedContent
}
function loadCurrentTabContent() {
if (!currentTab) return
contentLoaded = false
NotepadStorageService.loadTabContent(
NotepadStorageService.currentTabIndex,
(content) => {
lastSavedContent = content
textArea.text = content
contentLoaded = true
}
)
}
function saveCurrentTabContent() {
if (!currentTab || !contentLoaded) return
NotepadStorageService.saveTabContent(
NotepadStorageService.currentTabIndex,
textArea.text
)
lastSavedContent = textArea.text
}
function autoSaveToSession() {
if (!currentTab || !contentLoaded) return
saveCurrentTabContent()
}
spacing: Theme.spacingM
StyledRect {
width: parent.width
height: parent.height - bottomControls.height - Theme.spacingM
color: Theme.surface
border.color: Theme.outlineMedium
border.width: 1
radius: Theme.cornerRadius
ScrollView {
anchors.fill: parent
anchors.margins: 1
clip: true
TextArea {
id: textArea
placeholderText: qsTr("Start typing your notes here...")
font.family: SettingsData.notepadUseMonospace ? SettingsData.monoFontFamily : (SettingsData.notepadFontFamily || SettingsData.fontFamily)
font.pixelSize: SettingsData.notepadFontSize * SettingsData.fontScale
color: Theme.surfaceText
selectByMouse: true
selectByKeyboard: true
wrapMode: TextArea.Wrap
focus: true
activeFocusOnTab: true
textFormat: TextEdit.PlainText
inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase
persistentSelection: true
tabStopDistance: 40
leftPadding: Theme.spacingM
topPadding: Theme.spacingM
rightPadding: Theme.spacingM
bottomPadding: Theme.spacingM
Component.onCompleted: {
loadCurrentTabContent()
}
Connections {
target: NotepadStorageService
function onCurrentTabIndexChanged() {
loadCurrentTabContent()
}
function onTabsChanged() {
if (NotepadStorageService.tabs.length > 0 && !contentLoaded) {
loadCurrentTabContent()
}
}
}
onTextChanged: {
if (contentLoaded && text !== lastSavedContent) {
autoSaveTimer.restart()
}
root.contentChanged()
}
Keys.onEscapePressed: (event) => {
root.escapePressed()
event.accepted = true
}
Keys.onPressed: (event) => {
if (event.modifiers & Qt.ControlModifier) {
switch (event.key) {
case Qt.Key_S:
event.accepted = true
root.saveRequested()
break
case Qt.Key_O:
event.accepted = true
root.openRequested()
break
case Qt.Key_N:
event.accepted = true
root.newRequested()
break
case Qt.Key_A:
event.accepted = true
selectAll()
break
}
}
}
background: Rectangle {
color: "transparent"
}
}
}
}
Column {
id: bottomControls
width: parent.width
spacing: Theme.spacingS
Item {
width: parent.width
height: 32
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingL
Row {
spacing: Theme.spacingS
DankActionButton {
iconName: "save"
iconSize: Theme.iconSize - 2
iconColor: Theme.primary
enabled: currentTab && (hasUnsavedChanges() || textArea.text.length > 0)
onClicked: root.saveRequested()
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Save")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
}
}
Row {
spacing: Theme.spacingS
DankActionButton {
iconName: "folder_open"
iconSize: Theme.iconSize - 2
iconColor: Theme.secondary
onClicked: root.openRequested()
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Open")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
}
}
Row {
spacing: Theme.spacingS
DankActionButton {
iconName: "note_add"
iconSize: Theme.iconSize - 2
iconColor: Theme.surfaceText
onClicked: root.newRequested()
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: qsTr("New")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
}
}
}
DankActionButton {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
iconName: "more_horiz"
iconSize: Theme.iconSize - 2
iconColor: Theme.surfaceText
onClicked: root.settingsRequested()
}
}
Row {
width: parent.width
spacing: Theme.spacingL
StyledText {
text: textArea.text.length > 0 ? qsTr("%1 characters").arg(textArea.text.length) : qsTr("Empty")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
}
StyledText {
text: qsTr("Lines: %1").arg(textArea.lineCount)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
visible: textArea.text.length > 0
}
StyledText {
text: {
if (autoSaveTimer.running) {
return qsTr("Auto-saving...")
}
if (hasUnsavedChanges()) {
if (currentTab && currentTab.isTemporary) {
return qsTr("Unsaved note...")
} else {
return qsTr("Unsaved changes")
}
} else {
return qsTr("Saved")
}
}
font.pixelSize: Theme.fontSizeSmall
color: {
if (autoSaveTimer.running) {
return Theme.primary
}
if (hasUnsavedChanges()) {
return Theme.warning
} else {
return Theme.success
}
}
opacity: textArea.text.length > 0 ? 1 : 0
}
}
}
Timer {
id: autoSaveTimer
interval: 2000
repeat: false
onTriggered: {
autoSaveToSession()
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -35,9 +35,9 @@ PanelWindow {
if (!notepadVariants || !notepadVariants.instances) return null if (!notepadVariants || !notepadVariants.instances) return null
for (var i = 0; i < notepadVariants.instances.length; i++) { for (var i = 0; i < notepadVariants.instances.length; i++) {
var loader = notepadVariants.instances[i] var slideout = notepadVariants.instances[i]
if (loader.modelData && loader.modelData.name === root.screen?.name) { if (slideout.modelData && slideout.modelData.name === root.screen?.name) {
return loader.ensureLoaded() return slideout
} }
} }
return null return null
@@ -157,7 +157,7 @@ PanelWindow {
} }
property var notepadInstance: null property var notepadInstance: null
property bool notepadInstanceVisible: notepadInstance?.notepadVisible ?? false property bool notepadInstanceVisible: notepadInstance?.isVisible ?? false
readonly property bool hasActivePopout: { readonly property bool hasActivePopout: {
const loaders = [{ const loaders = [{
@@ -1065,7 +1065,7 @@ PanelWindow {
NotepadButton { NotepadButton {
property var notepadInstance: topBarCore.notepadInstance property var notepadInstance: topBarCore.notepadInstance
isActive: notepadInstance?.notepadVisible ?? false isActive: notepadInstance?.isVisible ?? false
widgetHeight: root.widgetHeight widgetHeight: root.widgetHeight
barHeight: root.effectiveBarHeight barHeight: root.effectiveBarHeight
section: topBarContent.getWidgetSection(parent) || "right" section: topBarContent.getWidgetSection(parent) || "right"

175
Widgets/DankSlideout.qml Normal file
View File

@@ -0,0 +1,175 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Widgets
pragma ComponentBehavior: Bound
PanelWindow {
id: root
property bool isVisible: false
property var targetScreen: null
property var modelData: null
property real slideoutWidth: 480
property bool expandable: false
property bool expandedWidth: false
property real expandedWidthValue: 960
property Component content: null
property string title: ""
property alias container: contentContainer
function show() {
visible = true
isVisible = true
}
function hide() {
isVisible = false
}
function toggle() {
if (isVisible) {
hide()
} else {
show()
}
}
visible: isVisible
screen: modelData
anchors.top: true
anchors.bottom: true
anchors.right: true
implicitWidth: expandable ? expandedWidthValue : slideoutWidth
implicitHeight: modelData ? modelData.height : 800
color: "transparent"
WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.exclusiveZone: 0
WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
StyledRect {
id: contentRect
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
width: expandable && expandedWidth ? expandedWidthValue : slideoutWidth
color: Theme.surfaceContainer
border.color: Theme.outlineMedium
border.width: 1
opacity: isVisible ? SettingsData.popupTransparency : 0
Behavior on opacity {
NumberAnimation {
duration: 700
easing.type: Easing.OutCubic
}
}
transform: Translate {
id: slideTransform
x: isVisible ? 0 : contentRect.width
Behavior on x {
NumberAnimation {
id: slideAnimation
duration: 450
easing.type: Easing.OutCubic
onRunningChanged: {
if (!running && !isVisible) {
root.visible = false
}
}
}
}
}
Behavior on width {
NumberAnimation {
duration: 250
easing.type: Easing.OutCubic
}
}
Column {
id: headerColumn
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
visible: root.title !== ""
Row {
width: parent.width
height: 32
Column {
width: parent.width - buttonRow.width
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: root.title
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
}
Row {
id: buttonRow
spacing: Theme.spacingXS
DankActionButton {
id: expandButton
iconName: root.expandedWidth ? "unfold_less" : "unfold_more"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
visible: root.expandable
onClicked: root.expandedWidth = !root.expandedWidth
transform: Rotation {
angle: 90
origin.x: expandButton.width / 2
origin.y: expandButton.height / 2
}
}
DankActionButton {
id: closeButton
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: root.hide()
}
}
}
}
Item {
id: contentContainer
anchors.top: root.title !== "" ? headerColumn.bottom : parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.topMargin: root.title !== "" ? 0 : Theme.spacingL
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
anchors.bottomMargin: Theme.spacingL
Loader {
anchors.fill: parent
sourceComponent: root.content
}
}
}
}

View File

@@ -18,6 +18,8 @@ import qs.Modules.ControlCenter
import qs.Modules.Dock import qs.Modules.Dock
import qs.Modules.Lock import qs.Modules.Lock
import qs.Modules.Notifications.Center import qs.Modules.Notifications.Center
import qs.Widgets
import "./Modules/Notepad"
import qs.Modules.Notifications.Popup import qs.Modules.Notifications.Popup
import qs.Modules.OSD import qs.Modules.OSD
import qs.Modules.ProcessList import qs.Modules.ProcessList
@@ -287,29 +289,28 @@ ShellRoot {
id: notepadSlideoutVariants id: notepadSlideoutVariants
model: SettingsData.getFilteredScreens("notepad") model: SettingsData.getFilteredScreens("notepad")
delegate: Loader { delegate: DankSlideout {
id: notepadLoader id: notepadSlideout
property var modelData: item modelData: item
active: false title: qsTr("Notepad")
slideoutWidth: 480
expandable: true
expandedWidthValue: 960
sourceComponent: Component { content: Component {
NotepadSlideout { Notepad {
id: notepadSlideout onHideRequested: {
modelData: notepadLoader.modelData notepadSlideout.hide()
Component.onCompleted: {
notepadLoader.loaded = true
} }
} }
} }
property bool loaded: false function toggle() {
if (isVisible) {
function ensureLoaded() { hide()
if (!active) { } else {
active = true show()
} }
return item
} }
} }
} }
@@ -476,28 +477,27 @@ ShellRoot {
} }
if (notepadSlideoutVariants.instances.length === 1) { if (notepadSlideoutVariants.instances.length === 1) {
return notepadSlideoutVariants.instances[0].ensureLoaded() return notepadSlideoutVariants.instances[0]
} }
var focusedScreen = getFocusedScreenName() var focusedScreen = getFocusedScreenName()
if (focusedScreen && notepadSlideoutVariants.instances.length > 0) { if (focusedScreen && notepadSlideoutVariants.instances.length > 0) {
for (var i = 0; i < notepadSlideoutVariants.instances.length; i++) { for (var i = 0; i < notepadSlideoutVariants.instances.length; i++) {
var loader = notepadSlideoutVariants.instances[i] var slideout = notepadSlideoutVariants.instances[i]
if (loader.modelData && loader.modelData.name === focusedScreen) { if (slideout.modelData && slideout.modelData.name === focusedScreen) {
loader.ensureLoaded() return slideout
return loader.item
} }
} }
} }
for (var i = 0; i < notepadSlideoutVariants.instances.length; i++) { for (var i = 0; i < notepadSlideoutVariants.instances.length; i++) {
var loader = notepadSlideoutVariants.instances[i] var slideout = notepadSlideoutVariants.instances[i]
if (loader.active && loader.item && loader.item.notepadVisible) { if (slideout.isVisible) {
return loader.item return slideout
} }
} }
return notepadSlideoutVariants.instances[0].ensureLoaded() return notepadSlideoutVariants.instances[0]
} }
function open(): string { function open(): string {