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

Compare commits

...

6 Commits

Author SHA1 Message Date
purian23 1df7e478df fix(FileBrowser): Improve save-to-file handling w/safety override diags
Fixes #2641
2026-06-15 01:11:32 -04:00
Artrix 1fc4890857 tray: add automatic overflow popup (#2629) 2026-06-15 00:10:02 -04:00
purian23 f5d52f1506 update(ipc): docs & dms ipc list 2026-06-14 23:35:22 -04:00
purian23 2026ba5bd2 fix(Notepad): clean up edge cases & updated popout handling 2026-06-14 22:15:34 -04:00
purian23 db56c8d74d 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
2026-06-14 20:20:36 -04:00
Huỳnh Thiện Lộc 9d1a81c93c feat(media-control): support scroll and right-click on audio output devices in media popout (#2615)
* feat(media-control): support scroll and right-click on audio output devices in media popout

* feat(media-control): make device list volume scrolling optional
2026-06-13 17:51:03 -04:00
34 changed files with 2110 additions and 523 deletions
+9 -1
View File
@@ -19,7 +19,12 @@ var (
var colorCmd = &cobra.Command{ var colorCmd = &cobra.Command{
Use: "color", Use: "color",
Short: "Color utilities", Short: "Color utilities",
Long: "Color utilities including picking colors from the screen", Long: `Color utilities including picking colors from the screen.
This is the screen eyedropper CLI. To open the in-shell color modal, use:
dms ipc call color-picker toggle
See: https://danklinux.com/docs/dankmaterialshell/keybinds-ipc`,
} }
var colorPickCmd = &cobra.Command{ var colorPickCmd = &cobra.Command{
@@ -29,6 +34,9 @@ var colorPickCmd = &cobra.Command{
Click on any pixel to capture its color, or press Escape to cancel. Click on any pixel to capture its color, or press Escape to cancel.
This is the screen eyedropper CLI. To open the in-shell color modal, use:
dms ipc call color-picker toggle
Output format flags (mutually exclusive, default: --hex): Output format flags (mutually exclusive, default: --hex):
--hex - Hexadecimal (#RRGGBB) --hex - Hexadecimal (#RRGGBB)
--rgb - RGB values (R G B) --rgb - RGB values (R G B)
+16 -3
View File
@@ -77,10 +77,15 @@ var killCmd = &cobra.Command{
} }
var ipcCmd = &cobra.Command{ var ipcCmd = &cobra.Command{
Use: "ipc [target] [function] [args...]", Use: "ipc",
Short: "Send IPC commands to running DMS shell", Short: "Send IPC commands to running DMS shell",
Long: `Send IPC commands to the running DMS shell.
dms ipc call <target> <function> [args...] invoke a command
dms ipc list list all targets and functions
Full reference: https://danklinux.com/docs/dankmaterialshell/keybinds-ipc`,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
_ = findConfig(cmd, args)
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
}, },
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
@@ -88,9 +93,17 @@ var ipcCmd = &cobra.Command{
}, },
} }
var ipcListCmd = &cobra.Command{
Use: "list",
Short: "List all IPC targets and functions",
Run: func(cmd *cobra.Command, args []string) {
printIPCHelp()
},
}
func init() { func init() {
ipcCmd.AddCommand(ipcListCmd)
ipcCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { ipcCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
_ = findConfig(cmd, args)
printIPCHelp() printIPCHelp()
}) })
} }
+44 -27
View File
@@ -601,12 +601,30 @@ func parseTargetsFromIPCShowOutput(output string) ipcTargets {
return targets return targets
} }
func getShellIPCCompletions(args []string, _ string) []string { func buildQsIPCBaseArgs() ([]string, error) {
cmdArgs := []string{"ipc"} cmdArgs := []string{"ipc"}
if qsHasAnyDisplay() { switch pid, ok := getFirstDMSPID(); {
cmdArgs = append(cmdArgs, "--any-display") case ok:
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
default:
if err := findConfig(nil, nil); err != nil {
return nil, err
}
if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display")
}
cmdArgs = append(cmdArgs, "-p", configPath)
} }
cmdArgs = append(cmdArgs, "-p", configPath, "show") return cmdArgs, nil
}
func getShellIPCCompletions(args []string, _ string) []string {
baseArgs, err := buildQsIPCBaseArgs()
if err != nil {
log.Debugf("Error building IPC args for completions: %v", err)
return nil
}
cmdArgs := append(baseArgs, "show")
cmd := exec.Command("qs", cmdArgs...) cmd := exec.Command("qs", cmdArgs...)
var targets ipcTargets var targets ipcTargets
@@ -623,7 +641,7 @@ func getShellIPCCompletions(args []string, _ string) []string {
if len(args) == 0 { if len(args) == 0 {
targetNames := make([]string, 0) targetNames := make([]string, 0)
targetNames = append(targetNames, "call") targetNames = append(targetNames, "call", "list")
for k := range targets { for k := range targets {
targetNames = append(targetNames, k) targetNames = append(targetNames, k)
} }
@@ -696,23 +714,11 @@ func runShellIPCCommand(args []string) {
args = append([]string{"call"}, args...) args = append([]string{"call"}, args...)
} }
cmdArgs := []string{"ipc"} baseArgs, err := buildQsIPCBaseArgs()
if err != nil {
switch pid, ok := getFirstDMSPID(); { log.Fatalf("Error finding config: %v", err)
case ok:
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
default:
if err := findConfig(nil, nil); err != nil {
log.Fatalf("Error finding config: %v", err)
}
// ! TODO - remove check when QS 0.3 is released
if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display")
}
cmdArgs = append(cmdArgs, "-p", configPath)
} }
cmdArgs := append(baseArgs, args...)
cmdArgs = append(cmdArgs, args...)
cmd := exec.Command("qs", cmdArgs...) cmd := exec.Command("qs", cmdArgs...)
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
@@ -724,19 +730,20 @@ func runShellIPCCommand(args []string) {
} }
func printIPCHelp() { func printIPCHelp() {
fmt.Println("Usage: dms ipc <target> <function> [args...]") fmt.Println("Usage: dms ipc call <target> <function> [args...]")
fmt.Println() fmt.Println()
cmdArgs := []string{"ipc"} baseArgs, err := buildQsIPCBaseArgs()
if qsHasAnyDisplay() { if err != nil {
cmdArgs = append(cmdArgs, "--any-display") printIPCHelpFailure(err)
return
} }
cmdArgs = append(cmdArgs, "-p", configPath, "show") cmdArgs := append(baseArgs, "show")
cmd := exec.Command("qs", cmdArgs...) cmd := exec.Command("qs", cmdArgs...)
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
fmt.Println("Could not retrieve available IPC targets (is DMS running?)") printIPCHelpFailure(err)
return return
} }
@@ -765,6 +772,16 @@ func printIPCHelp() {
} }
} }
func printIPCHelpFailure(err error) {
fmt.Println("Could not retrieve IPC targets.")
if err != nil {
fmt.Printf(" %v\n", err)
}
fmt.Println()
fmt.Println(" Full docs: https://danklinux.com/docs/dankmaterialshell/keybinds-ipc")
fmt.Println(" Try: dms ipc call <target> <function>")
}
// ensureFontCache rebuilds the fontconfig cache if user-configured fonts are missing while skipping defaults // ensureFontCache rebuilds the fontconfig cache if user-configured fonts are missing while skipping defaults
func ensureFontCache() { func ensureFontCache() {
if _, err := exec.LookPath("fc-list"); err != nil { if _, err := exec.LookPath("fc-list"); err != nil {
+21 -1
View File
@@ -6,6 +6,18 @@ DankMaterialShell provides comprehensive IPC (Inter-Process Communication) funct
dms ipc call <target> <function> [parameters...] dms ipc call <target> <function> [parameters...]
``` ```
## Discovering IPC commands
List all available targets and functions while DMS is running:
```bash
dms ipc list
dms ipc # same
dms ipc --help # same, plus usage text
```
Live listing requires DMS to be running. If listing fails, use this document or the [Keybinds & IPC docs](https://danklinux.com/docs/dankmaterialshell/keybinds-ipc) as an offline reference.
## Target: `audio` ## Target: `audio`
Audio system control and information. Audio system control and information.
@@ -707,7 +719,7 @@ File browser controls for selecting wallpapers and profile images.
- Both browsers support common image formats (jpg, jpeg, png, bmp, gif, webp) - Both browsers support common image formats (jpg, jpeg, png, bmp, gif, webp)
### Target: `color-picker` ### Target: `color-picker`
Color picker modal control. In-shell color picker modal for theme and settings color selection.
**Functions:** **Functions:**
- `open` - Show color picker modal - `open` - Show color picker modal
@@ -718,6 +730,14 @@ Color picker modal control.
- `toggle` - Toggle color picker modal visibility - `toggle` - Toggle color picker modal visibility
- `toggleInstant` - Toggle color picker modal visibility without animation on hide - `toggleInstant` - Toggle color picker modal visibility without animation on hide
**Note:** This controls the in-shell modal. To pick a pixel from the screen via CLI, use `dms color pick` instead (see [Color Picker CLI](https://danklinux.com/docs/dankmaterialshell/cli-color-picker)).
**Examples:**
```bash
dms ipc call color-picker toggle
dms ipc call color-picker openColor "#3f51b5"
```
### Target: `hypr` ### Target: `hypr`
Hyprland-specific controls including keybinds cheatsheet and workspace overview (Hyprland only). Hyprland-specific controls including keybinds cheatsheet and workspace overview (Hyprland only).
+3
View File
@@ -56,6 +56,9 @@ const DMS_ACTIONS = [
{ id: "spawn dms ipc call dankdash wallpaper", label: "Wallpaper Browser" }, { id: "spawn dms ipc call dankdash wallpaper", label: "Wallpaper Browser" },
{ id: "spawn dms ipc call file browse wallpaper", label: "File: Browse Wallpaper" }, { id: "spawn dms ipc call file browse wallpaper", label: "File: Browse Wallpaper" },
{ id: "spawn dms ipc call file browse profile", label: "File: Browse Profile" }, { id: "spawn dms ipc call file browse profile", label: "File: Browse Profile" },
{ id: "spawn dms ipc call color-picker toggle", label: "Color Picker: Toggle" },
{ id: "spawn dms ipc call color-picker open", label: "Color Picker: Open" },
{ id: "spawn dms ipc call color-picker close", label: "Color Picker: Close" },
{ id: "spawn dms ipc call keybinds toggle niri", label: "Keybinds Cheatsheet: Toggle", compositor: "niri" }, { id: "spawn dms ipc call keybinds toggle niri", label: "Keybinds Cheatsheet: Toggle", compositor: "niri" },
{ id: "spawn dms ipc call keybinds open niri", label: "Keybinds Cheatsheet: Open", compositor: "niri" }, { id: "spawn dms ipc call keybinds open niri", label: "Keybinds Cheatsheet: Open", compositor: "niri" },
{ id: "spawn dms ipc call keybinds close", label: "Keybinds Cheatsheet: Close" }, { id: "spawn dms ipc call keybinds close", label: "Keybinds Cheatsheet: Close" },
+30
View File
@@ -397,6 +397,7 @@ Singleton {
property bool audioVisualizerEnabled: true property bool audioVisualizerEnabled: true
property string audioScrollMode: "volume" property string audioScrollMode: "volume"
property int audioWheelScrollAmount: 5 property int audioWheelScrollAmount: 5
property bool audioDeviceScrollVolumeEnabled: false
property bool clockCompactMode: false property bool clockCompactMode: false
property int focusedWindowSize: 1 property int focusedWindowSize: 1
property bool focusedWindowCompactMode: false property bool focusedWindowCompactMode: false
@@ -404,6 +405,9 @@ Singleton {
property int barMaxVisibleApps: 0 property int barMaxVisibleApps: 0
property int barMaxVisibleRunningApps: 0 property int barMaxVisibleRunningApps: 0
property bool barShowOverflowBadge: true property bool barShowOverflowBadge: true
property bool trayAutoOverflow: true
property bool trayPopupSingleLine: true
property int trayMaxVisibleItems: 0
property bool appsDockHideIndicators: false property bool appsDockHideIndicators: false
property bool appsDockColorizeActive: false property bool appsDockColorizeActive: false
property string appsDockActiveColorMode: "primary" property string appsDockActiveColorMode: "primary"
@@ -519,13 +523,39 @@ Singleton {
property real notificationSummaryFontSize: Spec.SPEC.notificationSummaryFontSize.def property real notificationSummaryFontSize: Spec.SPEC.notificationSummaryFontSize.def
property real notificationBodyFontSize: Spec.SPEC.notificationBodyFontSize.def property real notificationBodyFontSize: Spec.SPEC.notificationBodyFontSize.def
property bool notepadShowLineNumbers: false property bool notepadShowLineNumbers: false
property bool notepadAutoSave: false
property string notepadSlideoutSide: "right"
property string notepadDefaultMode: "slideout"
property real notepadTransparencyOverride: -1 property real notepadTransparencyOverride: -1
property real notepadLastCustomTransparency: 0.7 property real notepadLastCustomTransparency: 0.7
property bool notepadUseCompositorGap: false
property int notepadEdgeGap: 0
// Compositor layout gap when enabled and available, else the manual value.
readonly property int notepadEffectiveEdgeGap: {
if (notepadUseCompositorGap) {
var g = -1;
if (CompositorService.isNiri)
g = niriLayoutGapsOverride;
else if (CompositorService.isHyprland)
g = hyprlandLayoutGapsOverride;
else if (CompositorService.isMango)
g = mangoLayoutGapsOverride;
if (g >= 0)
return g;
}
return Math.max(0, notepadEdgeGap);
}
onNotepadUseMonospaceChanged: saveSettings() onNotepadUseMonospaceChanged: saveSettings()
onNotepadFontFamilyChanged: saveSettings() onNotepadFontFamilyChanged: saveSettings()
onNotepadFontSizeChanged: saveSettings() onNotepadFontSizeChanged: saveSettings()
onNotepadShowLineNumbersChanged: saveSettings() onNotepadShowLineNumbersChanged: saveSettings()
onNotepadAutoSaveChanged: saveSettings()
onNotepadSlideoutSideChanged: saveSettings()
onNotepadDefaultModeChanged: saveSettings()
onNotepadUseCompositorGapChanged: saveSettings()
onNotepadEdgeGapChanged: saveSettings()
// onCenteringModeChanged: saveSettings() // onCenteringModeChanged: saveSettings()
onNotepadTransparencyOverrideChanged: { onNotepadTransparencyOverrideChanged: {
if (notepadTransparencyOverride > 0) { if (notepadTransparencyOverride > 0) {
@@ -156,6 +156,7 @@ var SPEC = {
audioVisualizerEnabled: { def: true }, audioVisualizerEnabled: { def: true },
audioScrollMode: { def: "volume" }, audioScrollMode: { def: "volume" },
audioWheelScrollAmount: { def: 5 }, audioWheelScrollAmount: { def: 5 },
audioDeviceScrollVolumeEnabled: { def: false },
clockCompactMode: { def: false }, clockCompactMode: { def: false },
focusedWindowCompactMode: { def: false }, focusedWindowCompactMode: { def: false },
focusedWindowSize: { def: 1 }, focusedWindowSize: { def: 1 },
@@ -163,6 +164,9 @@ var SPEC = {
barMaxVisibleApps: { def: 0 }, barMaxVisibleApps: { def: 0 },
barMaxVisibleRunningApps: { def: 0 }, barMaxVisibleRunningApps: { def: 0 },
barShowOverflowBadge: { def: true }, barShowOverflowBadge: { def: true },
trayAutoOverflow: { def: true },
trayPopupSingleLine: { def: true },
trayMaxVisibleItems: { def: 0 },
appsDockHideIndicators: { def: false }, appsDockHideIndicators: { def: false },
appsDockColorizeActive: { def: false }, appsDockColorizeActive: { def: false },
appsDockActiveColorMode: { def: "primary" }, appsDockActiveColorMode: { def: "primary" },
@@ -263,8 +267,13 @@ var SPEC = {
notificationSummaryFontSize: { def: 0 }, notificationSummaryFontSize: { def: 0 },
notificationBodyFontSize: { def: 0 }, notificationBodyFontSize: { def: 0 },
notepadShowLineNumbers: { def: false }, notepadShowLineNumbers: { def: false },
notepadAutoSave: { def: false },
notepadSlideoutSide: { def: "right" },
notepadDefaultMode: { def: "slideout" },
notepadTransparencyOverride: { def: -1 }, notepadTransparencyOverride: { def: -1 },
notepadLastCustomTransparency: { def: 0.7 }, notepadLastCustomTransparency: { def: 0.7 },
notepadUseCompositorGap: { def: false },
notepadEdgeGap: { def: 0 },
soundsEnabled: { def: true }, soundsEnabled: { def: true },
useSystemSoundTheme: { def: false }, useSystemSoundTheme: { def: false },
+29
View File
@@ -1093,11 +1093,22 @@ Item {
slideoutWidth: 480 slideoutWidth: 480
expandable: true expandable: true
expandedWidthValue: 960 expandedWidthValue: 960
edgeGap: SettingsData.notepadEffectiveEdgeGap
slideEdge: SettingsData.notepadSlideoutSide
onIsVisibleChanged: {
if (isVisible)
PopoutService.notepadPopout?.hide();
}
content: Component { content: Component {
Notepad { Notepad {
slideout: notepadSlideout slideout: notepadSlideout
onHideRequested: notepadSlideout.hide() onHideRequested: notepadSlideout.hide()
onPopoutRequested: {
notepadSlideout.hide();
PopoutService.openNotepadPopout();
}
} }
} }
@@ -1114,6 +1125,24 @@ Item {
Component.onCompleted: PopoutService.notepadSlideouts = instances Component.onCompleted: PopoutService.notepadSlideouts = instances
} }
LazyLoader {
id: notepadPopoutLoader
active: false
Component.onCompleted: {
PopoutService.notepadPopoutLoader = notepadPopoutLoader;
}
onActiveChanged: {
if (active && item) {
PopoutService.notepadPopout = item;
PopoutService._onNotepadPopoutLoaded();
}
}
NotepadPopoutWindow {}
}
LazyLoader { LazyLoader {
id: powerMenuModalLoader id: powerMenuModalLoader
+12
View File
@@ -373,6 +373,10 @@ Item {
} }
function open(): string { function open(): string {
if (SettingsData.notepadDefaultMode === "popout") {
PopoutService.openNotepadPopout();
return "NOTEPAD_OPEN_SUCCESS";
}
var instance = getActiveNotepadInstance(); var instance = getActiveNotepadInstance();
if (instance) { if (instance) {
instance.show(); instance.show();
@@ -382,6 +386,10 @@ Item {
} }
function close(): string { function close(): string {
if (SettingsData.notepadDefaultMode === "popout") {
PopoutService.notepadPopout?.hide();
return "NOTEPAD_CLOSE_SUCCESS";
}
var instance = getActiveNotepadInstance(); var instance = getActiveNotepadInstance();
if (instance) { if (instance) {
instance.hide(); instance.hide();
@@ -391,6 +399,10 @@ Item {
} }
function toggle(): string { function toggle(): string {
if (SettingsData.notepadDefaultMode === "popout") {
PopoutService.toggleNotepadPopout();
return "NOTEPAD_TOGGLE_SUCCESS";
}
var instance = getActiveNotepadInstance(); var instance = getActiveNotepadInstance();
if (instance) { if (instance) {
instance.toggle(); instance.toggle();
@@ -201,6 +201,21 @@ FocusScope {
keyboardSelectionRequested = true; keyboardSelectionRequested = true;
} }
function activateFile(path, name, isDir) {
if (isDir) {
navigateTo(path);
return;
}
if (saveMode) {
saveRow.fileName = name;
pendingFilePath = path;
showOverwriteConfirmation = true;
} else {
fileSelected(path);
closeRequested();
}
}
function handleSaveFile(filePath) { function handleSaveFile(filePath) {
var normalizedPath = filePath; var normalizedPath = filePath;
if (!normalizedPath.startsWith("file://")) { if (!normalizedPath.startsWith("file://")) {
@@ -652,6 +667,7 @@ FocusScope {
Row { Row {
anchors.fill: parent anchors.fill: parent
anchors.bottomMargin: root.saveMode ? 40 + Theme.spacingL * 2 : 0
spacing: 0 spacing: 0
Row { Row {
@@ -756,12 +772,7 @@ FocusScope {
onItemClicked: (index, path, name, isDir) => { onItemClicked: (index, path, name, isDir) => {
selectedIndex = index; selectedIndex = index;
setSelectedFileData(path, name, isDir); setSelectedFileData(path, name, isDir);
if (isDir) { root.activateFile(path, name, isDir);
navigateTo(path);
} else {
fileSelected(path);
root.closeRequested();
}
} }
onItemSelected: (index, path, name, isDir) => { onItemSelected: (index, path, name, isDir) => {
setSelectedFileData(path, name, isDir); setSelectedFileData(path, name, isDir);
@@ -776,12 +787,7 @@ FocusScope {
root.keyboardSelectionRequested = false; root.keyboardSelectionRequested = false;
selectedIndex = index; selectedIndex = index;
setSelectedFileData(filePath, fileName, fileIsDir); setSelectedFileData(filePath, fileName, fileIsDir);
if (fileIsDir) { root.activateFile(filePath, fileName, fileIsDir);
navigateTo(filePath);
} else {
fileSelected(filePath);
root.closeRequested();
}
} }
} }
@@ -817,12 +823,7 @@ FocusScope {
onItemClicked: (index, path, name, isDir) => { onItemClicked: (index, path, name, isDir) => {
selectedIndex = index; selectedIndex = index;
setSelectedFileData(path, name, isDir); setSelectedFileData(path, name, isDir);
if (isDir) { root.activateFile(path, name, isDir);
navigateTo(path);
} else {
fileSelected(path);
root.closeRequested();
}
} }
onItemSelected: (index, path, name, isDir) => { onItemSelected: (index, path, name, isDir) => {
setSelectedFileData(path, name, isDir); setSelectedFileData(path, name, isDir);
@@ -837,12 +838,7 @@ FocusScope {
root.keyboardSelectionRequested = false; root.keyboardSelectionRequested = false;
selectedIndex = index; selectedIndex = index;
setSelectedFileData(filePath, fileName, fileIsDir); setSelectedFileData(filePath, fileName, fileIsDir);
if (fileIsDir) { root.activateFile(filePath, fileName, fileIsDir);
navigateTo(filePath);
} else {
fileSelected(filePath);
root.closeRequested();
}
} }
} }
@@ -855,6 +851,7 @@ FocusScope {
} }
FileBrowserSaveRow { FileBrowserSaveRow {
id: saveRow
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
@@ -913,21 +910,21 @@ FocusScope {
} }
} }
} }
}
FileBrowserOverwriteDialog { FileBrowserOverwriteDialog {
anchors.fill: parent anchors.fill: parent
showDialog: showOverwriteConfirmation showDialog: showOverwriteConfirmation
pendingFilePath: root.pendingFilePath pendingFilePath: root.pendingFilePath
onConfirmed: filePath => { onConfirmed: filePath => {
showOverwriteConfirmation = false; showOverwriteConfirmation = false;
fileSelected(filePath); fileSelected(filePath);
pendingFilePath = ""; pendingFilePath = "";
Qt.callLater(() => root.closeRequested()); Qt.callLater(() => root.closeRequested());
} }
onCancelled: { onCancelled: {
showOverwriteConfirmation = false; showOverwriteConfirmation = false;
pendingFilePath = ""; pendingFilePath = "";
}
} }
} }
@@ -74,7 +74,7 @@ Item {
width: 80 width: 80
height: 36 height: 36
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: cancelArea.containsMouse ? Theme.surfaceVariantHover : Theme.surfaceVariant color: cancelArea.containsMouse ? Qt.lighter(Theme.surfaceVariant, 1.2) : Theme.surfaceVariant
border.color: Theme.outline border.color: Theme.outline
border.width: 1 border.width: 1
@@ -8,6 +8,7 @@ Row {
property bool saveMode: false property bool saveMode: false
property string defaultFileName: "" property string defaultFileName: ""
property string currentPath: "" property string currentPath: ""
property alias fileName: fileNameInput.text
signal saveRequested(string filePath) signal saveRequested(string filePath)
@@ -15,6 +15,7 @@ Item {
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null property var blurBarWindow: null
property real sectionAvailablePrimarySize: 0
property bool overrideAxisLayout: false property bool overrideAxisLayout: false
property bool forceVerticalLayout: false property bool forceVerticalLayout: false
@@ -359,6 +360,7 @@ Item {
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow blurBarWindow: root.blurBarWindow
sectionAvailablePrimarySize: root.sectionAvailablePrimarySize
isFirst: index === 0 isFirst: index === 0
isLast: index === centerRepeater.count - 1 isLast: index === centerRepeater.count - 1
sectionSpacing: parent.itemSpacing sectionSpacing: parent.itemSpacing
@@ -497,6 +497,7 @@ Item {
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
barSpacing: barConfig?.spacing ?? 4 barSpacing: barConfig?.spacing ?? 4
sectionAvailablePrimarySize: Math.max(1, hCenterSection.x > 0 ? hCenterSection.x : parent.width / 3)
} }
Binding { Binding {
@@ -529,6 +530,7 @@ Item {
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
barSpacing: barConfig?.spacing ?? 4 barSpacing: barConfig?.spacing ?? 4
sectionAvailablePrimarySize: Math.max(1, hCenterSection.x > 0 ? parent.width - (hCenterSection.x + hCenterSection.width) : parent.width / 3)
} }
Binding { Binding {
@@ -561,6 +563,7 @@ Item {
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
barSpacing: barConfig?.spacing ?? 4 barSpacing: barConfig?.spacing ?? 4
sectionAvailablePrimarySize: Math.max(1, hRightSection.x > 0 ? hRightSection.x - (hLeftSection.x + hLeftSection.width) : parent.width / 3)
} }
Binding { Binding {
@@ -600,6 +603,7 @@ Item {
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
barSpacing: barConfig?.spacing ?? 4 barSpacing: barConfig?.spacing ?? 4
sectionAvailablePrimarySize: Math.max(1, vCenterSection.y > 0 ? vCenterSection.y : parent.height / 3)
} }
Binding { Binding {
@@ -633,6 +637,7 @@ Item {
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
barSpacing: barConfig?.spacing ?? 4 barSpacing: barConfig?.spacing ?? 4
sectionAvailablePrimarySize: Math.max(1, vRightSection.y > 0 ? vRightSection.y - (vLeftSection.y + vLeftSection.height) : parent.height / 3)
} }
Binding { Binding {
@@ -667,6 +672,7 @@ Item {
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
barSpacing: barConfig?.spacing ?? 4 barSpacing: barConfig?.spacing ?? 4
sectionAvailablePrimarySize: Math.max(1, vCenterSection.y > 0 ? parent.height - (vCenterSection.y + vCenterSection.height) : parent.height / 3)
} }
Binding { Binding {
@@ -14,6 +14,7 @@ Item {
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null property var blurBarWindow: null
property real sectionAvailablePrimarySize: 0
property bool overrideAxisLayout: false property bool overrideAxisLayout: false
property bool forceVerticalLayout: false property bool forceVerticalLayout: false
@@ -61,6 +62,7 @@ Item {
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow blurBarWindow: root.blurBarWindow
sectionAvailablePrimarySize: root.sectionAvailablePrimarySize
isFirst: index === 0 isFirst: index === 0
isLast: index === rowRepeater.count - 1 isLast: index === rowRepeater.count - 1
sectionSpacing: parent.rowSpacing sectionSpacing: parent.rowSpacing
@@ -106,6 +108,7 @@ Item {
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow blurBarWindow: root.blurBarWindow
sectionAvailablePrimarySize: root.sectionAvailablePrimarySize
isFirst: index === 0 isFirst: index === 0
isLast: index === columnRepeater.count - 1 isLast: index === columnRepeater.count - 1
sectionSpacing: parent.columnSpacing sectionSpacing: parent.columnSpacing
@@ -14,6 +14,7 @@ Item {
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null property var blurBarWindow: null
property real sectionAvailablePrimarySize: 0
property bool overrideAxisLayout: false property bool overrideAxisLayout: false
property bool forceVerticalLayout: false property bool forceVerticalLayout: false
@@ -63,6 +64,7 @@ Item {
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow blurBarWindow: root.blurBarWindow
sectionAvailablePrimarySize: root.sectionAvailablePrimarySize
isFirst: index === 0 isFirst: index === 0
isLast: index === rowRepeater.count - 1 isLast: index === rowRepeater.count - 1
sectionSpacing: parent.rowSpacing sectionSpacing: parent.rowSpacing
@@ -108,6 +110,7 @@ Item {
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow blurBarWindow: root.blurBarWindow
sectionAvailablePrimarySize: root.sectionAvailablePrimarySize
isFirst: index === 0 isFirst: index === 0
isLast: index === columnRepeater.count - 1 isLast: index === columnRepeater.count - 1
sectionSpacing: parent.columnSpacing sectionSpacing: parent.columnSpacing
@@ -17,6 +17,7 @@ Loader {
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null property var blurBarWindow: null
property real sectionAvailablePrimarySize: 0
property bool isFirst: false property bool isFirst: false
property bool isLast: false property bool isLast: false
property real sectionSpacing: 0 property real sectionSpacing: 0
@@ -141,6 +142,14 @@ Loader {
restoreMode: Binding.RestoreNone restoreMode: Binding.RestoreNone
} }
Binding {
target: root.item
when: root.item && "sectionAvailablePrimarySize" in root.item
property: "sectionAvailablePrimarySize"
value: root.sectionAvailablePrimarySize
restoreMode: Binding.RestoreNone
}
Binding { Binding {
target: root.item target: root.item
when: root.item && "isLeftBarEdge" in root.item when: root.item && "isLeftBarEdge" in root.item
@@ -32,9 +32,20 @@ BasePill {
} }
readonly property var notepadInstance: resolveNotepadInstance() readonly property var notepadInstance: resolveNotepadInstance()
readonly property bool isActive: notepadInstance?.isVisible ?? false readonly property bool popoutDefault: SettingsData.notepadDefaultMode === "popout"
readonly property bool isActive: popoutDefault ? (PopoutService.notepadPopout?.visible ?? false) : (notepadInstance?.isVisible ?? false)
property bool isAutoHideBar: false property bool isAutoHideBar: false
function showActiveSurface() {
if (root.popoutDefault) {
PopoutService.openNotepadPopout();
return;
}
const instance = prepareNotepadInstance(root.notepadInstance);
if (instance && typeof instance.show === "function")
instance.show();
}
function prepareNotepadInstance(instance) { function prepareNotepadInstance(instance) {
if (instance) if (instance)
instance.triggerUsesOverlayLayer = root.barUsesOverlayLayer; instance.triggerUsesOverlayLayer = root.barUsesOverlayLayer;
@@ -75,20 +86,14 @@ BasePill {
function openTabByIndex(tabIndex) { function openTabByIndex(tabIndex) {
if (tabIndex < 0) if (tabIndex < 0)
return; return;
const instance = prepareNotepadInstance(root.notepadInstance); showActiveSurface();
if (instance && typeof instance.show === "function") {
instance.show();
}
Qt.callLater(() => { Qt.callLater(() => {
NotepadStorageService.switchToTab(tabIndex); NotepadStorageService.switchToTab(tabIndex);
}); });
} }
function openNewNote() { function openNewNote() {
const instance = prepareNotepadInstance(root.notepadInstance); showActiveSurface();
if (instance && typeof instance.show === "function") {
instance.show();
}
Qt.callLater(() => { Qt.callLater(() => {
NotepadStorageService.createNewTab(); NotepadStorageService.createNewTab();
}); });
@@ -147,6 +152,10 @@ BasePill {
openContextMenu(); openContextMenu();
return; return;
} }
if (root.popoutDefault) {
PopoutService.toggleNotepadPopout();
return;
}
const inst = prepareNotepadInstance(root.notepadInstance); const inst = prepareNotepadInstance(root.notepadInstance);
if (inst) { if (inst) {
inst.toggle(); inst.toggle();
@@ -22,6 +22,10 @@ BasePill {
property bool isAtBottom: false property bool isAtBottom: false
property bool isAutoHideBar: false property bool isAutoHideBar: false
property bool useOverflowPopup: !widgetData?.trayUseInlineExpansion property bool useOverflowPopup: !widgetData?.trayUseInlineExpansion
property bool useSingleLineOverflowPopup: widgetData?.trayPopupSingleLine ?? SettingsData.trayPopupSingleLine
property bool useAutomaticOverflow: widgetData?.trayAutoOverflow ?? SettingsData.trayAutoOverflow
property int configuredMaxVisibleItems: widgetData?.trayMaxVisibleItems ?? SettingsData.trayMaxVisibleItems
property real sectionAvailablePrimarySize: 0
readonly property var hiddenTrayIds: { readonly property var hiddenTrayIds: {
const envValue = Quickshell.env("DMS_HIDE_TRAYIDS") || ""; const envValue = Quickshell.env("DMS_HIDE_TRAYIDS") || "";
return envValue ? envValue.split(",").map(id => id.trim().toLowerCase()) : []; return envValue ? envValue.split(",").map(id => id.trim().toLowerCase()) : [];
@@ -146,12 +150,32 @@ BasePill {
readonly property var allSortedTrayItems: sortByPreferredOrder(allTrayItems, _trayOrderTrigger) readonly property var allSortedTrayItems: sortByPreferredOrder(allTrayItems, _trayOrderTrigger)
readonly property var allSortedTrayItemKeys: allSortedTrayItems.map(item => getTrayItemKey(item)) readonly property var allSortedTrayItemKeys: allSortedTrayItems.map(item => getTrayItemKey(item))
readonly property var mainBarItemsRaw: allSortedTrayItems.filter(item => !SessionData.isHiddenTrayId(root.getTrayItemKey(item))) readonly property var visibleSortedTrayItems: allSortedTrayItems.filter(item => !SessionData.isHiddenTrayId(root.getTrayItemKey(item)))
readonly property int automaticVisibleItemLimit: {
if (!root.useAutomaticOverflow)
return root.visibleSortedTrayItems.length;
const explicitLimit = Number(root.configuredMaxVisibleItems || 0);
if (explicitLimit > 0)
return Math.max(1, Math.min(root.visibleSortedTrayItems.length, explicitLimit));
const scale = (typeof CompositorService !== "undefined" && CompositorService.getScreenScale) ? Math.max(1, CompositorService.getScreenScale(root.parentScreen)) : 1;
const sectionPrimary = root.sectionAvailablePrimarySize > 0 ? root.sectionAvailablePrimarySize : (root.isVerticalOrientation ? (root.parentScreen?.height || 0) : (root.parentScreen?.width || 0));
const logicalPrimary = sectionPrimary > 0 ? (sectionPrimary / scale) : 640;
const maxTrayShare = root.isVerticalOrientation ? 0.55 : 0.50;
const itemSize = Math.max(1, root.trayItemSize);
const slots = Math.floor((logicalPrimary * maxTrayShare) / itemSize);
return Math.max(2, Math.min(10, Math.min(root.visibleSortedTrayItems.length, slots)));
}
readonly property var mainBarItemsRaw: visibleSortedTrayItems.slice(0, automaticVisibleItemLimit)
readonly property var mainBarItems: mainBarItemsRaw.map((item, idx) => ({ readonly property var mainBarItems: mainBarItemsRaw.map((item, idx) => ({
key: getTrayItemKey(item), key: getTrayItemKey(item),
item: item item: item
})) }))
readonly property var hiddenBarItems: allSortedTrayItems.filter(item => SessionData.isHiddenTrayId(root.getTrayItemKey(item))) readonly property var autoOverflowBarItems: visibleSortedTrayItems.slice(automaticVisibleItemLimit)
readonly property var manualHiddenBarItems: allSortedTrayItems.filter(item => SessionData.isHiddenTrayId(root.getTrayItemKey(item)))
readonly property var hiddenBarItemKeys: manualHiddenBarItems.concat(autoOverflowBarItems).map(item => root.getTrayItemKey(item))
readonly property var hiddenBarItems: allSortedTrayItems.filter(item => hiddenBarItemKeys.indexOf(root.getTrayItemKey(item)) !== -1)
readonly property string trayIconTintMode: { readonly property string trayIconTintMode: {
const configuredMode = SettingsData.systemTrayIconTintMode || "none"; const configuredMode = SettingsData.systemTrayIconTintMode || "none";
switch (configuredMode) { switch (configuredMode) {
@@ -219,6 +243,10 @@ BasePill {
const fromKey = mainBarItems[visibleFromIndex]?.key ?? null; const fromKey = mainBarItems[visibleFromIndex]?.key ?? null;
const toKey = mainBarItems[visibleToIndex]?.key ?? null; const toKey = mainBarItems[visibleToIndex]?.key ?? null;
moveTrayItemKeyInFullOrder(fromKey, toKey);
}
function moveTrayItemKeyInFullOrder(fromKey, toKey) {
if (!fromKey || !toKey) if (!fromKey || !toKey)
return; return;
@@ -233,10 +261,103 @@ BasePill {
SessionData.setTrayItemOrder(fullOrder); SessionData.setTrayItemOrder(fullOrder);
} }
function promoteTrayItemToBar(item) {
const itemKey = getTrayItemKey(item);
if (!itemKey)
return;
if (SessionData.isHiddenTrayId(itemKey)) {
SessionData.showTrayId(itemKey);
return;
}
const fullOrder = [...allSortedTrayItemKeys];
const fromIndex = fullOrder.indexOf(itemKey);
if (fromIndex < 0)
return;
const movedKey = fullOrder.splice(fromIndex, 1)[0];
const targetIndex = Math.max(0, Math.min(root.automaticVisibleItemLimit - 1, fullOrder.length));
fullOrder.splice(targetIndex, 0, movedKey);
SessionData.setTrayItemOrder(fullOrder);
}
function isManualHiddenTrayItem(item) {
return SessionData.isHiddenTrayId(getTrayItemKey(item));
}
function isAutoOverflowTrayItem(item) {
const key = getTrayItemKey(item);
return key && !isManualHiddenTrayItem(item) && root.autoOverflowBarItems.some(overflowItem => getTrayItemKey(overflowItem) === key);
}
function dragShiftOffset(index, draggedIndex, dropTargetIndex, shiftAmount) {
if (draggedIndex < 0 || index === draggedIndex || dropTargetIndex < 0)
return 0;
if (draggedIndex < dropTargetIndex && index > draggedIndex && index <= dropTargetIndex)
return -shiftAmount;
if (draggedIndex > dropTargetIndex && index >= dropTargetIndex && index < draggedIndex)
return shiftAmount;
return 0;
}
function beginMainDrag(visualIndex, reversed) {
root.draggedIndex = reversed ? (root.mainBarItems.length - 1 - visualIndex) : visualIndex;
root.dropTargetIndex = root.draggedIndex;
}
function updateMainDrag(axisOffset, visualIndex, reversed) {
const itemSize = root.trayItemSize;
const slotOffset = Math.round(axisOffset / itemSize);
const visualTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, visualIndex + slotOffset));
const newTargetIndex = reversed ? (root.mainBarItems.length - 1 - visualTargetIndex) : visualTargetIndex;
if (newTargetIndex !== root.dropTargetIndex)
root.dropTargetIndex = newTargetIndex;
}
function finishMainDrag() {
const didReorder = root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex;
if (didReorder) {
root.suppressShiftAnimation = true;
root.moveTrayItemInFullOrder(root.draggedIndex, root.dropTargetIndex);
Qt.callLater(() => root.suppressShiftAnimation = false);
}
root.draggedIndex = -1;
root.dropTargetIndex = -1;
return didReorder;
}
function beginPopupDrag(index) {
root.popupDraggedIndex = index;
root.popupDropTargetIndex = index;
}
function updatePopupDrag(axisOffset, index) {
const itemSize = root.trayItemSize + 6;
const slotOffset = Math.round(axisOffset / itemSize);
const newTargetIndex = Math.max(0, Math.min(root.hiddenBarItems.length - 1, index + slotOffset));
if (newTargetIndex !== root.popupDropTargetIndex)
root.popupDropTargetIndex = newTargetIndex;
}
function finishPopupDrag() {
const didReorder = root.popupDropTargetIndex >= 0 && root.popupDropTargetIndex !== root.popupDraggedIndex;
if (didReorder) {
const fromItem = root.hiddenBarItems[root.popupDraggedIndex];
const toItem = root.hiddenBarItems[root.popupDropTargetIndex];
root.suppressShiftAnimation = true;
root.moveTrayItemKeyInFullOrder(root.getTrayItemKey(fromItem), root.getTrayItemKey(toItem));
Qt.callLater(() => root.suppressShiftAnimation = false);
}
root.popupDraggedIndex = -1;
root.popupDropTargetIndex = -1;
return didReorder;
}
property int draggedIndex: -1 property int draggedIndex: -1
property int dropTargetIndex: -1 property int dropTargetIndex: -1
property int popupDraggedIndex: -1
property int popupDropTargetIndex: -1
property bool suppressShiftAnimation: false property bool suppressShiftAnimation: false
readonly property bool hasHiddenItems: allTrayItems.length > mainBarItems.length readonly property bool hasHiddenItems: hiddenBarItems.length > 0
readonly property bool inlineExpanded: hasHiddenItems && !useOverflowPopup && menuOpen readonly property bool inlineExpanded: hasHiddenItems && !useOverflowPopup && menuOpen
visible: allTrayItems.length > 0 visible: allTrayItems.length > 0
opacity: allTrayItems.length > 0 ? 1 : 0 opacity: allTrayItems.length > 0 ? 1 : 0
@@ -351,22 +472,7 @@ BasePill {
height: root.barThickness height: root.barThickness
z: dragHandler.dragging ? 100 : 0 z: dragHandler.dragging ? 100 : 0
property real shiftOffset: { property real shiftOffset: root.dragShiftOffset(index, root.draggedIndex, root.dropTargetIndex, root.trayItemSize)
if (root.draggedIndex < 0)
return 0;
if (index === root.draggedIndex)
return 0;
const dragIdx = root.draggedIndex;
const dropIdx = root.dropTargetIndex;
const shiftAmount = root.trayItemSize;
if (dropIdx < 0)
return 0;
if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
return -shiftAmount;
if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx)
return shiftAmount;
return 0;
}
transform: Translate { transform: Translate {
x: delegateRoot.shiftOffset x: delegateRoot.shiftOffset
@@ -466,19 +572,12 @@ BasePill {
onReleased: mouse => { onReleased: mouse => {
longPressTimer.stop(); longPressTimer.stop();
const wasDragging = dragHandler.dragging; const wasDragging = dragHandler.dragging;
const didReorder = wasDragging && root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex; if (wasDragging)
root.finishMainDrag();
if (didReorder) {
root.suppressShiftAnimation = true;
root.moveTrayItemInFullOrder(root.draggedIndex, root.dropTargetIndex);
Qt.callLater(() => root.suppressShiftAnimation = false);
}
dragHandler.longPressing = false; dragHandler.longPressing = false;
dragHandler.dragging = false; dragHandler.dragging = false;
dragHandler.dragAxisOffset = 0; dragHandler.dragAxisOffset = 0;
root.draggedIndex = -1;
root.dropTargetIndex = -1;
if (wasDragging || mouse.button !== Qt.LeftButton) if (wasDragging || mouse.button !== Qt.LeftButton)
return; return;
@@ -501,8 +600,7 @@ BasePill {
const distance = Math.abs(mouse.x - dragHandler.dragStartPos.x); const distance = Math.abs(mouse.x - dragHandler.dragStartPos.x);
if (distance > 5) { if (distance > 5) {
dragHandler.dragging = true; dragHandler.dragging = true;
root.draggedIndex = root.reverseInlineHorizontal ? (root.mainBarItems.length - 1 - index) : index; root.beginMainDrag(index, root.reverseInlineHorizontal);
root.dropTargetIndex = root.draggedIndex;
} }
} }
if (!dragHandler.dragging) if (!dragHandler.dragging)
@@ -510,13 +608,7 @@ BasePill {
const axisOffset = mouse.x - dragHandler.dragStartPos.x; const axisOffset = mouse.x - dragHandler.dragStartPos.x;
dragHandler.dragAxisOffset = axisOffset; dragHandler.dragAxisOffset = axisOffset;
const itemSize = root.trayItemSize; root.updateMainDrag(axisOffset, index, root.reverseInlineHorizontal);
const slotOffset = Math.round(axisOffset / itemSize);
const visualTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
const newTargetIndex = root.reverseInlineHorizontal ? (root.mainBarItems.length - 1 - visualTargetIndex) : visualTargetIndex;
if (newTargetIndex !== root.dropTargetIndex) {
root.dropTargetIndex = newTargetIndex;
}
} }
onClicked: mouse => { onClicked: mouse => {
@@ -706,22 +798,7 @@ BasePill {
height: root.trayItemSize height: root.trayItemSize
z: dragHandler.dragging ? 100 : 0 z: dragHandler.dragging ? 100 : 0
property real shiftOffset: { property real shiftOffset: root.dragShiftOffset(index, root.draggedIndex, root.dropTargetIndex, root.trayItemSize)
if (root.draggedIndex < 0)
return 0;
if (index === root.draggedIndex)
return 0;
const dragIdx = root.draggedIndex;
const dropIdx = root.dropTargetIndex;
const shiftAmount = root.trayItemSize;
if (dropIdx < 0)
return 0;
if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
return -shiftAmount;
if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx)
return shiftAmount;
return 0;
}
transform: Translate { transform: Translate {
y: shiftOffset y: shiftOffset
@@ -821,19 +898,12 @@ BasePill {
onReleased: mouse => { onReleased: mouse => {
longPressTimer.stop(); longPressTimer.stop();
const wasDragging = dragHandler.dragging; const wasDragging = dragHandler.dragging;
const didReorder = wasDragging && root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex; if (wasDragging)
root.finishMainDrag();
if (didReorder) {
root.suppressShiftAnimation = true;
root.moveTrayItemInFullOrder(root.draggedIndex, root.dropTargetIndex);
Qt.callLater(() => root.suppressShiftAnimation = false);
}
dragHandler.longPressing = false; dragHandler.longPressing = false;
dragHandler.dragging = false; dragHandler.dragging = false;
dragHandler.dragAxisOffset = 0; dragHandler.dragAxisOffset = 0;
root.draggedIndex = -1;
root.dropTargetIndex = -1;
if (wasDragging || mouse.button !== Qt.LeftButton) if (wasDragging || mouse.button !== Qt.LeftButton)
return; return;
@@ -856,8 +926,7 @@ BasePill {
const distance = Math.abs(mouse.y - dragHandler.dragStartPos.y); const distance = Math.abs(mouse.y - dragHandler.dragStartPos.y);
if (distance > 5) { if (distance > 5) {
dragHandler.dragging = true; dragHandler.dragging = true;
root.draggedIndex = index; root.beginMainDrag(index, false);
root.dropTargetIndex = root.draggedIndex;
} }
} }
if (!dragHandler.dragging) if (!dragHandler.dragging)
@@ -865,12 +934,7 @@ BasePill {
const axisOffset = mouse.y - dragHandler.dragStartPos.y; const axisOffset = mouse.y - dragHandler.dragStartPos.y;
dragHandler.dragAxisOffset = axisOffset; dragHandler.dragAxisOffset = axisOffset;
const itemSize = root.trayItemSize; root.updateMainDrag(axisOffset, index, false);
const slotOffset = Math.round(axisOffset / itemSize);
const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
if (newTargetIndex !== root.dropTargetIndex) {
root.dropTargetIndex = newTargetIndex;
}
} }
onClicked: mouse => { onClicked: mouse => {
@@ -1136,20 +1200,38 @@ BasePill {
id: menuContainer id: menuContainer
objectName: "overflowMenuContainer" objectName: "overflowMenuContainer"
readonly property bool popupUsesVerticalLine: root.useSingleLineOverflowPopup && root.isVerticalOrientation
readonly property real popupPadding: Theme.spacingS + (popupUsesVerticalLine ? 3 : 0)
readonly property real rawWidth: { readonly property real rawWidth: {
const itemCount = root.hiddenBarItems.length; const itemCount = root.hiddenBarItems.length;
const cols = Math.min(5, itemCount); if (itemCount === 0)
return 0;
if (popupUsesVerticalLine)
return root.trayItemSize + 4 + popupPadding * 2;
const cols = root.useSingleLineOverflowPopup ? itemCount : Math.min(5, itemCount);
const itemSize = root.trayItemSize + 4; const itemSize = root.trayItemSize + 4;
const spacing = 2; const spacing = 2;
return cols * itemSize + (cols - 1) * spacing + Theme.spacingS * 2; const desiredWidth = cols * itemSize + (cols - 1) * spacing + popupPadding * 2;
if (!root.useSingleLineOverflowPopup)
return desiredWidth;
const maxWidth = Math.max(itemSize + popupPadding * 2, overflowMenu.maskWidth - 20);
return Math.min(desiredWidth, maxWidth);
} }
readonly property real rawHeight: { readonly property real rawHeight: {
const itemCount = root.hiddenBarItems.length; const itemCount = root.hiddenBarItems.length;
const cols = Math.min(5, itemCount); if (itemCount === 0)
const rows = Math.ceil(itemCount / cols); return 0;
const itemSize = root.trayItemSize + 4; const itemSize = root.trayItemSize + 4;
const spacing = 2; const spacing = 2;
return rows * itemSize + (rows - 1) * spacing + Theme.spacingS * 2; if (popupUsesVerticalLine) {
const desiredHeight = itemCount * itemSize + (itemCount - 1) * spacing + popupPadding * 2;
const maxHeight = Math.max(itemSize + popupPadding * 2, overflowMenu.maskHeight - 20);
return Math.min(desiredHeight, maxHeight);
}
const cols = root.useSingleLineOverflowPopup ? itemCount : Math.min(5, itemCount);
const rows = Math.ceil(itemCount / cols);
return rows * itemSize + (rows - 1) * spacing + popupPadding * 2;
} }
readonly property real alignedWidth: Theme.px(rawWidth, overflowMenu.dpr) readonly property real alignedWidth: Theme.px(rawWidth, overflowMenu.dpr)
@@ -1230,76 +1312,161 @@ BasePill {
z: 100 z: 100
} }
Grid { Flickable {
id: menuGrid
anchors.centerIn: parent anchors.centerIn: parent
columns: Math.min(5, root.hiddenBarItems.length) width: parent.width - menuContainer.popupPadding * 2
spacing: 2 height: parent.height - menuContainer.popupPadding * 2
rowSpacing: 2 contentWidth: menuGrid.implicitWidth
contentHeight: menuGrid.implicitHeight
boundsBehavior: Flickable.StopAtBounds
clip: true
interactive: root.useSingleLineOverflowPopup && (menuContainer.popupUsesVerticalLine ? contentHeight > height : contentWidth > width)
Repeater { Grid {
model: root.hiddenBarItems id: menuGrid
anchors.verticalCenter: menuContainer.popupUsesVerticalLine ? undefined : parent.verticalCenter
anchors.horizontalCenter: menuContainer.popupUsesVerticalLine ? parent.horizontalCenter : undefined
columns: menuContainer.popupUsesVerticalLine ? 1 : (root.useSingleLineOverflowPopup ? root.hiddenBarItems.length : Math.min(5, root.hiddenBarItems.length))
spacing: 2
rowSpacing: 2
delegate: Rectangle { Repeater {
property var trayItem: modelData model: root.hiddenBarItems
property string iconSource: root.trayIconSourceFor(trayItem)
width: root.trayItemSize + 4 delegate: Rectangle {
height: root.trayItemSize + 4 id: overflowItemRoot
radius: Theme.cornerRadius property var trayItem: modelData
color: itemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0) property string itemKey: root.getTrayItemKey(trayItem)
property string iconSource: root.trayIconSourceFor(trayItem)
IconImage { width: root.trayItemSize + 4
id: menuIconImg height: root.trayItemSize + 4
anchors.centerIn: parent z: popupDragHandler.dragging ? 100 : 0
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) radius: Theme.cornerRadius
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) color: itemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0)
source: parent.iconSource border.width: popupDragHandler.dragging ? 2 : 0
asynchronous: true border.color: Theme.primary
smooth: true opacity: popupDragHandler.dragging ? 0.8 : 1.0
mipmap: true
visible: status === Image.Ready
layer.enabled: root.trayIconTintEnabled
layer.effect: MultiEffect {
saturation: root.trayIconSaturation
colorization: root.trayIconColorization
colorizationColor: root.trayIconTintColor
}
}
StyledText { property real shiftOffset: root.dragShiftOffset(index, root.popupDraggedIndex, root.popupDropTargetIndex, root.trayItemSize + 6)
anchors.centerIn: parent
visible: !menuIconImg.visible
text: {
const itemId = trayItem?.id || "";
if (!itemId)
return "?";
return itemId.charAt(0).toUpperCase();
}
font.pixelSize: 10
color: Theme.widgetTextColor
}
MouseArea { transform: Translate {
id: itemArea x: !menuContainer.popupUsesVerticalLine ? overflowItemRoot.shiftOffset + (popupDragHandler.dragging ? popupDragHandler.dragAxisOffset : 0) : 0
anchors.fill: parent y: menuContainer.popupUsesVerticalLine ? overflowItemRoot.shiftOffset + (popupDragHandler.dragging ? popupDragHandler.dragAxisOffset : 0) : 0
hoverEnabled: true Behavior on x {
acceptedButtons: Qt.LeftButton | Qt.RightButton enabled: !root.suppressShiftAnimation && !menuContainer.popupUsesVerticalLine
cursorShape: Qt.PointingHandCursor NumberAnimation {
onClicked: mouse => { duration: 150
if (!trayItem) easing.type: Easing.OutCubic
return; }
if (mouse.button === Qt.LeftButton && !trayItem.onlyMenu) {
trayItem.activate();
root.menuOpen = false;
return;
} }
if (!trayItem.hasMenu) { Behavior on y {
const gp = itemArea.mapToGlobal(mouse.x, mouse.y); enabled: !root.suppressShiftAnimation && menuContainer.popupUsesVerticalLine
root.callContextMenuFallback(trayItem.id, Math.round(gp.x), Math.round(gp.y)); NumberAnimation {
return; duration: 150
easing.type: Easing.OutCubic
}
}
}
Item {
id: popupDragHandler
anchors.fill: parent
property bool dragging: false
property point dragStartPos: Qt.point(0, 0)
property real dragAxisOffset: 0
property bool longPressing: false
Timer {
id: popupLongPressTimer
interval: 400
repeat: false
onTriggered: popupDragHandler.longPressing = true
}
}
IconImage {
id: menuIconImg
anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
source: parent.iconSource
asynchronous: true
smooth: true
mipmap: true
visible: status === Image.Ready
layer.enabled: root.trayIconTintEnabled
layer.effect: MultiEffect {
saturation: root.trayIconSaturation
colorization: root.trayIconColorization
colorizationColor: root.trayIconTintColor
}
}
StyledText {
anchors.centerIn: parent
visible: !menuIconImg.visible
text: {
const itemId = trayItem?.id || "";
if (!itemId)
return "?";
return itemId.charAt(0).toUpperCase();
}
font.pixelSize: 10
color: Theme.widgetTextColor
}
MouseArea {
id: itemArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: popupDragHandler.longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
onPressed: mouse => {
if (mouse.button === Qt.LeftButton) {
popupDragHandler.dragStartPos = Qt.point(mouse.x, mouse.y);
popupLongPressTimer.start();
}
}
onReleased: mouse => {
popupLongPressTimer.stop();
const wasDragging = popupDragHandler.dragging;
if (wasDragging)
root.finishPopupDrag();
popupDragHandler.longPressing = false;
popupDragHandler.dragging = false;
popupDragHandler.dragAxisOffset = 0;
}
onPositionChanged: mouse => {
const axisDelta = menuContainer.popupUsesVerticalLine ? (mouse.y - popupDragHandler.dragStartPos.y) : (mouse.x - popupDragHandler.dragStartPos.x);
if (popupDragHandler.longPressing && !popupDragHandler.dragging && Math.abs(axisDelta) > 5) {
popupDragHandler.dragging = true;
root.beginPopupDrag(index);
}
if (!popupDragHandler.dragging)
return;
popupDragHandler.dragAxisOffset = axisDelta;
root.updatePopupDrag(axisDelta, index);
}
onClicked: mouse => {
if (popupDragHandler.dragging)
return;
if (!trayItem)
return;
if (mouse.button === Qt.LeftButton && !trayItem.onlyMenu) {
trayItem.activate();
root.menuOpen = false;
return;
}
if (!trayItem.hasMenu) {
const gp = itemArea.mapToGlobal(mouse.x, mouse.y);
root.callContextMenuFallback(trayItem.id, Math.round(gp.x), Math.round(gp.y));
return;
}
root.showForTrayItem(trayItem, menuContainer, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
} }
root.showForTrayItem(trayItem, menuContainer, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
} }
} }
} }
@@ -1695,7 +1862,12 @@ BasePill {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Theme.spacingS anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: menuRoot.trayItem?.id || "Unknown" text: {
const itemId = menuRoot.trayItem?.id || "Unknown";
if (root.isAutoOverflowTrayItem(menuRoot.trayItem))
return itemId + " · " + I18n.tr("Keep in Bar");
return itemId;
}
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium color: Theme.surfaceTextMedium
elide: Text.ElideMiddle elide: Text.ElideMiddle
@@ -1706,7 +1878,11 @@ BasePill {
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: Theme.spacingS anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
name: SessionData.isHiddenTrayId(root.getTrayItemKey(menuRoot.trayItem)) ? "visibility" : "visibility_off" name: {
if (root.isAutoOverflowTrayItem(menuRoot.trayItem))
return "push_pin";
return root.isManualHiddenTrayItem(menuRoot.trayItem) ? "visibility" : "visibility_off";
}
size: 16 size: 16
color: Theme.widgetTextColor color: Theme.widgetTextColor
} }
@@ -1720,7 +1896,9 @@ BasePill {
const itemKey = root.getTrayItemKey(menuRoot.trayItem); const itemKey = root.getTrayItemKey(menuRoot.trayItem);
if (!itemKey) if (!itemKey)
return; return;
if (SessionData.isHiddenTrayId(itemKey)) { if (root.isAutoOverflowTrayItem(menuRoot.trayItem)) {
root.promoteTrayItemToBar(menuRoot.trayItem);
} else if (root.isManualHiddenTrayItem(menuRoot.trayItem)) {
SessionData.showTrayId(itemKey); SessionData.showTrayId(itemKey);
} else { } else {
SessionData.hideTrayId(itemKey); SessionData.hideTrayId(itemKey);
@@ -108,9 +108,6 @@ DankPopout {
MprisController.setActivePlayer(player); MprisController.setActivePlayer(player);
root.__hideDropdowns(); root.__hideDropdowns();
} }
onDeviceSelected: device => {
root.__hideDropdowns();
}
} }
} }
@@ -383,7 +383,27 @@ Item {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { acceptedButtons: Qt.LeftButton | Qt.RightButton
onPressed: mouse => {
if (mouse.button === Qt.RightButton) {
mouse.accepted = true;
}
}
onWheel: wheelEvent => {
if (SettingsData.audioDeviceScrollVolumeEnabled && wheelEvent.x >= deviceMouseArea.width / 2) {
AudioService.handleNodeVolumeWheel(modelData, wheelEvent);
} else {
wheelEvent.accepted = false;
}
}
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
if (modelData && modelData.audio) {
SessionData.suppressOSDTemporarily();
modelData.audio.muted = !modelData.audio.muted;
}
return;
}
if (modelData && modelData.name) { if (modelData && modelData.name) {
AudioService.setDefaultSinkByName(modelData.name); AudioService.setDefaultSinkByName(modelData.name);
root.deviceSelected(modelData); root.deviceSelected(modelData);
+21 -1
View File
@@ -866,7 +866,27 @@ Item {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { acceptedButtons: Qt.LeftButton | Qt.RightButton
onPressed: mouse => {
if (mouse.button === Qt.RightButton) {
mouse.accepted = true;
}
}
onWheel: wheelEvent => {
const delta = wheelEvent.angleDelta.y;
if (delta !== 0) {
AudioService.cycleAudioOutputDirection(delta < 0);
wheelEvent.accepted = true;
}
}
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
if (AudioService.sink?.audio) {
SessionData.suppressOSDTemporarily();
AudioService.sink.audio.muted = !AudioService.sink.audio.muted;
}
return;
}
if (devicesExpanded) { if (devicesExpanded) {
const sinks = AudioService.getAvailableSinks(); const sinks = AudioService.getAvailableSinks();
if (sinks && sinks.length > 1) { if (sinks && sinks.length > 1) {
+278 -9
View File
@@ -1,5 +1,6 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
@@ -21,21 +22,71 @@ Item {
property var currentTab: NotepadStorageService.tabs.length > NotepadStorageService.currentTabIndex ? NotepadStorageService.tabs[NotepadStorageService.currentTabIndex] : null property var currentTab: NotepadStorageService.tabs.length > NotepadStorageService.currentTabIndex ? NotepadStorageService.tabs[NotepadStorageService.currentTabIndex] : null
property bool showSettingsMenu: false property bool showSettingsMenu: false
property string pendingSaveContent: "" property string pendingSaveContent: ""
readonly property bool conflictBannerVisible: currentTab !== null && NotepadStorageService.conflictTabId === currentTab.id
property var slideout: null property var slideout: null
property bool inPopout: false
property bool surfaceVisible: slideout ? slideout.isVisible : true
signal hideRequested signal hideRequested
signal popoutRequested
signal dockRequested
signal previewRequested(string content) signal previewRequested(string content)
function externalSync() {
textEditor.syncFromDisk();
}
function flushAutoSave() {
textEditor.autoSaveToSession();
}
Ref { Ref {
service: NotepadStorageService service: NotepadStorageService
} }
// In connected frame mode the slideout sits on the Overlay layer
onFileDialogOpenChanged: {
if (slideout)
slideout.suppressOverlayLayer = fileDialogOpen;
}
Connections { Connections {
target: slideout target: slideout
enabled: slideout !== null enabled: slideout !== null
function onAboutToHide() { function onAboutToHide() {
textEditor.autoSaveToSession(); 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() { function hasUnsavedChanges() {
@@ -51,10 +102,14 @@ Item {
} }
function performCreateNewTab() { function performCreateNewTab() {
textEditor.commitLiveBuffer();
NotepadStorageService.createNewTab(); NotepadStorageService.createNewTab();
textEditor.applyingShared = true;
textEditor.text = ""; textEditor.text = "";
textEditor.lastSavedContent = ""; textEditor.lastSavedContent = "";
textEditor.loadedTabId = -1;
textEditor.contentLoaded = true; textEditor.contentLoaded = true;
textEditor.applyingShared = false;
textEditor.textArea.forceActiveFocus(); textEditor.textArea.forceActiveFocus();
} }
@@ -86,7 +141,6 @@ Item {
NotepadStorageService.switchToTab(tabIndex); NotepadStorageService.switchToTab(tabIndex);
Qt.callLater(() => { Qt.callLater(() => {
textEditor.loadCurrentTabContent();
if (currentTab) { if (currentTab) {
root.currentFileName = currentTab.fileName || ""; root.currentFileName = currentTab.fileName || "";
root.currentFileUrl = currentTab.fileUrl || ""; root.currentFileUrl = currentTab.fileUrl || "";
@@ -100,6 +154,7 @@ Item {
var content = textEditor.text; var content = textEditor.text;
var filePath = fileUrl.toString().replace(/^file:\/\//, ''); var filePath = fileUrl.toString().replace(/^file:\/\//, '');
textEditor.externalWatchPaused = true;
saveFileView.path = ""; saveFileView.path = "";
pendingSaveContent = content; pendingSaveContent = content;
saveFileView.path = filePath; saveFileView.path = filePath;
@@ -109,6 +164,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) { function loadFromFile(fileUrl) {
if (hasUnsavedTemporaryContent()) { if (hasUnsavedTemporaryContent()) {
root.pendingFileUrl = fileUrl; root.pendingFileUrl = fileUrl;
@@ -146,14 +248,155 @@ Item {
root.currentFileName = fileName; root.currentFileName = fileName;
root.currentFileUrl = fileUrl; 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()
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: 32
Row {
id: bannerActions
anchors.right: parent.right
spacing: Theme.spacingS
readonly property real available: parent.width
StyledRect {
width: Math.min(keepText.implicitWidth + Theme.spacingM * 2, Math.max(104, (bannerActions.available - bannerActions.spacing) / 2))
height: 32
radius: Theme.cornerRadius
color: "transparent"
border.color: Theme.outlineMedium
border.width: 1
StateLayer {
anchors.fill: parent
cornerRadius: parent.radius
stateColor: Theme.surfaceText
onClicked: root.resolveConflictKeepEdits()
}
StyledText {
id: keepText
anchors.centerIn: parent
width: parent.width - Theme.spacingM
text: I18n.tr("Keep My Edits")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
}
}
StyledRect {
width: Math.min(reloadText.implicitWidth + Theme.spacingM * 2, Math.max(116, (bannerActions.available - bannerActions.spacing) / 2))
height: 32
radius: Theme.cornerRadius
color: Theme.primary
StateLayer {
anchors.fill: parent
cornerRadius: parent.radius
stateColor: Theme.background
onClicked: root.resolveConflictReload()
}
StyledText {
id: reloadText
anchors.centerIn: parent
width: parent.width - Theme.spacingM
text: I18n.tr("Reload From Disk")
font.pixelSize: Theme.fontSizeSmall
color: Theme.background
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
}
}
}
}
}
}
}
Column { 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 spacing: Theme.spacingM
NotepadTabs { NotepadTabs {
@@ -178,11 +421,12 @@ Item {
id: textEditor id: textEditor
width: parent.width width: parent.width
height: parent.height - tabBar.height - Theme.spacingM * 2 height: parent.height - tabBar.height - Theme.spacingM * 2
inPopout: root.inPopout
surfaceVisible: root.surfaceVisible
onSaveRequested: { onSaveRequested: {
if (currentTab && !currentTab.isTemporary && currentTab.filePath) { if (currentTab && !currentTab.isTemporary && currentTab.filePath) {
var fileUrl = "file://" + currentTab.filePath; root.saveExternalWithFreshnessCheck();
saveToFile(fileUrl);
} else { } else {
root.fileDialogOpen = true; root.fileDialogOpen = true;
saveBrowserLoader.active = true; saveBrowserLoader.active = true;
@@ -214,12 +458,28 @@ Item {
onEscapePressed: { onEscapePressed: {
textEditor.autoSaveToSession(); textEditor.autoSaveToSession();
root.hideRequested(); if (showSettingsMenu) {
showSettingsMenu = false;
return;
}
if (!root.inPopout) {
root.hideRequested();
}
} }
onSettingsRequested: { onSettingsRequested: {
showSettingsMenu = !showSettingsMenu; showSettingsMenu = !showSettingsMenu;
} }
onPopoutRequested: root.popoutRequested()
onDockRequested: root.dockRequested()
onConflictDetected: diskContent => {
root.showConflictBanner(diskContent);
}
onAutoSaveRequested: root.autoSaveExternal()
} }
} }
@@ -242,17 +502,24 @@ Item {
printErrors: true printErrors: true
onSaved: { onSaved: {
if (currentTab && saveFileView.path && pendingSaveContent) { if (currentTab && saveFileView.path) {
NotepadStorageService.updateTabMetadata(NotepadStorageService.currentTabIndex, { NotepadStorageService.updateTabMetadata(NotepadStorageService.currentTabIndex, {
hasUnsavedChanges: false, hasUnsavedChanges: false,
lastSavedContent: pendingSaveContent lastSavedContent: pendingSaveContent
}); });
root.lastSavedFileContent = pendingSaveContent; root.lastSavedFileContent = pendingSaveContent;
pendingSaveContent = ""; textEditor.lastSavedContent = pendingSaveContent;
textEditor.ignoreNextExternalChange = true;
textEditor.commitLiveBuffer();
if (root.conflictBannerVisible)
NotepadStorageService.clearConflict();
} }
textEditor.externalWatchPaused = false;
pendingSaveContent = "";
} }
onSaveFailed: error => { onSaveFailed: error => {
textEditor.externalWatchPaused = false;
pendingSaveContent = ""; pendingSaveContent = "";
} }
} }
@@ -298,6 +565,7 @@ Item {
root.currentFileName = fileName; root.currentFileName = fileName;
root.currentFileUrl = fileUrl; root.currentFileUrl = fileUrl;
textEditor.externalWatchPaused = true;
if (currentTab) { if (currentTab) {
NotepadStorageService.saveTabAs(NotepadStorageService.currentTabIndex, cleanPath); NotepadStorageService.saveTabAs(NotepadStorageService.currentTabIndex, cleanPath);
@@ -343,7 +611,7 @@ Item {
browserTitle: I18n.tr("Open Notepad File") browserTitle: I18n.tr("Open Notepad File")
browserIcon: "folder_open" browserIcon: "folder_open"
browserType: "notepad_load" browserType: "notepad_load"
fileExtensions: ["*.txt", "*.md", "*.*"] fileExtensions: ["*"]
allowStacking: true allowStacking: true
onFileSelected: path => { onFileSelected: path => {
@@ -376,6 +644,7 @@ Item {
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 180 modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 180
shouldBeVisible: false shouldBeVisible: false
allowStacking: true allowStacking: true
useOverlayLayer: true
onBackgroundClicked: { onBackgroundClicked: {
close(); close();
@@ -0,0 +1,137 @@
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();
}
}
// A compositor close (e.g. niri close-window)
onClosed: win.visible = false
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 cachedFontFamilies: []
property var cachedMonoFamilies: [] property var cachedMonoFamilies: []
property bool fontsEnumerated: false property bool fontsEnumerated: false
property bool shortcutsExpanded: false
signal settingsRequested signal settingsRequested
signal findRequested signal findRequested
@@ -62,11 +63,23 @@ Item {
} }
} }
MouseArea { Rectangle {
anchors.fill: parent anchors.fill: parent
visible: root.isVisible visible: root.isVisible
onClicked: root.settingsRequested()
z: 50 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 { Rectangle {
@@ -74,8 +87,8 @@ Item {
visible: root.isVisible visible: root.isVisible
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: 360 width: Math.min(360, root.width - Theme.spacingL * 2)
height: settingsColumn.implicitHeight + Theme.spacingXL * 2 height: Math.min(settingsColumn.implicitHeight + Theme.spacingXL * 2, root.height - Theme.spacingL * 2)
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, Theme.notepadTransparency) 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.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
@@ -93,274 +106,458 @@ Item {
z: parent.z - 1 z: parent.z - 1
} }
Column { DankFlickable {
id: settingsColumn id: settingsFlickable
width: parent.width - Theme.spacingXL * 2 anchors.fill: parent
anchors.horizontalCenter: parent.horizontalCenter clip: true
anchors.top: parent.top contentWidth: width
anchors.topMargin: Theme.spacingXL contentHeight: settingsColumn.implicitHeight + Theme.spacingXL * 2
spacing: Theme.spacingS
Rectangle { Column {
width: parent.width id: settingsColumn
height: 36 x: Theme.spacingXL
color: "transparent" y: Theme.spacingXL
width: settingsFlickable.width - Theme.spacingXL * 2
spacing: Theme.spacingS
StyledText { Rectangle {
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
width: parent.width width: parent.width
spacing: Theme.spacingS height: 36
color: "transparent"
Column { StyledText {
width: parent.width - fontSizeControls.width - Theme.spacingM anchors.left: parent.left
spacing: Theme.spacingXS 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 { Rectangle {
text: I18n.tr("Font Size") width: parent.width
font.pixelSize: Theme.fontSizeSmall height: 1
font.weight: Font.Medium color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
color: Theme.surfaceText }
}
StyledText { DankToggle {
text: SettingsData.notepadFontSize + "px" anchors.left: parent.left
font.pixelSize: Theme.fontSizeSmall anchors.leftMargin: -Theme.spacingM
color: Theme.surfaceVariantText width: parent.width + Theme.spacingM
width: parent.width 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 { Row {
id: fontSizeControls anchors.left: parent.left
spacing: Theme.spacingS anchors.leftMargin: -Theme.spacingM
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankActionButton { DankIcon {
buttonSize: 32 name: "search"
iconName: "remove" size: Theme.iconSize - 2
iconSize: Theme.iconSizeSmall color: Theme.primary
enabled: SettingsData.notepadFontSize > 8 anchors.verticalCenter: parent.verticalCenter
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 { Column {
width: 60 anchors.verticalCenter: parent.verticalCenter
height: 32 spacing: Theme.spacingXS
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 { StyledText {
anchors.centerIn: parent text: I18n.tr("Find in Text")
text: SettingsData.notepadFontSize + "px" 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.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceText color: Theme.surfaceText
} }
StyledText {
text: SettingsData.notepadFontSize + "px"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
}
} }
DankActionButton { Row {
buttonSize: 32 id: fontSizeControls
iconName: "add" spacing: Theme.spacingS
iconSize: Theme.iconSizeSmall anchors.verticalCenter: parent.verticalCenter
enabled: SettingsData.notepadFontSize < 48
backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) DankActionButton {
iconColor: Theme.surfaceText buttonSize: 32
onClicked: { iconName: "remove"
var newSize = Math.min(48, SettingsData.notepadFontSize + 1); iconSize: Theme.iconSizeSmall
SettingsData.notepadFontSize = newSize; 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 { Rectangle {
width: parent.width
height: transparencySliderColumn.height + Theme.spacingS
color: "transparent"
Column {
id: transparencySliderColumn
width: parent.width width: parent.width
spacing: Theme.spacingS height: transparencySliderColumn.height + Theme.spacingS
color: "transparent"
DankToggle { Column {
anchors.left: parent.left id: transparencySliderColumn
anchors.leftMargin: -Theme.spacingM width: parent.width
width: parent.width + Theme.spacingM spacing: Theme.spacingS
text: I18n.tr("Custom Transparency")
description: I18n.tr("Override global transparency for Notepad") DankToggle {
checked: SettingsData.notepadTransparencyOverride >= 0 anchors.left: parent.left
onToggled: checked => { anchors.leftMargin: -Theme.spacingM
if (checked) { width: parent.width + Theme.spacingM
SettingsData.notepadTransparencyOverride = SettingsData.notepadLastCustomTransparency; text: I18n.tr("Surface Opacity")
} else { description: I18n.tr("Override global transparency for Notepad")
SettingsData.notepadTransparencyOverride = -1; checked: SettingsData.notepadTransparencyOverride >= 0
onToggled: checked => {
if (checked) {
SettingsData.notepadTransparencyOverride = SettingsData.notepadLastCustomTransparency;
} else {
SettingsData.notepadTransparencyOverride = -1;
}
} }
} }
}
DankSlider { DankSlider {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: -Theme.spacingM anchors.leftMargin: -Theme.spacingM
width: parent.width + Theme.spacingM width: parent.width + Theme.spacingM
height: 24 height: 24
visible: SettingsData.notepadTransparencyOverride >= 0 visible: SettingsData.notepadTransparencyOverride >= 0
value: Math.round((SettingsData.notepadTransparencyOverride >= 0 ? SettingsData.notepadTransparencyOverride : SettingsData.popupTransparency) * 100) value: Math.round((SettingsData.notepadTransparencyOverride >= 0 ? SettingsData.notepadTransparencyOverride : SettingsData.popupTransparency) * 100)
minimum: 0 minimum: 0
maximum: 100 maximum: 100
unit: "" unit: ""
showValue: true showValue: true
wheelEnabled: false wheelEnabled: false
onSliderValueChanged: newValue => { onSliderValueChanged: newValue => {
if (SettingsData.notepadTransparencyOverride >= 0) { if (SettingsData.notepadTransparencyOverride >= 0) {
SettingsData.notepadTransparencyOverride = newValue / 100; SettingsData.notepadTransparencyOverride = newValue / 100;
}
} }
} }
} }
} }
}
StyledText { Rectangle {
width: parent.width 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") height: gapColumn.height + Theme.spacingS
font.pixelSize: Theme.fontSizeSmall color: "transparent"
color: Theme.surfaceTextMedium
wrapMode: Text.WordWrap Column {
opacity: 0.8 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 pluginHighlightedHtml: ""
property string lastPluginContent: "" property string lastPluginContent: ""
property int loadRequestId: 0 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 saveRequested
signal openRequested signal openRequested
@@ -40,6 +57,10 @@ Column {
signal escapePressed signal escapePressed
signal contentChanged signal contentChanged
signal settingsRequested signal settingsRequested
signal popoutRequested
signal dockRequested
signal conflictDetected(string diskContent)
signal autoSaveRequested
function hasUnsavedChanges() { function hasUnsavedChanges() {
if (!currentTab || !contentLoaded) { if (!currentTab || !contentLoaded) {
@@ -52,6 +73,12 @@ Column {
return textArea.text !== lastSavedContent; return textArea.text !== lastSavedContent;
} }
function commitLiveBuffer() {
if (loadedTabId < 0 || !contentLoaded)
return;
NotepadStorageService.setSessionBuffer(loadedTabId, textArea.text, lastSavedContent);
}
function loadCurrentTabContent() { function loadCurrentTabContent() {
if (!currentTab) if (!currentTab)
return; return;
@@ -62,8 +89,25 @@ Column {
const activeTab = NotepadStorageService.tabs.length > NotepadStorageService.currentTabIndex ? NotepadStorageService.tabs[NotepadStorageService.currentTabIndex] : null; const activeTab = NotepadStorageService.tabs.length > NotepadStorageService.currentTabIndex ? NotepadStorageService.tabs[NotepadStorageService.currentTabIndex] : null;
if (requestId !== loadRequestId || !activeTab || activeTab.id !== requestedTabId) if (requestId !== loadRequestId || !activeTab || activeTab.id !== requestedTabId)
return; 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; lastSavedContent = content;
textArea.text = content; textArea.text = content;
applyingShared = false;
loadedTabId = requestedTabId;
contentLoaded = true; contentLoaded = true;
syncContentToPlugin(); syncContentToPlugin();
}); });
@@ -72,14 +116,56 @@ Column {
function saveCurrentTabContent() { function saveCurrentTabContent() {
if (!currentTab || !contentLoaded) if (!currentTab || !contentLoaded)
return; return;
if (!currentTab.isTemporary)
return;
NotepadStorageService.saveTabContent(NotepadStorageService.currentTabIndex, textArea.text); NotepadStorageService.saveTabContent(NotepadStorageService.currentTabIndex, textArea.text);
lastSavedContent = textArea.text; lastSavedContent = textArea.text;
NotepadStorageService.clearSessionBuffer(loadedTabId);
} }
function autoSaveToSession() { function autoSaveToSession() {
commitLiveBuffer();
if (!currentTab || !contentLoaded) if (!currentTab || !contentLoaded)
return; 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() { function setTextDocumentLineHeight() {
@@ -202,7 +288,8 @@ Column {
if (!currentTab) if (!currentTab)
return; return;
const filePath = currentTab?.filePath || ""; 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; const content = textArea.text;
if (content === lastPluginContent && SettingsData.getBuiltInPluginSetting("dankNotepadModule", "previewActive", false) === inlinePreviewVisible) { if (content === lastPluginContent && SettingsData.getBuiltInPluginSetting("dankNotepadModule", "previewActive", false) === inlinePreviewVisible) {
@@ -550,6 +637,7 @@ Column {
Connections { Connections {
target: NotepadStorageService target: NotepadStorageService
function onCurrentTabIndexChanged() { function onCurrentTabIndexChanged() {
root.commitLiveBuffer();
loadCurrentTabContent(); loadCurrentTabContent();
Qt.callLater(() => { Qt.callLater(() => {
textArea.forceActiveFocus(); textArea.forceActiveFocus();
@@ -570,7 +658,9 @@ Column {
} }
onTextChanged: { 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(); autoSaveTimer.restart();
} }
root.contentChanged(); root.contentChanged();
@@ -744,6 +834,7 @@ Column {
spacing: Theme.spacingS spacing: Theme.spacingS
Item { Item {
id: buttonBarItem
width: parent.width width: parent.width
height: 32 height: 32
@@ -820,17 +911,98 @@ Column {
} }
} }
DankActionButton { Row {
id: rightButtonRow
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
iconName: "more_horiz" spacing: Theme.spacingS
iconSize: Theme.iconSize - 2
iconColor: Theme.surfaceText DankActionButton {
onClicked: root.settingsRequested() 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 { Row {
id: statusRow
width: parent.width width: parent.width
spacing: Theme.spacingL spacing: Theme.spacingL
@@ -853,35 +1025,46 @@ Column {
opacity: 1.0 opacity: 1.0
} }
StyledText { Row {
text: { visible: textArea.text.length > 0
if (autoSaveTimer.running) { spacing: Theme.spacingXS
return I18n.tr("Auto-saving...");
}
if (hasUnsavedChanges()) { StyledText {
if (currentTab && currentTab.isTemporary) { anchors.verticalCenter: parent.verticalCenter
return I18n.tr("Unsaved note..."); readonly property bool savingToDisk: autoSaveTimer.running && currentTab && (currentTab.isTemporary || SettingsData.notepadAutoSave)
} else { text: {
return I18n.tr("Unsaved changes"); if (savingToDisk) {
return I18n.tr("Saving...");
} }
} else {
return I18n.tr("Saved");
}
}
font.pixelSize: Theme.fontSizeSmall
color: {
if (autoSaveTimer.running) {
return Theme.primary;
}
if (hasUnsavedChanges()) { if (currentTab && currentTab.isTemporary) {
return Theme.warning; return I18n.tr("Auto saved");
} else { }
return Theme.success;
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() 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 { Connections {
target: SettingsData target: SettingsData
function onBuiltInPluginSettingsChanged() { 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();
}
}
}
} }
@@ -113,6 +113,13 @@ Item {
} }
} }
} }
SettingsToggleRow {
text: I18n.tr("Device list scroll volume")
description: I18n.tr("Allow adjusting device volume by scrolling on the right half of items in the device list")
checked: SettingsData.audioDeviceScrollVolumeEnabled
onToggled: checked => SettingsData.set("audioDeviceScrollVolumeEnabled", checked)
}
} }
} }
} }
+7 -1
View File
@@ -460,7 +460,7 @@ Item {
"id": widget.id, "id": widget.id,
"enabled": widget.enabled "enabled": widget.enabled
}; };
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowSize", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "keyboardLayoutNameShowIcon", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "showIdleInhibitorIcon", "showDoNotDisturbIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion", "hideWhenIdle"]; var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowSize", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "keyboardLayoutNameShowIcon", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "showIdleInhibitorIcon", "showDoNotDisturbIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion", "trayPopupSingleLine", "trayAutoOverflow", "trayMaxVisibleItems", "hideWhenIdle"];
for (var i = 0; i < keys.length; i++) { for (var i = 0; i < keys.length; i++) {
if (widget[keys[i]] !== undefined) if (widget[keys[i]] !== undefined)
result[keys[i]] = widget[keys[i]]; result[keys[i]] = widget[keys[i]];
@@ -803,6 +803,12 @@ Item {
item.barShowOverflowBadge = widget.barShowOverflowBadge; item.barShowOverflowBadge = widget.barShowOverflowBadge;
if (widget.trayUseInlineExpansion !== undefined) if (widget.trayUseInlineExpansion !== undefined)
item.trayUseInlineExpansion = widget.trayUseInlineExpansion; item.trayUseInlineExpansion = widget.trayUseInlineExpansion;
if (widget.trayPopupSingleLine !== undefined)
item.trayPopupSingleLine = widget.trayPopupSingleLine;
if (widget.trayAutoOverflow !== undefined)
item.trayAutoOverflow = widget.trayAutoOverflow;
if (widget.trayMaxVisibleItems !== undefined)
item.trayMaxVisibleItems = widget.trayMaxVisibleItems;
if (widget.hideWhenIdle !== undefined) if (widget.hideWhenIdle !== undefined)
item.hideWhenIdle = widget.hideWhenIdle; item.hideWhenIdle = widget.hideWhenIdle;
} }
@@ -43,7 +43,7 @@ Column {
"id": widget.id, "id": widget.id,
"enabled": widget.enabled "enabled": widget.enabled
}; };
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowSize", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "keyboardLayoutNameShowIcon", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "showIdleInhibitorIcon", "showDoNotDisturbIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion"]; var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowSize", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "keyboardLayoutNameShowIcon", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "showIdleInhibitorIcon", "showDoNotDisturbIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion", "trayPopupSingleLine", "trayAutoOverflow", "trayMaxVisibleItems"];
for (var i = 0; i < keys.length; i++) { for (var i = 0; i < keys.length; i++) {
if (widget[keys[i]] !== undefined) if (widget[keys[i]] !== undefined)
result[keys[i]] = widget[keys[i]]; result[keys[i]] = widget[keys[i]];
@@ -1126,6 +1126,188 @@ Column {
} }
} }
} }
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: trayPopupLineArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
opacity: (trayContextMenu.currentWidgetData?.trayUseInlineExpansion ?? false) ? 0.55 : 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "view_week"
size: 16
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Single-Line Popup")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
id: trayPopupLineToggle
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
width: 40
height: 20
checked: trayContextMenu.currentWidgetData?.trayPopupSingleLine ?? SettingsData.trayPopupSingleLine
enabled: !(trayContextMenu.currentWidgetData?.trayUseInlineExpansion ?? false)
}
MouseArea {
id: trayPopupLineArea
anchors.fill: parent
hoverEnabled: true
cursorShape: (trayContextMenu.currentWidgetData?.trayUseInlineExpansion ?? false) ? Qt.ArrowCursor : Qt.PointingHandCursor
onClicked: {
if (trayContextMenu.currentWidgetData?.trayUseInlineExpansion ?? false)
return;
const newValue = !(trayContextMenu.currentWidgetData?.trayPopupSingleLine ?? SettingsData.trayPopupSingleLine);
root.overflowSettingChanged(trayContextMenu.sectionId, trayContextMenu.widgetIndex, "trayPopupSingleLine", newValue);
}
}
}
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: trayAutoOverflowArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "responsive_layout"
size: 16
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Auto Overflow")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
id: trayAutoOverflowToggle
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
width: 40
height: 20
checked: trayContextMenu.currentWidgetData?.trayAutoOverflow ?? SettingsData.trayAutoOverflow
}
MouseArea {
id: trayAutoOverflowArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
const newValue = !(trayContextMenu.currentWidgetData?.trayAutoOverflow ?? SettingsData.trayAutoOverflow);
root.overflowSettingChanged(trayContextMenu.sectionId, trayContextMenu.widgetIndex, "trayAutoOverflow", newValue);
}
}
}
Rectangle {
width: parent.width
height: 36
radius: Theme.cornerRadius
color: trayMaxVisibleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
opacity: (trayContextMenu.currentWidgetData?.trayAutoOverflow ?? SettingsData.trayAutoOverflow) ? 1 : 0.55
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "low_priority"
size: 16
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Max Visible")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
const value = trayContextMenu.currentWidgetData?.trayMaxVisibleItems ?? SettingsData.trayMaxVisibleItems;
return value > 0 ? String(value) : I18n.tr("Auto");
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
spacing: 2
DankActionButton {
buttonSize: 28
iconName: "remove"
iconSize: 16
iconColor: Theme.surfaceText
enabled: trayContextMenu.currentWidgetData?.trayAutoOverflow ?? SettingsData.trayAutoOverflow
onClicked: {
const current = trayContextMenu.currentWidgetData?.trayMaxVisibleItems ?? SettingsData.trayMaxVisibleItems;
root.overflowSettingChanged(trayContextMenu.sectionId, trayContextMenu.widgetIndex, "trayMaxVisibleItems", Math.max(0, current - 1));
}
}
DankActionButton {
buttonSize: 28
iconName: "add"
iconSize: 16
iconColor: Theme.surfaceText
enabled: trayContextMenu.currentWidgetData?.trayAutoOverflow ?? SettingsData.trayAutoOverflow
onClicked: {
const current = trayContextMenu.currentWidgetData?.trayMaxVisibleItems ?? SettingsData.trayMaxVisibleItems;
root.overflowSettingChanged(trayContextMenu.sectionId, trayContextMenu.widgetIndex, "trayMaxVisibleItems", Math.min(20, current + 1));
}
}
}
MouseArea {
id: trayMaxVisibleArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
}
}
} }
} }
} }
+35 -2
View File
@@ -58,6 +58,8 @@ Singleton {
return SessionData.deviceMaxVolumes[name] ?? 100; return SessionData.deviceMaxVolumes[name] ?? 100;
} }
readonly property int wheelVolumeStep: SettingsData.audioWheelScrollAmount
signal micMuteChanged signal micMuteChanged
signal audioOutputCycled(string deviceName, string deviceIcon) signal audioOutputCycled(string deviceName, string deviceIcon)
signal deviceAliasChanged(string nodeName, string newAlias) signal deviceAliasChanged(string nodeName, string newAlias)
@@ -156,14 +158,19 @@ Singleton {
return false; return false;
} }
function cycleAudioOutput() { function cycleAudioOutputDirection(forward) {
const sinks = getAvailableSinks(); const sinks = getAvailableSinks();
if (sinks.length < 2) if (sinks.length < 2)
return null; return null;
const currentName = root.sink?.name ?? ""; const currentName = root.sink?.name ?? "";
const currentIndex = sinks.findIndex(s => s.name === currentName); const currentIndex = sinks.findIndex(s => s.name === currentName);
const nextIndex = (currentIndex + 1) % sinks.length; let nextIndex;
if (forward) {
nextIndex = (currentIndex + 1) % sinks.length;
} else {
nextIndex = (currentIndex - 1 + sinks.length) % sinks.length;
}
const nextSink = sinks[nextIndex]; const nextSink = sinks[nextIndex];
setDefaultSinkByName(nextSink.name); setDefaultSinkByName(nextSink.name);
const name = displayName(nextSink); const name = displayName(nextSink);
@@ -171,6 +178,10 @@ Singleton {
return name; return name;
} }
function cycleAudioOutput() {
return cycleAudioOutputDirection(true);
}
function getDeviceAlias(nodeName) { function getDeviceAlias(nodeName) {
if (!nodeName) if (!nodeName)
return null; return null;
@@ -833,6 +844,28 @@ EOFCONFIG
return root.sink.audio.muted ? "Audio muted" : "Audio unmuted"; return root.sink.audio.muted ? "Audio muted" : "Audio unmuted";
} }
function handleNodeVolumeWheel(node, wheelEvent) {
if (!node?.audio)
return;
SessionData.suppressOSDTemporarily();
const delta = wheelEvent.angleDelta.y;
if (delta === 0)
return;
const current = Math.round(node.audio.volume * 100);
const maxVol = getMaxVolumePercent(node);
const newVolume = delta > 0 ? Math.min(maxVol, current + root.wheelVolumeStep) : Math.max(0, current - root.wheelVolumeStep);
node.audio.muted = false;
node.audio.volume = newVolume / 100;
if (node === sink) {
playVolumeChangeSoundIfEnabled();
}
wheelEvent.accepted = true;
}
function setMicVolume(percentage) { function setMicVolume(percentage) {
if (!root.source?.audio) { if (!root.source?.audio) {
return "No audio source available"; return "No audio source available";
@@ -23,6 +23,49 @@ Singleton {
property var tabsBeingCreated: ({}) property var tabsBeingCreated: ({})
property bool metadataLoaded: false property bool metadataLoaded: false
// Shared live edit state across slideout and popout surfaces.
property var sessionBuffers: ({})
property int sessionBufferRevision: 0
function setSessionBuffer(tabId, content, baseline) {
if (tabId === undefined || tabId === null || tabId < 0)
return
var next = Object.assign({}, sessionBuffers)
if (content !== baseline) {
next[tabId] = { content: content, baseline: baseline }
} else {
delete next[tabId]
}
sessionBuffers = next
sessionBufferRevision++
}
function getSessionBuffer(tabId) {
return sessionBuffers[tabId]
}
function clearSessionBuffer(tabId) {
if (sessionBuffers[tabId] === undefined)
return
var next = Object.assign({}, sessionBuffers)
delete next[tabId]
sessionBuffers = next
sessionBufferRevision++
}
property var conflictTabId: -1
property string conflictDiskContent: ""
function flagConflict(tabId, diskContent) {
conflictDiskContent = diskContent
conflictTabId = tabId
}
function clearConflict() {
conflictTabId = -1
conflictDiskContent = ""
}
Component.onCompleted: { Component.onCompleted: {
ensureDirectories() ensureDirectories()
} }
@@ -209,6 +252,10 @@ Singleton {
if (tabIndex < 0 || tabIndex >= tabs.length) return if (tabIndex < 0 || tabIndex >= tabs.length) return
var newTabs = tabs.slice() var newTabs = tabs.slice()
var closedTabId = newTabs[tabIndex] ? newTabs[tabIndex].id : -1
clearSessionBuffer(closedTabId)
if (conflictTabId === closedTabId)
clearConflict()
if (newTabs.length <= 1) { if (newTabs.length <= 1) {
var id = Date.now() var id = Date.now()
+77 -1
View File
@@ -789,21 +789,97 @@ Singleton {
networkInfoModal?.close(); networkInfoModal?.close();
} }
function openNotepad() { function closeNotepadSlideouts() {
for (var i = 0; i < notepadSlideouts.length; i++) {
if (notepadSlideouts[i] && notepadSlideouts[i].isVisible)
notepadSlideouts[i].hide();
}
}
function openNotepadSlideout() {
notepadPopout?.hide();
if (notepadSlideouts.length > 0) { if (notepadSlideouts.length > 0) {
notepadSlideouts[0]?.show(); notepadSlideouts[0]?.show();
} }
} }
// Keep the notepad in a single presentation for default modes
Connections {
target: SettingsData
function onNotepadDefaultModeChanged() {
if (SettingsData.notepadDefaultMode === "popout") {
var hadSlideout = false;
for (var i = 0; i < root.notepadSlideouts.length; i++) {
if (root.notepadSlideouts[i] && root.notepadSlideouts[i].isVisible) {
hadSlideout = true;
root.notepadSlideouts[i].hide();
}
}
if (hadSlideout)
root.openNotepadPopout();
} else if (root.notepadPopout && root.notepadPopout.visible) {
root.notepadPopout.hide();
root.openNotepadSlideout();
}
}
}
function openNotepad() {
if (SettingsData.notepadDefaultMode === "popout") {
openNotepadPopout();
return;
}
openNotepadSlideout();
}
function closeNotepad() { function closeNotepad() {
if (SettingsData.notepadDefaultMode === "popout") {
notepadPopout?.hide();
return;
}
if (notepadSlideouts.length > 0) { if (notepadSlideouts.length > 0) {
notepadSlideouts[0]?.hide(); notepadSlideouts[0]?.hide();
} }
} }
function toggleNotepad() { function toggleNotepad() {
if (SettingsData.notepadDefaultMode === "popout") {
toggleNotepadPopout();
return;
}
if (notepadSlideouts.length > 0) { if (notepadSlideouts.length > 0) {
notepadSlideouts[0]?.toggle(); notepadSlideouts[0]?.toggle();
} }
} }
property var notepadPopout: null
property var notepadPopoutLoader: null
property bool _notepadPopoutWantsOpen: false
function openNotepadPopout() {
closeNotepadSlideouts();
if (notepadPopout) {
notepadPopout.show();
} else if (notepadPopoutLoader) {
_notepadPopoutWantsOpen = true;
notepadPopoutLoader.active = true;
}
}
function _onNotepadPopoutLoaded() {
if (_notepadPopoutWantsOpen && notepadPopout) {
_notepadPopoutWantsOpen = false;
notepadPopout.show();
}
}
function toggleNotepadPopout() {
if (notepadPopout) {
if (!notepadPopout.visible)
closeNotepadSlideouts();
notepadPopout.toggle();
} else {
openNotepadPopout();
}
}
} }
+6 -5
View File
@@ -13,11 +13,12 @@ Row {
property var initialSelection: [] property var initialSelection: []
property var currentSelection: initialSelection property var currentSelection: initialSelection
property bool checkEnabled: true property bool checkEnabled: true
property int buttonHeight: 40 property string size: "medium"
property int minButtonWidth: 64 property int buttonHeight: size === "small" ? 32 : 40
property int buttonPadding: Theme.spacingL property int minButtonWidth: size === "small" ? 56 : 64
property int checkIconSize: Theme.iconSizeSmall property int buttonPadding: size === "small" ? Theme.spacingM : Theme.spacingL
property int textSize: Theme.fontSizeMedium property int checkIconSize: size === "small" ? Theme.iconSizeSmall - 2 : Theme.iconSizeSmall
property int textSize: size === "small" ? Theme.fontSizeSmall : Theme.fontSizeMedium
property bool userInteracted: false property bool userInteracted: false
signal selectionChanged(int index, bool selected) signal selectionChanged(int index, bool selected)
+23 -12
View File
@@ -16,21 +16,28 @@ PanelWindow {
property var targetScreen: null property var targetScreen: null
property var modelData: null property var modelData: null
property bool triggerUsesOverlayLayer: false property bool triggerUsesOverlayLayer: false
// Drop off the Overlay layer (back to Top) while an overlay modal
property bool suppressOverlayLayer: false
property real slideoutWidth: 480 property real slideoutWidth: 480
property bool expandable: false property bool expandable: false
property bool expandedWidth: false property bool expandedWidth: false
property real expandedWidthValue: 960 property real expandedWidthValue: 960
property real edgeGap: 0
property string slideEdge: "right"
readonly property bool slideFromLeft: slideEdge === "left"
property Component content: null property Component content: null
property string title: "" property string title: ""
property alias container: contentContainer property alias container: contentContainer
property real customTransparency: -1 property real customTransparency: -1
property bool mappedVisible: false property bool mappedVisible: false
signal aboutToHide signal aboutToHide
signal revealed
function show() { function show() {
mappedVisible = true; mappedVisible = true;
Qt.callLater(() => { Qt.callLater(() => {
isVisible = true; isVisible = true;
revealed();
}); });
} }
@@ -52,9 +59,9 @@ PanelWindow {
anchors.top: true anchors.top: true
anchors.bottom: true anchors.bottom: true
anchors.right: true anchors.right: !root.slideFromLeft
anchors.left: root.slideFromLeft
// Expandable: fixed max surface width; strip width is slideContainer only (keeps blur/mask aligned).
implicitWidth: expandable ? expandedWidthValue : slideoutWidth implicitWidth: expandable ? expandedWidthValue : slideoutWidth
implicitHeight: modelData ? modelData.height : 800 implicitHeight: modelData ? modelData.height : 800
@@ -62,21 +69,22 @@ PanelWindow {
readonly property bool slideoutBlurActive: root.visible && BlurService.enabled && Theme.connectedSurfaceBlurEnabled readonly property bool slideoutBlurActive: root.visible && BlurService.enabled && Theme.connectedSurfaceBlurEnabled
WlrLayershell.layer: (triggerUsesOverlayLayer || CompositorService.framePeerSurfacesUseOverlayForScreen(modelData)) ? WlrLayershell.Overlay : WlrLayershell.Top WlrLayershell.layer: (!suppressOverlayLayer && (triggerUsesOverlayLayer || CompositorService.framePeerSurfacesUseOverlayForScreen(modelData))) ? WlrLayershell.Overlay : WlrLayershell.Top
WlrLayershell.exclusiveZone: 0 WlrLayershell.exclusiveZone: 0
WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
readonly property real dpr: CompositorService.getScreenScale(root.screen) readonly property real dpr: CompositorService.getScreenScale(root.screen)
readonly property real alignedWidth: Theme.px(expandable && expandedWidth ? expandedWidthValue : slideoutWidth, dpr) readonly property real alignedWidth: Theme.px(expandable && expandedWidth ? expandedWidthValue : slideoutWidth, dpr)
readonly property real alignedHeight: Theme.px(modelData ? modelData.height : 800, dpr) readonly property real alignedHeight: Theme.px(modelData ? modelData.height : 800, dpr)
readonly property real alignedEdgeGap: Theme.px(edgeGap, dpr)
readonly property real slideoutSlideSnapX: Theme.snap(slideContainer.slideOffset, dpr) readonly property real slideoutSlideSnapX: Theme.snap(slideContainer.slideOffset, dpr)
mask: Region { mask: Region {
item: Rectangle { item: Rectangle {
x: root.width - slideContainer.width x: root.slideFromLeft ? root.alignedEdgeGap : (root.width - slideContainer.width - root.alignedEdgeGap)
y: 0 y: root.alignedEdgeGap
width: slideContainer.width width: slideContainer.width
height: root.height height: root.height - root.alignedEdgeGap * 2
} }
} }
@@ -84,16 +92,21 @@ PanelWindow {
id: slideContainer id: slideContainer
anchors.top: parent.top anchors.top: parent.top
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.right: parent.right anchors.right: root.slideFromLeft ? undefined : parent.right
anchors.left: root.slideFromLeft ? parent.left : undefined
anchors.topMargin: root.alignedEdgeGap
anchors.bottomMargin: root.alignedEdgeGap
anchors.rightMargin: root.alignedEdgeGap
anchors.leftMargin: root.alignedEdgeGap
width: root.alignedWidth width: root.alignedWidth
height: root.alignedHeight height: root.alignedHeight - root.alignedEdgeGap * 2
property real slideOffset: root.alignedWidth property real slideOffset: root.slideFromLeft ? -root.alignedWidth : root.alignedWidth
Connections { Connections {
target: root target: root
function onIsVisibleChanged() { function onIsVisibleChanged() {
slideContainer.slideOffset = root.isVisible ? 0 : slideContainer.width; slideContainer.slideOffset = root.isVisible ? 0 : (root.slideFromLeft ? -slideContainer.width : slideContainer.width);
} }
} }
@@ -111,7 +124,6 @@ PanelWindow {
} }
} }
// Expandable only; mask/blur bind to slideContainer geometry so they track this animation.
Behavior on width { Behavior on width {
enabled: root.expandable enabled: root.expandable
NumberAnimation { NumberAnimation {
@@ -217,7 +229,6 @@ PanelWindow {
} }
} }
// Blur region from slideContainer (not layered contentRect); position uses x + slideoutSlideSnapX, not mapToItem(root).
WindowBlur { WindowBlur {
targetWindow: root targetWindow: root
blurX: root.slideoutBlurActive ? slideContainer.x + root.slideoutSlideSnapX : 0 blurX: root.slideoutBlurActive ? slideContainer.x + root.slideoutSlideSnapX : 0