1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-16 02:12:51 -05:00

switch hto monorepo structure

This commit is contained in:
bbedward
2025-11-12 17:18:45 -05:00
parent 6013c994a6
commit 24e800501a
768 changed files with 76284 additions and 221 deletions

View File

@@ -0,0 +1,513 @@
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()
Ref {
service: NotepadStorageService
}
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
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) => {
}
}
FileBrowserModal {
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"
}
}
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: I18n.tr("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: 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
}
}
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: 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
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: 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
saveBrowser.open()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,378 @@
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()
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 === 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: 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
}
}
}
}

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 : Theme.withAlpha(Theme.primaryPressed, 0)
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: 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
}
}
}
}
}
}
DankActionButton {
id: newTabButton
width: 32
height: 32
iconName: "add"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: root.newTabRequested()
}
}
}

View File

@@ -0,0 +1,619 @@
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
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
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()
}
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 hideSearch() {
searchVisible = false
searchQuery = ""
searchMatches = []
matchCount = 0
currentMatchIndex = -1
textArea.select(0, 0)
textArea.forceActiveFocus()
}
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()
}
// Close button
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
DankFlickable {
id: flickable
anchors.fill: parent
anchors.margins: 1
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: I18n.tr("Start typing your notes here...")
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
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()
}
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
case Qt.Key_F:
event.accepted = true
root.showSearch()
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: 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
}
}
}
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 ? I18n.tr("%1 characters").arg(textArea.text.length) : I18n.tr("Empty")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
}
StyledText {
text: 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()
}
}
}