1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-19 01:25:21 -04:00

Compare commits

...

35 Commits

Author SHA1 Message Date
purian23 fc72b6d779 feat(popouts): implement hover popout functionality 2026-06-12 23:19:29 -04:00
bbedward 3701b3d7a3 wallpaper: re-introduce updatesEnabled 2026-06-12 20:00:53 -04:00
purian23 bae98daa5c fix(wallpaper): simplify wallpaper rendering logic & reliability
- Keep wallpaper surfaces persistent and remove `updatesEnabled` throttling that could leave wallpapers grey or frozen after DPMS, suspend, fullscreen, or output changes

Fixes #2612
Fixes #2299
Fixes #2272
Fixes #2028
2026-06-12 17:30:54 -04:00
jbwfu b34a04f723 fix(clipboard): hide pin action while keeping saved indicator (#2626) 2026-06-12 15:39:23 -04:00
purian23 1c0245f2db fix(translations): add newline at end of JSON file and output file 2026-06-12 15:06:36 -04:00
purian23 7777e87dc8 refactor(settings): reorg to break out sections and verbiage 2026-06-12 14:57:25 -04:00
jbwfu 820fa07846 feat(settings): add clipboard entry action visibility controls (#2621)
* feat(settings): add clipboard entry action visibility controls

* fix(clipboard): show pinned indicator for saved entries when pin action is hidden
2026-06-12 14:08:05 -04:00
purian23 66794582c9 fix(fullscreen): retain user dbar standalone configs while in framemode fullscreen 2026-06-12 12:39:38 -04:00
jbwfu 73eb471ae3 fix(clipboard): keep first item selected when navigating upward (#2622) 2026-06-12 11:35:07 -04:00
jbwfu 0f2f4b96c4 Fix/clipboard confirmation keyboard safety (#2623)
* fix(clipboard): improve confirmation dialog keyboard focus

* fix(clipboard): require confirmation for clear-all shortcut
2026-06-12 11:34:16 -04:00
purian23 d53809cf2b refactor(framemode): unify connected surface chrome via SDF pipeline
- Shadow system rewrite with SDF quads
- Replace ConnectedShape/layer FBOs w/frame & chrome SDF shaders
- Improve frame blur performance
- Plugin performance gate
2026-06-12 11:03:39 -04:00
Klesh Wong 08fd6e26d8 feat(notifications): user-configurable font size for notification summary and body (#2461)
* feat(notifications): add user-configurable font size for summary and body in notification popups

* feat: add Unset for falling back to previous default values

* fix: prek hook errors

---------

Co-authored-by: Klesh Wong <kleshwong@gmail.com>
2026-06-11 15:40:33 -04:00
Youseffo13 29e8470f2e fix(settings): fix text truncation in some section of settings and update icons (#2618)
* fixed spacing issues

* added one missing icon and replaced two
2026-06-11 15:35:51 -04:00
Bogdan Velicu 573785d4ce feat(notifications): add opt-in timeout progress bar on popups (#2587)
Adds a thin bar pinned to the bottom of the notification card that drains
full->empty over the auto-dismiss timer, as a visual countdown to
dismissal. Opt-in via notificationShowTimeoutBar (default off), with a
toggle in Settings > Notifications. Shown for any timed notification
(timer.interval > 0, including timed criticals); inset by the corner
radius, and frozen while hovered or during the exit animation. Plain
Rectangle - no offscreen textures or shader passes. A Connections on the
timer resets the bar on every (re)start, including the in-place restart
on a deduped notification.

Co-authored-by: bogdan-velicu <hydrotech074@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:22:22 -04:00
jbwfu 5483303714 Fix/clipboard pinned recents dedupe (#2605)
* fix(clipboard): unpin pinned duplicates from history entries

* fix(clipboard): dedupe recents when using pinned entries
2026-06-11 15:05:28 -04:00
David Mireles 5a5cc4f4e9 feat(plugins): expose scan/rescan/reload IPC handlers for runtime plugin discovery (#2611)
* feat(plugins): expose IPC handlers for runtime plugin discovery

Follow-up to #1659. That issue landed hot-reload for settings.json via
FileView.watchChanges + a 1ms Timer to skirt the JSON parse race. It does
not cover plugin discovery in runtime: adding a new plugin directory to
~/.config/DankMaterialShell/plugins/ while the shell is running is not
consistently picked up by the existing FolderListModel watcher in
PluginService.qml, and there is no IPC handle for forcing a rescan from
outside the shell.

Adds an IpcHandler on PluginService with five small functions:

- scan(): wraps existing scanPlugins(), returns count snapshot
- rescan(pluginId): wraps existing forceRescanPlugin(id), validates id
- reload(pluginId): wraps existing reloadPlugin(id), validates id
- list(): newline-joined id\tloaded\ttype\tname for every known plugin
- status(pluginId): loaded\ttype\terror for one plugin

Scope intentionally small: no file-watcher changes, no new daemons, no
schema additions. Target string "plugins" does not collide with any
existing target in DMSShellIPC.qml.

Validation:
- qs ipc --pid <PID> call plugins list returns one row per known plugin
- qs ipc --pid <PID> call plugins scan returns SCAN_TRIGGERED with count
- qs ipc --pid <PID> call plugins rescan <id> returns RESCAN_TRIGGERED
- Empty-arg paths return ERROR strings instead of throwing
- git merge-tree against origin/master is clean

* hardening(plugins): fix 7 review findings in scan-ipc IPC handlers

Follow-up to commit 43603f56 which ported PR #2601 (AvengeMedia scan-ipc)
to the fork. The original port was functionally correct but had seven
review issues that would block upstream adoption. This patch addresses
each one with a minimal, focused change.

* B1 IPC target collision: renamed `target: "plugins"` to
  `target: "plugin-scan"`. The original name collided with the
  existing IpcHandler in DMSShellIPC.qml:1180 which already registers
  enable/disable/toggle/list/status under "plugins". The split keeps
  both APIs discoverable without one shadowing the other.

* H1 Fire-and-forget scan: documented that scan() returns the
  pre-debounce count and that callers must poll list/status (or wait
  ~200ms) to observe the post-debounce state. A proper requestId +
  await mechanism was considered and rejected for scope reasons.

* H2 TOCTOU in rescan(): the handler now reads availablePlugins[id]
  inside forceRescanPlugin via the id string only — no captured
  object reference. A parallel resyncDebounce tick can otherwise
  mutate the entry between the read and the use.

* M1 list() cap: added a 256-entry cap and a leading header line
  (`# count=N returned=M`) so callers can detect truncation. A
  hostile / buggy plugin mass-creating entries could otherwise
  allocate 80 KB+ per IPC call.

* M2 status() prefix: "unknown\t\t" became
  `ERROR: unknown pluginId '...'` to match the rest of the
  handlers' prefix convention. Empty trailing field means no error.

* M3 id sanitization: every handler that takes pluginId now
  validates against `/^[a-zA-Z0-9_\-:]{1,64}$` before use. This
  rejects shell-injection payloads ("foo\tmalicious") and prototype
  pollution attempts ("__proto__", "constructor"). The list() and
  status() handlers also sanitize \t/\n in name and error fields
  so callers can rely on the TSV structure.

Verification: brace count balanced (252/252). Manual read of all
five handlers confirms no logic regression. QML runtime tests are
not part of the DMS test suite, so end-to-end validation requires
rebuilding the shell — deferred to the user.

Not pushed. Stage-local-first rule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(plugins): strip inline comments per review feedback

Purian23 in PR #2611 review: 'let's address the amount of line
comments in the code, there's not a need for all of them to exist.'

Removed 48 comment lines. The substantive justification (why the
regex, why fire-and-forget, why re-read inside forceRescanPlugin,
why the 256 cap, why the target rename) now lives in the PR body
under 'Review-driven fixes in this iteration' and 'What changed'
where the reviewer already reads it.

No code logic changed. Brace count 252/252. Diff is -48/+0 on
quickshell/Services/PluginService.qml.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------
2026-06-11 14:44:41 -04:00
bbedward cd672c341f settings: add DankSpinner, re-org some settings 2026-06-10 18:53:43 -04:00
bbedward 12438d63c2 mango: remove legacy dwl service 2026-06-10 17:01:03 -04:00
Ralph Zhou 35255e4053 fix(lock): bypass IME for password input (#2609) 2026-06-10 16:29:05 -04:00
goatnath 8856d45887 add local to-do planner / tasks to calendar overvie… (#2583)
* feat(quickshell): add local to-do planner / tasks to calendar overview card

* feat(quickshell): add auto-focus and task reordering support in calendar planner

* feat(quickshell): implement smooth drag-and-drop task reordering and inline editing

* fix(quickshell): resolve overlap and jitter in task drag-and-drop

* fix(quickshell): fix boundary swaps and prevent task list scrambling on reload

* fix(quickshell): resolve race, fix qml error, simplify dragging, and remove python dependency

* fix(quickshell): use Log service instead of console.warn in CalendarService

* style: format QML files w/qmlformat-qt6

---------
2026-06-09 00:43:41 -04:00
jbwfu 38af56c6fd fix(clipboard): exit saved filter when pinned entries are empty (#2604) 2026-06-08 23:54:06 -04:00
jbwfu 9111e4809d fix(powermenu): close control center on lock and power actions (#2598) 2026-06-08 23:53:43 -04:00
purian23 d08c7c5e55 refactor(frame): improve connected mode surface recovery
- share modal and launcher ownership handling
- recover missing background and blur layers
2026-06-07 17:47:24 -04:00
purian23 69f3dee25a feat(settings): add compositor section & restructured settings
- add dedicated Compositor pages for comp specifc features
- add Dank Bar Appearance subsection
- improve lazy loading, caching, search routing, & IPC navigation
- standardized responsive Setting categories from global animations
2026-06-07 03:52:00 -04:00
purian23 8155970ba2 fix(fonts): auto-rebuild font cache when configured fonts are missing
- Add Fonts category to dms doctor for manual diagnostics
- Fix a default font setting warning
2026-06-06 19:24:52 -04:00
pathmann d356957dad fix: ignore keyboard shortcuts of disabled powermenu actions (#2580)
* fix: ignore keyboard shortcuts of disabled powermenu actions

* fix typo when checking for lock shortcut

* ignore shortcuts for hidden powermenu actions in grid navigation

* ignore keyboard shortcuts of disabled actions in lock power menu

* ignore keyboard shortcuts of disabled actions in lock power menu (list navigation)
2026-06-06 18:28:38 -04:00
purian23 e7ccb702a3 refactor: update KeybindsModal dynamic sizing 2026-06-05 23:17:14 -04:00
Connor Welsh bf3ce6deb2 fix(osd): size from PanelWindow.screen (#2582) 2026-06-05 23:14:51 -04:00
purian23 f5295fb35d fix(greeter): remove auto-login state resolution from first install configuration
- Update auto-login command to:
`dms greeter sync --autologin`
2026-06-05 23:05:56 -04:00
purian23 6c5836722a fix(authModals): enable overlay layer for for auth popups 2026-06-05 21:27:37 -04:00
Youseffo13 5716249bd9 (Control Center): revamp of 25% pill option (#2568)
* revamp of control center

* update comment of SmallCompoundButton.qml
2026-06-05 19:46:01 -04:00
purian23 4d0aab773b fix(wallpaper): external management toggle & partial monitor DPMS recovery
Fixes #2579, #2581
2026-06-05 19:36:23 -04:00
purian23 e50ac208e3 feat(mangowm): add live config reloads & misc QOL updates
- Hide workspace tags during Mango overview
- Add HJKL focus/move defaults
- Add Mango natural touchpad scrolling &  cursor configs
- Fix Mango startup
2026-06-05 10:53:26 -04:00
bbedward bcb5617194 plugins: add support for composite plugins
- single plugin can register multiple types - e.g. daemon, bar widget,
  desktop widget
2026-06-05 10:33:34 -04:00
bbedward d3c23ba737 settings: add missing tabs to index and tweak search scoring 2026-06-05 09:49:49 -04:00
207 changed files with 15322 additions and 12114 deletions
@@ -235,7 +235,7 @@ Conditionally show/hide the bar pill:
```qml
PluginComponent {
visibilityCommand: "pgrep -x myapp"
visibilityInterval: 5000 // check every 5 seconds
visibilityInterval: 5 // seconds between checks; polling pauses while the bar is hidden
}
```
+2
View File
@@ -115,3 +115,5 @@ core.*
.direnv/
quickshell/dms-plugins
__pycache__
.vscode/
+1 -1
View File
@@ -72,7 +72,7 @@ func runResolveInclude(cmd *cobra.Command, args []string) {
result, err = checkHyprlandInclude(filename)
case "niri":
result, err = checkNiriInclude(filename)
case "mangowc", "dwl", "mango":
case "mangowc", "mango":
result, err = checkMangoWCInclude(filename)
default:
log.Fatalf("Unknown compositor: %s", compositor)
+101
View File
@@ -125,6 +125,7 @@ const (
catConfigFiles
catServices
catEnvironment
catFonts
)
func (c category) String() string {
@@ -147,6 +148,8 @@ func (c category) String() string {
return "Services"
case catEnvironment:
return "Environment"
case catFonts:
return "Fonts"
default:
return "Unknown"
}
@@ -213,6 +216,7 @@ func runDoctor(cmd *cobra.Command, args []string) {
checkConfigurationFiles(),
checkSystemdServices(),
checkEnvironmentVars(),
checkFonts(),
)
switch {
@@ -1135,3 +1139,100 @@ func formatResultsPlain(results []checkResult) string {
return sb.String()
}
func checkFonts() []checkResult {
var results []checkResult
url := doctorDocsURL + "#fonts"
configDir, err := os.UserConfigDir()
if err != nil {
return nil
}
settingsPath := filepath.Join(configDir, "DankMaterialShell", "settings.json")
fontFamily := "Inter Variable"
monoFontFamily := "Fira Code"
if data, err := os.ReadFile(settingsPath); err == nil {
var settings struct {
FontFamily string `json:"fontFamily"`
MonoFontFamily string `json:"monoFontFamily"`
}
if err := json.Unmarshal(data, &settings); err == nil {
if settings.FontFamily != "" {
fontFamily = settings.FontFamily
}
if settings.MonoFontFamily != "" {
monoFontFamily = settings.MonoFontFamily
}
}
}
if !utils.CommandExists("fc-list") {
results = append(results, checkResult{catFonts, "Fontconfig Tools", statusWarn, "fc-list not installed", "Cannot verify if fonts are cached.", url})
return results
}
// Retrieve font list
output, err := exec.Command("fc-list", ":", "family").Output()
if err != nil {
results = append(results, checkResult{catFonts, "Fontconfig Cache", statusError, "Failed to query font list", "Fontconfig cache query failed. Try running 'fc-cache -fv'.", url})
return results
}
outStr := string(output)
if len(strings.TrimSpace(outStr)) == 0 {
results = append(results, checkResult{catFonts, "Fontconfig Cache", statusError, "Cache is empty", "No fonts found in fontconfig cache. Try running 'fc-cache -fv'.", url})
return results
}
lowerFonts := strings.ToLower(outStr)
// Helper to check if a font exists
hasFont := func(name string) bool {
target := strings.ToLower(strings.TrimSpace(name))
if target == "" {
return false
}
for _, line := range strings.Split(lowerFonts, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
// Each line can have comma-separated families
families := strings.Split(line, ",")
for _, fam := range families {
if strings.TrimSpace(fam) == target {
return true
}
}
}
return false
}
// Normal Font Check
if hasFont(fontFamily) {
results = append(results, checkResult{catFonts, "Normal Font", statusOK, fontFamily, "Available", url})
} else {
results = append(results, checkResult{
catFonts, "Normal Font", statusWarn,
fmt.Sprintf("'%s' not found", fontFamily),
"Font is not registered. Try running 'fc-cache -fv' or install the font.",
url,
})
}
// Monospace Font Check
if hasFont(monoFontFamily) {
results = append(results, checkResult{catFonts, "Monospace Font", statusOK, monoFontFamily, "Available", url})
} else {
results = append(results, checkResult{
catFonts, "Monospace Font", statusWarn,
fmt.Sprintf("'%s' not found", monoFontFamily),
"Font is not registered. Try running 'fc-cache -fv' or install the font.",
url,
})
}
return results
}
+3 -3
View File
@@ -75,7 +75,7 @@ var greeterSyncCmd = &cobra.Command{
auth, _ := cmd.Flags().GetBool("auth")
local, _ := cmd.Flags().GetBool("local")
profile, _ := cmd.Flags().GetBool("profile")
autologinOnly, _ := cmd.Flags().GetBool("autologin-only")
autologinOnly, _ := cmd.Flags().GetBool("autologin")
term, _ := cmd.Flags().GetBool("terminal")
if term {
if err := syncInTerminal(yes, auth, local, profile, autologinOnly); err != nil {
@@ -101,7 +101,7 @@ func init() {
greeterSyncCmd.Flags().BoolP("auth", "a", false, "Configure PAM for fingerprint and U2F (adds both if modules exist); overrides UI toggles")
greeterSyncCmd.Flags().BoolP("local", "l", false, "Developer mode: force greetd config to use a local DMS checkout path")
greeterSyncCmd.Flags().BoolP("profile", "p", false, "Sync only your per-user greeter slot (no sudo; for secondary accounts)")
greeterSyncCmd.Flags().Bool("autologin-only", false, "Apply only greeter auto-login on startup settings to greetd (no theme or auth sync)")
greeterSyncCmd.Flags().Bool("autologin", false, "Apply only greeter auto-login on startup settings to greetd (no theme or auth sync)")
}
var greeterEnableCmd = &cobra.Command{
@@ -544,7 +544,7 @@ func syncInTerminal(nonInteractive bool, forceAuth bool, local bool, profileOnly
syncFlags = append(syncFlags, "--profile")
}
if autologinOnly {
syncFlags = append(syncFlags, "--autologin-only")
syncFlags = append(syncFlags, "--autologin")
}
shellSyncCmd := "dms greeter sync"
if len(syncFlags) > 0 {
+2 -2
View File
@@ -39,7 +39,7 @@ Modes:
full - Capture the focused output
all - Capture all outputs combined
output - Capture a specific output by name
window - Capture the focused window (Hyprland/DWL)
window - Capture the focused window (Hyprland/Mango)
last - Capture the last selected region
Output format (--format):
@@ -97,7 +97,7 @@ If no previous region exists, falls back to interactive selection.`,
var ssWindowCmd = &cobra.Command{
Use: "window",
Short: "Capture the focused window",
Long: `Capture the currently focused window. Supported on Hyprland and DWL.`,
Long: `Capture the currently focused window. Supported on Hyprland and Mango.`,
Run: runScreenshotWindow,
}
+8 -1
View File
@@ -294,7 +294,14 @@ func runSetup() error {
wm, wmSelected := promptCompositor()
terminal, terminalSelected := promptTerminal()
useSystemd := promptSystemd()
useSystemd := true
if wmSelected {
if wm == deps.WindowManagerMango {
useSystemd = false
} else {
useSystemd = promptSystemd()
}
}
if !wmSelected && !terminalSelected {
fmt.Println("No configurations selected. Exiting.")
+152 -6
View File
@@ -2,7 +2,9 @@ package main
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
@@ -192,6 +194,7 @@ func runShellInteractive(session bool) {
}
}()
ensureFontCache()
log.Infof("Spawning quickshell with -p %s", configPath)
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
@@ -227,8 +230,10 @@ func runShellInteractive(session bool) {
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
tracker := &stderrTracker{parent: os.Stderr}
cmd.Stderr = tracker
startTime := time.Now()
if err := cmd.Start(); err != nil {
log.Fatalf("Error starting quickshell: %v", err)
}
@@ -277,7 +282,9 @@ func runShellInteractive(session bool) {
case <-errChan:
cancel()
os.Remove(socketPath)
os.Exit(getProcessExitCode(cmd.ProcessState))
exitCode := getProcessExitCode(cmd.ProcessState)
logStartupFailure(startTime, exitCode, tracker)
os.Exit(exitCode)
case <-time.After(500 * time.Millisecond):
}
@@ -294,7 +301,9 @@ func runShellInteractive(session bool) {
cmd.Process.Signal(syscall.SIGTERM)
}
os.Remove(socketPath)
os.Exit(getProcessExitCode(cmd.ProcessState))
exitCode := getProcessExitCode(cmd.ProcessState)
logStartupFailure(startTime, exitCode, tracker)
os.Exit(exitCode)
}
}
}
@@ -434,6 +443,7 @@ func runShellDaemon(session bool) {
}
}()
ensureFontCache()
log.Infof("Spawning quickshell with -p %s", configPath)
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
@@ -478,8 +488,10 @@ func runShellDaemon(session bool) {
cmd.Stdin = devNull
cmd.Stdout = devNull
cmd.Stderr = devNull
tracker := &stderrTracker{parent: devNull}
cmd.Stderr = tracker
startTime := time.Now()
if err := cmd.Start(); err != nil {
log.Fatalf("Error starting daemon: %v", err)
}
@@ -528,7 +540,9 @@ func runShellDaemon(session bool) {
case <-errChan:
cancel()
os.Remove(socketPath)
os.Exit(getProcessExitCode(cmd.ProcessState))
exitCode := getProcessExitCode(cmd.ProcessState)
logStartupFailure(startTime, exitCode, tracker)
os.Exit(exitCode)
case <-time.After(500 * time.Millisecond):
}
@@ -543,7 +557,9 @@ func runShellDaemon(session bool) {
cmd.Process.Signal(syscall.SIGTERM)
}
os.Remove(socketPath)
os.Exit(getProcessExitCode(cmd.ProcessState))
exitCode := getProcessExitCode(cmd.ProcessState)
logStartupFailure(startTime, exitCode, tracker)
os.Exit(exitCode)
}
}
}
@@ -748,3 +764,133 @@ func printIPCHelp() {
fmt.Printf(" %-16s %s\n", targetName, strings.Join(funcNames, ", "))
}
}
// ensureFontCache rebuilds the fontconfig cache if user-configured fonts are missing while skipping defaults
func ensureFontCache() {
if _, err := exec.LookPath("fc-list"); err != nil {
return
}
if _, err := exec.LookPath("fc-cache"); err != nil {
return
}
var fontsToCheck []string
if configDir, err := os.UserConfigDir(); err == nil {
settingsPath := filepath.Join(configDir, "DankMaterialShell", "settings.json")
if data, err := os.ReadFile(settingsPath); err == nil {
var settings struct {
FontFamily string `json:"fontFamily"`
MonoFontFamily string `json:"monoFontFamily"`
}
if err := json.Unmarshal(data, &settings); err == nil {
if settings.FontFamily != "" && settings.FontFamily != "Inter Variable" {
fontsToCheck = append(fontsToCheck, settings.FontFamily)
}
if settings.MonoFontFamily != "" && settings.MonoFontFamily != "Fira Code" {
fontsToCheck = append(fontsToCheck, settings.MonoFontFamily)
}
}
}
}
if len(fontsToCheck) == 0 {
return
}
output, err := exec.Command("fc-list", ":", "family").Output()
if err != nil || len(strings.TrimSpace(string(output))) == 0 {
log.Warnf("Font cache appears empty or corrupt, rebuilding...")
rebuildFontCache()
return
}
cacheFonts := strings.ToLower(string(output))
var missing []string
for _, font := range fontsToCheck {
if !fontInCache(strings.ToLower(font), cacheFonts) {
missing = append(missing, font)
}
}
if len(missing) > 0 {
log.Warnf("Font(s) not found in cache: %s — rebuilding...", strings.Join(missing, ", "))
rebuildFontCache()
}
}
func fontInCache(target, cache string) bool {
for _, line := range strings.Split(cache, "\n") {
for _, fam := range strings.Split(strings.TrimSpace(line), ",") {
if strings.TrimSpace(fam) == target {
return true
}
}
}
return false
}
func rebuildFontCache() {
cmd := exec.Command("fc-cache", "-f")
if output, err := cmd.CombinedOutput(); err != nil {
log.Warnf("Failed to rebuild font cache: %v\n%s", err, string(output))
} else {
log.Infof("Font cache rebuilt successfully")
}
}
type stderrTracker struct {
mu sync.Mutex
buf strings.Builder
parent io.Writer
}
func (s *stderrTracker) Write(p []byte) (n int, err error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.buf.Len() < 8192 {
s.buf.Write(p)
}
if s.parent != nil {
return s.parent.Write(p)
}
return len(p), nil
}
func (s *stderrTracker) String() string {
s.mu.Lock()
defer s.mu.Unlock()
return s.buf.String()
}
// logStartupFailure logs diagnostic advice if qs crashes within 5s of launch.
func logStartupFailure(startTime time.Time, exitCode int, tracker *stderrTracker) {
if time.Since(startTime) >= 5*time.Second || exitCode == 0 || exitCode > 128 {
return
}
if containsFontCrashSignature(tracker.String()) {
log.Errorf("DMS startup failed due to a potential font/rendering crash. Try running 'fc-cache -fv' and restarting DMS.")
} else {
log.Errorf("DMS startup failed (exit code %d). Run 'dms doctor' for more diagnostics.", exitCode)
}
}
func containsFontCrashSignature(logStr string) bool {
logStr = strings.ToLower(logStr)
signatures := []string{
"fontconfig",
"freetype",
"ft_load_glyph",
"ft_face",
"fc-list",
"fc-cache",
"glyph",
"typeface",
}
for _, sig := range signatures {
if strings.Contains(logStr, sig) {
return true
}
}
return false
}
+12
View File
@@ -520,6 +520,18 @@ func TestHyprlandConfigStructure(t *testing.T) {
assert.Contains(t, HyprlandLuaConfig, "input =")
}
func TestMangoConfigStructure(t *testing.T) {
assert.Contains(t, MangoConfig, "exec-once=dms run")
assert.NotContains(t, MangoConfig, "exec_once=dms run")
assert.Contains(t, MangoConfig, "source=./dms/binds.conf")
assert.Contains(t, MangoBindsConfig, "bind=SUPER,H,focusdir,left")
assert.Contains(t, MangoBindsConfig, "bind=SUPER,J,focusdir,down")
assert.Contains(t, MangoBindsConfig, "bind=SUPER,K,focusdir,up")
assert.Contains(t, MangoBindsConfig, "bind=SUPER,L,focusdir,right")
assert.Contains(t, MangoBindsConfig, "gesturebind=none,right,3,viewtoleft_have_client")
assert.Contains(t, MangoBindsConfig, "gesturebind=none,left,3,viewtoright_have_client")
}
func TestGhosttyConfigStructure(t *testing.T) {
assert.Contains(t, GhosttyConfig, "window-decoration = false")
assert.Contains(t, GhosttyConfig, "background-opacity = 1.0")
+13 -55
View File
@@ -1,7 +1,6 @@
# DMS default keybinds (MangoWM) — managed by DMS, regenerated by `dms setup`.
# Format: bind=MODS,key,action[,args]
# Descriptions go on the line ABOVE each bind (mango does not strip inline
# comments — a trailing `# ...` would be passed to spawn as extra arguments).
# Put bind descriptions above bind lines; inline # comments break Mango spawn args.
# === Application Launchers ===
# Open Terminal
@@ -52,131 +51,90 @@ bind=CTRL,Print,spawn,dms screenshot full
bind=ALT,Print,spawn,dms screenshot window
# === Audio Controls ===
# Volume Up
bind=none,XF86AudioRaiseVolume,spawn,dms ipc call audio increment 3
# Volume Down
bind=none,XF86AudioLowerVolume,spawn,dms ipc call audio decrement 3
# Mute Output
bind=none,XF86AudioMute,spawn,dms ipc call audio mute
# Mute Microphone
bind=none,XF86AudioMicMute,spawn,dms ipc call audio micmute
# Play/Pause
bind=none,XF86AudioPlay,spawn,dms ipc call mpris playPause
# Play/Pause
bind=none,XF86AudioPause,spawn,dms ipc call mpris playPause
# Previous Track
bind=none,XF86AudioPrev,spawn,dms ipc call mpris previous
# Next Track
bind=none,XF86AudioNext,spawn,dms ipc call mpris next
# === Brightness Controls ===
# Brightness Up
bind=none,XF86MonBrightnessUp,spawn,dms ipc call brightness increment 5
# Brightness Down
bind=none,XF86MonBrightnessDown,spawn,dms ipc call brightness decrement 5
# === Window Management ===
# Close Window
bind=SUPER,q,killclient,
# Toggle Fullscreen
bind=SUPER,f,togglefullscreen,
# Toggle Maximize
bind=SUPER,a,togglemaximizescreen,
# Toggle Floating
bind=SUPER+SHIFT,space,togglefloating,
# Toggle Overview
bind=SUPER,o,toggleoverview
bind=ALT,Tab,toggleoverview
# Exit Compositor
bind=SUPER+SHIFT,e,quit,
# === Focus Navigation ===
# Focus Next Window
bind=SUPER,Tab,focusstack,next
# Focus Previous Window
bind=SUPER+SHIFT,Tab,focusstack,prev
# Focus Left
bind=SUPER,Left,focusdir,left
# Focus Right
bind=SUPER,H,focusdir,left
bind=SUPER,Right,focusdir,right
# Focus Up
bind=SUPER,L,focusdir,right
bind=SUPER,Up,focusdir,up
# Focus Down
bind=SUPER,K,focusdir,up
bind=SUPER,Down,focusdir,down
bind=SUPER,J,focusdir,down
# === Window Movement ===
# Move Window Left
bind=SUPER+SHIFT,Left,exchange_client,left
# Move Window Right
bind=SUPER+SHIFT,Right,exchange_client,right
# Move Window Up
bind=SUPER+SHIFT,Up,exchange_client,up
# Move Window Down
bind=SUPER+SHIFT,Down,exchange_client,down
bind=SUPER+SHIFT,H,exchange_client,left
bind=SUPER+SHIFT,L,exchange_client,right
bind=SUPER+SHIFT,K,exchange_client,up
bind=SUPER+SHIFT,J,exchange_client,down
# === Monitor Navigation ===
# Focus Monitor Left
bind=SUPER+ALT,Left,focusmon,left
# Focus Monitor Right
bind=SUPER+ALT,Right,focusmon,right
# Move to Monitor Left
bind=SUPER+ALT+SHIFT,Left,tagmon,left
# Move to Monitor Right
bind=SUPER+ALT+SHIFT,Right,tagmon,right
# === Layout ===
# Cycle Layout
bind=SUPER,j,switch_layout
# Increase Gaps
# Cycle Layout - Gaps, Floating, Tiling
bind=SUPER+ALT,j,switch_layout
bind=SUPER+SHIFT,equal,incgaps,1
# Decrease Gaps
bind=SUPER+SHIFT,minus,incgaps,-1
# === Tags (1-9): view tag ===
# View Tag 1
bind=SUPER,1,view,1
# View Tag 2
bind=SUPER,2,view,2
# View Tag 3
bind=SUPER,3,view,3
# View Tag 4
bind=SUPER,4,view,4
# View Tag 5
bind=SUPER,5,view,5
# View Tag 6
bind=SUPER,6,view,6
# View Tag 7
bind=SUPER,7,view,7
# View Tag 8
bind=SUPER,8,view,8
# View Tag 9
bind=SUPER,9,view,9
# === Tags (1-9): move focused window to tag ===
# Move to Tag 1
bind=SUPER+SHIFT,1,tag,1
# Move to Tag 2
bind=SUPER+SHIFT,2,tag,2
# Move to Tag 3
bind=SUPER+SHIFT,3,tag,3
# Move to Tag 4
bind=SUPER+SHIFT,4,tag,4
# Move to Tag 5
bind=SUPER+SHIFT,5,tag,5
# Move to Tag 6
bind=SUPER+SHIFT,6,tag,6
# Move to Tag 7
bind=SUPER+SHIFT,7,tag,7
# Move to Tag 8
bind=SUPER+SHIFT,8,tag,8
# Move to Tag 9
bind=SUPER+SHIFT,9,tag,9
# === Touchpad Gestures ===
# Syntax: gesturebind=MODIFIERS,DIRECTION,FINGERS,COMMAND,PARAMETERS
# 3-finger horizontal swipe: switch between occupied workspaces
gesturebind=none,left,3,viewtoleft_have_client
gesturebind=none,right,3,viewtoright_have_client
gesturebind=none,right,3,viewtoleft_have_client
gesturebind=none,left,3,viewtoright_have_client
# 4-finger vertical swipe: toggle the overview
gesturebind=none,up,4,toggleoverview
gesturebind=none,down,4,toggleoverview
+2 -2
View File
@@ -5,10 +5,10 @@
env=XDG_CURRENT_DESKTOP,mango
env=XDG_SESSION_TYPE,wayland
# exec_once runs only at startup. Do NOT use exec= for the shell: mango re-runs
# exec-once runs only at startup. Do NOT use exec= for the shell: mango re-runs
# every exec= on each config reload, and DMS reloads the config, which would
# spawn a new shell on every reload.
exec_once=dms run
exec-once=dms run
source=./dms/colors.conf
source=./dms/layout.conf
-12
View File
@@ -2153,18 +2153,6 @@ vt = 1
commandLine := fmt.Sprintf(`command = "%s"`, commandValue)
newConfig := upsertDefaultSession(configContent, greeterUser, commandLine)
homeDir, homeErr := os.UserHomeDir()
if homeErr == nil {
enabled, loginUser, sessionExec, resolveErr := resolveGreeterAutoLoginState(GreeterCacheDir, homeDir)
if resolveErr != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to resolve greeter auto-login state: %v", resolveErr))
} else if enabled && loginUser != "" && sessionExec != "" {
newConfig = upsertInitialSession(newConfig, loginUser, sessionExec, true)
} else {
newConfig = upsertInitialSession(newConfig, "", "", false)
}
}
if err := writeGreetdConfig(configPath, newConfig, logFunc, sudoPassword, fmt.Sprintf("✓ Updated greetd configuration (user: %s, command: %s)", greeterUser, commandValue)); err != nil {
return err
}
+272 -55
View File
@@ -7,6 +7,7 @@ import (
"sort"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
@@ -228,11 +229,20 @@ func (m *MangoWCProvider) SetBind(key, action, description string, options map[s
}
normalizedKey := strings.ToLower(key)
prefix := "bind"
if existing, ok := existingBinds[normalizedKey]; ok && existing.Prefix != "" {
prefix = existing.Prefix
}
if optionPrefix := m.bindPrefixFromOptions(options); optionPrefix != "" {
prefix = optionPrefix
}
existingBinds[normalizedKey] = &mangowcOverrideBind{
Key: key,
Action: action,
Description: description,
Options: options,
Prefix: prefix,
}
return m.writeOverrideBinds(existingBinds)
@@ -246,7 +256,7 @@ func (m *MangoWCProvider) RemoveBind(key string) error {
normalizedKey := strings.ToLower(key)
delete(existingBinds, normalizedKey)
return m.writeOverrideBinds(existingBinds)
return m.writeOverrideBindsWithRemoved(existingBinds, map[string]bool{normalizedKey: true})
}
func (m *MangoWCProvider) ResetBind(key string) error {
@@ -258,6 +268,7 @@ type mangowcOverrideBind struct {
Action string
Description string
Options map[string]any
Prefix string
}
func (m *MangoWCProvider) loadOverrideBinds() (map[string]*mangowcOverrideBind, error) {
@@ -272,62 +283,99 @@ func (m *MangoWCProvider) loadOverrideBinds() (map[string]*mangowcOverrideBind,
return nil, err
}
lines := strings.Split(string(data), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
var pendingComment string
for _, line := range strings.Split(string(data), "\n") {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
pendingComment = ""
continue
}
if strings.HasPrefix(trimmed, "#") {
pendingComment = strings.TrimSpace(strings.TrimPrefix(trimmed, "#"))
if isMangoWCSectionComment(pendingComment) {
pendingComment = ""
}
continue
}
if !strings.HasPrefix(line, "bind") {
bind, ok := m.parseOverrideBindLine(line, pendingComment)
pendingComment = ""
if !ok || bind == nil {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) < 2 {
continue
}
content := strings.TrimSpace(parts[1])
commentParts := strings.SplitN(content, "#", 2)
bindContent := strings.TrimSpace(commentParts[0])
var comment string
if len(commentParts) > 1 {
comment = strings.TrimSpace(commentParts[1])
}
fields := strings.SplitN(bindContent, ",", 4)
if len(fields) < 3 {
continue
}
mods := strings.TrimSpace(fields[0])
keyName := strings.TrimSpace(fields[1])
command := strings.TrimSpace(fields[2])
var params string
if len(fields) > 3 {
params = strings.TrimSpace(fields[3])
}
keyStr := m.buildKeyString(mods, keyName)
normalizedKey := strings.ToLower(keyStr)
action := command
if params != "" {
action = command + " " + params
}
binds[normalizedKey] = &mangowcOverrideBind{
Key: keyStr,
Action: action,
Description: comment,
}
binds[strings.ToLower(bind.Key)] = bind
}
return binds, nil
}
func (m *MangoWCProvider) parseOverrideBindLine(line, precedingComment string) (*mangowcOverrideBind, bool) {
trimmed := strings.TrimSpace(line)
parts := strings.SplitN(trimmed, "=", 2)
if len(parts) < 2 {
return nil, false
}
prefix := strings.TrimSpace(parts[0])
if !m.isBindPrefix(prefix) {
return nil, false
}
content := strings.TrimSpace(parts[1])
commentParts := strings.SplitN(content, "#", 2)
bindContent := strings.TrimSpace(commentParts[0])
description := strings.TrimSpace(precedingComment)
if isMangoWCSectionComment(description) {
description = ""
}
if len(commentParts) > 1 {
description = strings.TrimSpace(commentParts[1])
}
if strings.HasPrefix(description, MangoWCHideComment) {
return nil, true
}
fields := strings.SplitN(bindContent, ",", 4)
if len(fields) < 3 {
return nil, false
}
mods := strings.TrimSpace(fields[0])
keyName := strings.TrimSpace(fields[1])
command := strings.TrimSpace(fields[2])
var params string
if len(fields) > 3 {
params = strings.TrimSpace(fields[3])
}
action := command
if params != "" {
action = command + " " + params
}
return &mangowcOverrideBind{
Key: m.buildKeyString(mods, keyName),
Action: action,
Description: description,
Prefix: prefix,
}, true
}
func (m *MangoWCProvider) isBindPrefix(prefix string) bool {
if !strings.HasPrefix(prefix, "bind") {
return false
}
for _, ch := range strings.TrimPrefix(prefix, "bind") {
if !strings.ContainsRune("lsrp", ch) {
return false
}
}
return true
}
func (m *MangoWCProvider) buildKeyString(mods, key string) string {
if mods == "" || strings.EqualFold(mods, "none") {
return key
@@ -362,21 +410,113 @@ func (m *MangoWCProvider) getBindSortPriority(action string) int {
}
func (m *MangoWCProvider) writeOverrideBinds(binds map[string]*mangowcOverrideBind) error {
return m.writeOverrideBindsWithRemoved(binds, nil)
}
func (m *MangoWCProvider) writeOverrideBindsWithRemoved(binds map[string]*mangowcOverrideBind, removed map[string]bool) error {
overridePath := m.GetOverridePath()
content := m.generateBindsContent(binds)
existingContent := ""
if data, err := os.ReadFile(overridePath); err == nil {
existingContent = string(data)
}
content := m.generatePreservedBindsContent(existingContent, binds, removed)
return os.WriteFile(overridePath, []byte(content), 0o644)
}
func (m *MangoWCProvider) generateBindsContent(binds map[string]*mangowcOverrideBind) string {
if len(binds) == 0 {
return ""
func (m *MangoWCProvider) generatePreservedBindsContent(existingContent string, binds map[string]*mangowcOverrideBind, removed map[string]bool) string {
useStockScaffold := m.shouldUseStockScaffold(existingContent)
source := existingContent
if useStockScaffold {
source = m.stockBindsScaffold(binds)
}
remaining := make(map[string]*mangowcOverrideBind, len(binds))
for key, bind := range binds {
remaining[key] = bind
}
if useStockScaffold {
m.dropReplacedStockBinds(remaining)
}
var lines []string
for _, line := range strings.Split(source, "\n") {
templateBind, ok := m.parseOverrideBindLine(line, m.previousComment(lines))
if !ok || templateBind == nil {
lines = append(lines, line)
continue
}
normalizedKey := strings.ToLower(templateBind.Key)
m.dropPreviousDescriptionComment(&lines)
if bind, exists := remaining[normalizedKey]; exists {
if useStockScaffold && bind.Description == "" {
bind = m.copyBindWithDescription(bind, templateBind.Description)
}
m.writeBindLineToLines(&lines, bind)
delete(remaining, normalizedKey)
continue
}
if useStockScaffold && !removed[normalizedKey] {
m.writeBindLineToLines(&lines, templateBind)
}
}
if len(remaining) > 0 {
m.trimTrailingEmptyLines(&lines)
if len(lines) > 0 {
lines = append(lines, "")
}
lines = append(lines, "# === Custom Keybinds ===")
for _, bind := range m.sortedBinds(remaining) {
m.writeBindLineToLines(&lines, bind)
}
}
m.trimTrailingEmptyLines(&lines)
if len(lines) == 0 {
return ""
}
return strings.Join(lines, "\n") + "\n"
}
func (m *MangoWCProvider) shouldUseStockScaffold(content string) bool {
if strings.TrimSpace(content) == "" {
return true
}
if strings.Contains(content, "gesturebind=") && strings.Contains(content, "# ===") {
return false
}
return !strings.Contains(content, "gesturebind=") && (strings.Count(content, "\nbind=")+strings.Count(content, "\nbindl=")+strings.Count(content, "\nbinds=")+strings.Count(content, "\nbindr=")+strings.Count(content, "\nbindp=") >= 10 || strings.Contains(content, "dms ipc call"))
}
func (m *MangoWCProvider) stockBindsScaffold(binds map[string]*mangowcOverrideBind) string {
terminalCommand := "ghostty"
for _, key := range []string{"super+t", "super+return"} {
if bind, ok := binds[key]; ok {
command, params := m.parseAction(bind.Action)
if command == "spawn" && strings.TrimSpace(params) != "" && !strings.Contains(params, "dms ") {
terminalCommand = params
break
}
}
}
return strings.ReplaceAll(config.MangoBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
}
func (m *MangoWCProvider) dropReplacedStockBinds(binds map[string]*mangowcOverrideBind) {
if bind, ok := binds["super+j"]; ok && bind.Action == "switch_layout" {
delete(binds, "super+j")
}
}
func (m *MangoWCProvider) sortedBinds(binds map[string]*mangowcOverrideBind) []*mangowcOverrideBind {
bindList := make([]*mangowcOverrideBind, 0, len(binds))
for _, bind := range binds {
bindList = append(bindList, bind)
}
sort.Slice(bindList, func(i, j int) bool {
pi, pj := m.getBindSortPriority(bindList[i].Action), m.getBindSortPriority(bindList[j].Action)
if pi != pj {
@@ -384,13 +524,55 @@ func (m *MangoWCProvider) generateBindsContent(binds map[string]*mangowcOverride
}
return bindList[i].Key < bindList[j].Key
})
return bindList
}
func (m *MangoWCProvider) writeBindLineToLines(lines *[]string, bind *mangowcOverrideBind) {
var sb strings.Builder
for _, bind := range bindList {
m.writeBindLine(&sb, bind)
m.writeBindLine(&sb, bind)
text := strings.TrimSuffix(sb.String(), "\n")
if text == "" {
return
}
*lines = append(*lines, strings.Split(text, "\n")...)
}
return sb.String()
func (m *MangoWCProvider) previousComment(lines []string) string {
if len(lines) == 0 {
return ""
}
trimmed := strings.TrimSpace(lines[len(lines)-1])
if !strings.HasPrefix(trimmed, "#") {
return ""
}
comment := strings.TrimSpace(strings.TrimPrefix(trimmed, "#"))
if isMangoWCSectionComment(comment) {
return ""
}
return comment
}
func (m *MangoWCProvider) dropPreviousDescriptionComment(lines *[]string) {
if len(*lines) == 0 {
return
}
trimmed := strings.TrimSpace((*lines)[len(*lines)-1])
if !strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, "# ===") {
return
}
*lines = (*lines)[:len(*lines)-1]
}
func (m *MangoWCProvider) trimTrailingEmptyLines(lines *[]string) {
for len(*lines) > 0 && strings.TrimSpace((*lines)[len(*lines)-1]) == "" {
*lines = (*lines)[:len(*lines)-1]
}
}
func (m *MangoWCProvider) copyBindWithDescription(bind *mangowcOverrideBind, description string) *mangowcOverrideBind {
copy := *bind
copy.Description = description
return &copy
}
func (m *MangoWCProvider) writeBindLine(sb *strings.Builder, bind *mangowcOverrideBind) {
@@ -405,7 +587,12 @@ func (m *MangoWCProvider) writeBindLine(sb *strings.Builder, bind *mangowcOverri
sb.WriteString("\n")
}
sb.WriteString("bind=")
prefix := bind.Prefix
if prefix == "" {
prefix = "bind"
}
sb.WriteString(prefix)
sb.WriteString("=")
if mods == "" {
sb.WriteString("none")
} else {
@@ -424,6 +611,36 @@ func (m *MangoWCProvider) writeBindLine(sb *strings.Builder, bind *mangowcOverri
sb.WriteString("\n")
}
func (m *MangoWCProvider) bindPrefixFromOptions(options map[string]any) string {
if options == nil {
return ""
}
value, ok := options["flags"]
if !ok {
return ""
}
flags := ""
switch v := value.(type) {
case string:
flags = v
case fmt.Stringer:
flags = v.String()
default:
return ""
}
flags = strings.TrimSpace(flags)
if flags == "" {
return "bind"
}
var clean strings.Builder
for _, ch := range flags {
if strings.ContainsRune("lsrp", ch) && !strings.ContainsRune(clean.String(), ch) {
clean.WriteRune(ch)
}
}
return "bind" + clean.String()
}
func (m *MangoWCProvider) parseKeyString(keyStr string) (mods, key string) {
parts := strings.Split(keyStr, "+")
switch len(parts) {
@@ -15,6 +15,10 @@ const (
var MangoWCModSeparators = []rune{'+', ' '}
func isMangoWCSectionComment(comment string) bool {
return strings.HasPrefix(strings.TrimSpace(comment), "===")
}
type MangoWCKeyBinding struct {
Mods []string `json:"mods"`
Key string `json:"key"`
@@ -235,6 +239,9 @@ func (p *MangoWCParser) ParseKeys() []MangoWCKeyBinding {
}
if strings.HasPrefix(trimmed, "#") {
pendingComment = strings.TrimSpace(strings.TrimPrefix(trimmed, "#"))
if isMangoWCSectionComment(pendingComment) {
pendingComment = ""
}
continue
}
if !strings.HasPrefix(trimmed, "bind") {
@@ -414,6 +421,9 @@ func (p *MangoWCParser) parseFileWithSource(filePath string) ([]MangoWCKeyBindin
if strings.HasPrefix(trimmed, "#") {
pendingComment = strings.TrimSpace(strings.TrimPrefix(trimmed, "#"))
if isMangoWCSectionComment(pendingComment) {
pendingComment = ""
}
continue
}
@@ -483,7 +493,7 @@ func (p *MangoWCParser) parseDMSBindsDirectly(dmsBindsPath string) []MangoWCKeyB
// line directly above) is the description: mango feeds inline comments to spawn
// as argv, so DMS keeps descriptions on the line above; inline `#` is a fallback.
func (p *MangoWCParser) getKeybindAtLineContent(line string, precedingComment string) *MangoWCKeyBinding {
bindMatch := regexp.MustCompile(`^(bind[lsr]*)\s*=\s*(.+)$`)
bindMatch := regexp.MustCompile(`^(bind[lsrp]*)\s*=\s*(.+)$`)
matches := bindMatch.FindStringSubmatch(line)
if len(matches) < 3 {
return nil
@@ -499,6 +509,9 @@ func (p *MangoWCParser) getKeybindAtLineContent(line string, precedingComment st
}
if comment == "" {
comment = strings.TrimSpace(precedingComment)
if isMangoWCSectionComment(comment) {
comment = ""
}
}
if strings.HasPrefix(comment, MangoWCHideComment) {
@@ -71,9 +71,10 @@ func TestMangoWCAutogenerateComment(t *testing.T) {
func TestMangoWCGetKeybindAtLine(t *testing.T) {
tests := []struct {
name string
line string
expected *MangoWCKeyBinding
name string
line string
precedingComment string
expected *MangoWCKeyBinding
}{
{
name: "basic_keybind",
@@ -157,6 +158,41 @@ func TestMangoWCGetKeybindAtLine(t *testing.T) {
Comment: "dms ipc call lock lock",
},
},
{
name: "bindp_flag",
line: "bindp=SUPER,p,spawn,pass-through",
expected: &MangoWCKeyBinding{
Mods: []string{"SUPER"},
Key: "p",
Command: "spawn",
Params: "pass-through",
Comment: "pass-through",
},
},
{
name: "preceding_comment",
line: "bind=SUPER+SHIFT,S,spawn,dms screenshot",
precedingComment: "Screenshot: Interactive",
expected: &MangoWCKeyBinding{
Mods: []string{"SUPER", "SHIFT"},
Key: "S",
Command: "spawn",
Params: "dms screenshot",
Comment: "Screenshot: Interactive",
},
},
{
name: "section_header_not_description",
line: "bind=none,XF86AudioRaiseVolume,spawn,dms ipc call audio increment 3",
precedingComment: "=== Audio Controls ===",
expected: &MangoWCKeyBinding{
Mods: []string{},
Key: "XF86AudioRaiseVolume",
Command: "spawn",
Params: "dms ipc call audio increment 3",
Comment: "dms ipc call audio increment 3",
},
},
{
name: "keybind_with_spaces",
line: "bind = SUPER, r, reload_config",
@@ -174,7 +210,7 @@ func TestMangoWCGetKeybindAtLine(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
parser := NewMangoWCParser("")
parser.contentLines = []string{tt.line}
result := parser.getKeybindAtLine(0, "")
result := parser.getKeybindAtLine(0, tt.precedingComment)
if tt.expected == nil {
if result != nil {
@@ -3,7 +3,10 @@ package providers
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
)
func TestMangoWCProviderName(t *testing.T) {
@@ -318,3 +321,138 @@ bind=Ctrl,1,view,1,0
t.Error("Did not find terminal keybind with correct key and description")
}
}
func TestMangoWCSetBindPreservesStockCommentsAndGestures(t *testing.T) {
tmpDir := t.TempDir()
dmsDir := filepath.Join(tmpDir, "dms")
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
t.Fatalf("failed to create dms dir: %v", err)
}
bindsPath := filepath.Join(dmsDir, "binds.conf")
stock := strings.ReplaceAll(config.MangoBindsConfig, "{{TERMINAL_COMMAND}}", "ghostty")
if err := os.WriteFile(bindsPath, []byte(stock), 0o644); err != nil {
t.Fatalf("failed to write stock binds: %v", err)
}
provider := NewMangoWCProvider(tmpDir)
if err := provider.SetBind("SUPER+SHIFT+S", "spawn dms screenshot", "Screenshot: Interactive", nil); err != nil {
t.Fatalf("SetBind failed: %v", err)
}
contentBytes, err := os.ReadFile(bindsPath)
if err != nil {
t.Fatalf("failed to read binds: %v", err)
}
content := string(contentBytes)
for _, want := range []string{
"# === Application Launchers ===",
"# === Touchpad Gestures ===",
"gesturebind=none,right,3,viewtoleft_have_client",
"gesturebind=none,left,3,viewtoright_have_client",
"# Screenshot: Interactive\nbind=SUPER+SHIFT,S,spawn,dms screenshot",
} {
if !strings.Contains(content, want) {
t.Fatalf("expected saved binds to contain %q\ncontent:\n%s", want, content)
}
}
if strings.Contains(content, "# === Audio Controls ===\n# === Audio Controls ===") {
t.Fatalf("section header should not be duplicated as a bind description\ncontent:\n%s", content)
}
}
func TestMangoWCSetBindRestoresScaffoldForStrippedFile(t *testing.T) {
tmpDir := t.TempDir()
dmsDir := filepath.Join(tmpDir, "dms")
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
t.Fatalf("failed to create dms dir: %v", err)
}
bindsPath := filepath.Join(dmsDir, "binds.conf")
stripped := `bind=SUPER,t,spawn,ghostty
bind=SUPER,Return,spawn,ghostty
bind=SUPER,space,spawn,dms ipc call spotlight toggle
bind=SUPER,v,spawn,dms ipc call clipboard toggle
bind=SUPER,q,killclient
bind=SUPER,Left,focusdir,left
bind=SUPER,Right,focusdir,right
bind=SUPER,Up,focusdir,up
bind=SUPER,Down,focusdir,down
bind=SUPER,1,view,1
bind=SUPER,2,view,2
bind=SUPER,3,view,3
`
if err := os.WriteFile(bindsPath, []byte(stripped), 0o644); err != nil {
t.Fatalf("failed to write stripped binds: %v", err)
}
provider := NewMangoWCProvider(tmpDir)
if err := provider.SetBind("SUPER+SHIFT+S", "spawn dms screenshot", "Screenshot: Interactive", nil); err != nil {
t.Fatalf("SetBind failed: %v", err)
}
contentBytes, err := os.ReadFile(bindsPath)
if err != nil {
t.Fatalf("failed to read binds: %v", err)
}
content := string(contentBytes)
for _, want := range []string{
"# DMS default keybinds (MangoWM)",
"# === Touchpad Gestures ===",
"gesturebind=none,right,3,viewtoleft_have_client",
"bind=SUPER,H,focusdir,left",
"bind=SUPER,J,focusdir,down",
"bind=SUPER,K,focusdir,up",
"bind=SUPER,L,focusdir,right",
"# === Custom Keybinds ===",
"# Screenshot: Interactive\nbind=SUPER+SHIFT,S,spawn,dms screenshot",
"bind=SUPER,t,spawn,ghostty",
} {
if !strings.Contains(content, want) {
t.Fatalf("expected restored binds to contain %q\ncontent:\n%s", want, content)
}
}
if strings.Contains(content, "{{TERMINAL_COMMAND}}") {
t.Fatalf("terminal placeholder should have been resolved\ncontent:\n%s", content)
}
}
func TestMangoWCRemoveBindPreservesNonBindLines(t *testing.T) {
tmpDir := t.TempDir()
dmsDir := filepath.Join(tmpDir, "dms")
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
t.Fatalf("failed to create dms dir: %v", err)
}
bindsPath := filepath.Join(dmsDir, "binds.conf")
stock := strings.ReplaceAll(config.MangoBindsConfig, "{{TERMINAL_COMMAND}}", "ghostty")
if err := os.WriteFile(bindsPath, []byte(stock), 0o644); err != nil {
t.Fatalf("failed to write stock binds: %v", err)
}
provider := NewMangoWCProvider(tmpDir)
if err := provider.RemoveBind("SUPER+Tab"); err != nil {
t.Fatalf("RemoveBind failed: %v", err)
}
contentBytes, err := os.ReadFile(bindsPath)
if err != nil {
t.Fatalf("failed to read binds: %v", err)
}
content := string(contentBytes)
if strings.Contains(content, "bind=SUPER,Tab,focusstack,next") {
t.Fatalf("removed bind should be absent\ncontent:\n%s", content)
}
if strings.Contains(content, "# Focus Next Window") {
t.Fatalf("removed bind description should be absent\ncontent:\n%s", content)
}
for _, want := range []string{
"# === Focus Navigation ===",
"# === Touchpad Gestures ===",
"gesturebind=none,down,4,toggleoverview",
} {
if !strings.Contains(content, want) {
t.Fatalf("expected non-bind line %q to be preserved\ncontent:\n%s", want, content)
}
}
}
-791
View File
@@ -1,791 +0,0 @@
// Generated by go-wayland-scanner
// https://github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/cmd/go-wayland-scanner
// XML file : internal/proto/xml/dwl-ipc-unstable-v2.xml
//
// dwl_ipc_unstable_v2 Protocol Copyright:
package dwl_ipc
import "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
// ZdwlIpcManagerV2InterfaceName is the name of the interface as it appears in the [client.Registry].
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
const ZdwlIpcManagerV2InterfaceName = "zdwl_ipc_manager_v2"
// ZdwlIpcManagerV2 : manage dwl state
//
// This interface is exposed as a global in wl_registry.
//
// Clients can use this interface to get a dwl_ipc_output.
// After binding the client will recieve the dwl_ipc_manager.tags and dwl_ipc_manager.layout events.
// The dwl_ipc_manager.tags and dwl_ipc_manager.layout events expose tags and layouts to the client.
type ZdwlIpcManagerV2 struct {
client.BaseProxy
tagsHandler ZdwlIpcManagerV2TagsHandlerFunc
layoutHandler ZdwlIpcManagerV2LayoutHandlerFunc
}
// NewZdwlIpcManagerV2 : manage dwl state
//
// This interface is exposed as a global in wl_registry.
//
// Clients can use this interface to get a dwl_ipc_output.
// After binding the client will recieve the dwl_ipc_manager.tags and dwl_ipc_manager.layout events.
// The dwl_ipc_manager.tags and dwl_ipc_manager.layout events expose tags and layouts to the client.
func NewZdwlIpcManagerV2(ctx *client.Context) *ZdwlIpcManagerV2 {
zdwlIpcManagerV2 := &ZdwlIpcManagerV2{}
ctx.Register(zdwlIpcManagerV2)
return zdwlIpcManagerV2
}
// Release : release dwl_ipc_manager
//
// Indicates that the client will not the dwl_ipc_manager object anymore.
// Objects created through this instance are not affected.
func (i *ZdwlIpcManagerV2) Release() error {
defer i.MarkZombie()
const opcode = 0
const _reqBufLen = 8
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// GetOutput : get a dwl_ipc_outout for a wl_output
//
// Get a dwl_ipc_outout for the specified wl_output.
func (i *ZdwlIpcManagerV2) GetOutput(output *client.Output) (*ZdwlIpcOutputV2, error) {
id := NewZdwlIpcOutputV2(i.Context())
const opcode = 1
const _reqBufLen = 8 + 4 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], id.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], output.ID())
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return id, err
}
// ZdwlIpcManagerV2TagsEvent : Announces tag amount
//
// This event is sent after binding.
// A roundtrip after binding guarantees the client recieved all tags.
type ZdwlIpcManagerV2TagsEvent struct {
Amount uint32
}
type ZdwlIpcManagerV2TagsHandlerFunc func(ZdwlIpcManagerV2TagsEvent)
// SetTagsHandler : sets handler for ZdwlIpcManagerV2TagsEvent
func (i *ZdwlIpcManagerV2) SetTagsHandler(f ZdwlIpcManagerV2TagsHandlerFunc) {
i.tagsHandler = f
}
// ZdwlIpcManagerV2LayoutEvent : Announces a layout
//
// This event is sent after binding.
// A roundtrip after binding guarantees the client recieved all layouts.
type ZdwlIpcManagerV2LayoutEvent struct {
Name string
}
type ZdwlIpcManagerV2LayoutHandlerFunc func(ZdwlIpcManagerV2LayoutEvent)
// SetLayoutHandler : sets handler for ZdwlIpcManagerV2LayoutEvent
func (i *ZdwlIpcManagerV2) SetLayoutHandler(f ZdwlIpcManagerV2LayoutHandlerFunc) {
i.layoutHandler = f
}
func (i *ZdwlIpcManagerV2) Dispatch(opcode uint32, fd int, data []byte) {
switch opcode {
case 0:
if i.tagsHandler == nil {
return
}
var e ZdwlIpcManagerV2TagsEvent
l := 0
e.Amount = client.Uint32(data[l : l+4])
l += 4
i.tagsHandler(e)
case 1:
if i.layoutHandler == nil {
return
}
var e ZdwlIpcManagerV2LayoutEvent
l := 0
nameLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
l += 4
e.Name = client.String(data[l : l+nameLen])
l += nameLen
i.layoutHandler(e)
}
}
// ZdwlIpcOutputV2InterfaceName is the name of the interface as it appears in the [client.Registry].
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
const ZdwlIpcOutputV2InterfaceName = "zdwl_ipc_output_v2"
// ZdwlIpcOutputV2 : control dwl output
//
// Observe and control a dwl output.
//
// Events are double-buffered:
// Clients should cache events and redraw when a dwl_ipc_output.frame event is sent.
//
// Request are not double-buffered:
// The compositor will update immediately upon request.
type ZdwlIpcOutputV2 struct {
client.BaseProxy
toggleVisibilityHandler ZdwlIpcOutputV2ToggleVisibilityHandlerFunc
activeHandler ZdwlIpcOutputV2ActiveHandlerFunc
tagHandler ZdwlIpcOutputV2TagHandlerFunc
layoutHandler ZdwlIpcOutputV2LayoutHandlerFunc
titleHandler ZdwlIpcOutputV2TitleHandlerFunc
appidHandler ZdwlIpcOutputV2AppidHandlerFunc
layoutSymbolHandler ZdwlIpcOutputV2LayoutSymbolHandlerFunc
frameHandler ZdwlIpcOutputV2FrameHandlerFunc
fullscreenHandler ZdwlIpcOutputV2FullscreenHandlerFunc
floatingHandler ZdwlIpcOutputV2FloatingHandlerFunc
xHandler ZdwlIpcOutputV2XHandlerFunc
yHandler ZdwlIpcOutputV2YHandlerFunc
widthHandler ZdwlIpcOutputV2WidthHandlerFunc
heightHandler ZdwlIpcOutputV2HeightHandlerFunc
lastLayerHandler ZdwlIpcOutputV2LastLayerHandlerFunc
kbLayoutHandler ZdwlIpcOutputV2KbLayoutHandlerFunc
keymodeHandler ZdwlIpcOutputV2KeymodeHandlerFunc
scalefactorHandler ZdwlIpcOutputV2ScalefactorHandlerFunc
}
// NewZdwlIpcOutputV2 : control dwl output
//
// Observe and control a dwl output.
//
// Events are double-buffered:
// Clients should cache events and redraw when a dwl_ipc_output.frame event is sent.
//
// Request are not double-buffered:
// The compositor will update immediately upon request.
func NewZdwlIpcOutputV2(ctx *client.Context) *ZdwlIpcOutputV2 {
zdwlIpcOutputV2 := &ZdwlIpcOutputV2{}
ctx.Register(zdwlIpcOutputV2)
return zdwlIpcOutputV2
}
// Release : release dwl_ipc_outout
//
// Indicates to that the client no longer needs this dwl_ipc_output.
func (i *ZdwlIpcOutputV2) Release() error {
defer i.MarkZombie()
const opcode = 0
const _reqBufLen = 8
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// SetTags : Set the active tags of this output
//
// tagmask: bitmask of the tags that should be set.
// toggleTagset: toggle the selected tagset, zero for invalid, nonzero for valid.
func (i *ZdwlIpcOutputV2) SetTags(tagmask, toggleTagset uint32) error {
const opcode = 1
const _reqBufLen = 8 + 4 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(tagmask))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(toggleTagset))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// SetClientTags : Set the tags of the focused client.
//
// The tags are updated as follows:
// new_tags = (current_tags AND and_tags) XOR xor_tags
func (i *ZdwlIpcOutputV2) SetClientTags(andTags, xorTags uint32) error {
const opcode = 2
const _reqBufLen = 8 + 4 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(andTags))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(xorTags))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// SetLayout : Set the layout of this output
//
// index: index of a layout recieved by dwl_ipc_manager.layout
func (i *ZdwlIpcOutputV2) SetLayout(index uint32) error {
const opcode = 3
const _reqBufLen = 8 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(index))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// Quit : Quit mango
// This request allows clients to instruct the compositor to quit mango.
func (i *ZdwlIpcOutputV2) Quit() error {
const opcode = 4
const _reqBufLen = 8
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// SendDispatch : Set the active tags of this output
//
// dispatch: dispatch name.
// arg1: arg1.
// arg2: arg2.
// arg3: arg3.
// arg4: arg4.
// arg5: arg5.
func (i *ZdwlIpcOutputV2) SendDispatch(dispatch, arg1, arg2, arg3, arg4, arg5 string) error {
const opcode = 5
dispatchLen := client.PaddedLen(len(dispatch) + 1)
arg1Len := client.PaddedLen(len(arg1) + 1)
arg2Len := client.PaddedLen(len(arg2) + 1)
arg3Len := client.PaddedLen(len(arg3) + 1)
arg4Len := client.PaddedLen(len(arg4) + 1)
arg5Len := client.PaddedLen(len(arg5) + 1)
_reqBufLen := 8 + (4 + dispatchLen) + (4 + arg1Len) + (4 + arg2Len) + (4 + arg3Len) + (4 + arg4Len) + (4 + arg5Len)
_reqBuf := make([]byte, _reqBufLen)
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutString(_reqBuf[l:l+(4+dispatchLen)], dispatch)
l += (4 + dispatchLen)
client.PutString(_reqBuf[l:l+(4+arg1Len)], arg1)
l += (4 + arg1Len)
client.PutString(_reqBuf[l:l+(4+arg2Len)], arg2)
l += (4 + arg2Len)
client.PutString(_reqBuf[l:l+(4+arg3Len)], arg3)
l += (4 + arg3Len)
client.PutString(_reqBuf[l:l+(4+arg4Len)], arg4)
l += (4 + arg4Len)
client.PutString(_reqBuf[l:l+(4+arg5Len)], arg5)
l += (4 + arg5Len)
err := i.Context().WriteMsg(_reqBuf, nil)
return err
}
type ZdwlIpcOutputV2TagState uint32
// ZdwlIpcOutputV2TagState :
const (
// ZdwlIpcOutputV2TagStateNone : no state
ZdwlIpcOutputV2TagStateNone ZdwlIpcOutputV2TagState = 0
// ZdwlIpcOutputV2TagStateActive : tag is active
ZdwlIpcOutputV2TagStateActive ZdwlIpcOutputV2TagState = 1
// ZdwlIpcOutputV2TagStateUrgent : tag has at least one urgent client
ZdwlIpcOutputV2TagStateUrgent ZdwlIpcOutputV2TagState = 2
)
func (e ZdwlIpcOutputV2TagState) Name() string {
switch e {
case ZdwlIpcOutputV2TagStateNone:
return "none"
case ZdwlIpcOutputV2TagStateActive:
return "active"
case ZdwlIpcOutputV2TagStateUrgent:
return "urgent"
default:
return ""
}
}
func (e ZdwlIpcOutputV2TagState) Value() string {
switch e {
case ZdwlIpcOutputV2TagStateNone:
return "0"
case ZdwlIpcOutputV2TagStateActive:
return "1"
case ZdwlIpcOutputV2TagStateUrgent:
return "2"
default:
return ""
}
}
func (e ZdwlIpcOutputV2TagState) String() string {
return e.Name() + "=" + e.Value()
}
// ZdwlIpcOutputV2ToggleVisibilityEvent : Toggle client visibilty
//
// Indicates the client should hide or show themselves.
// If the client is visible then hide, if hidden then show.
type ZdwlIpcOutputV2ToggleVisibilityEvent struct{}
type ZdwlIpcOutputV2ToggleVisibilityHandlerFunc func(ZdwlIpcOutputV2ToggleVisibilityEvent)
// SetToggleVisibilityHandler : sets handler for ZdwlIpcOutputV2ToggleVisibilityEvent
func (i *ZdwlIpcOutputV2) SetToggleVisibilityHandler(f ZdwlIpcOutputV2ToggleVisibilityHandlerFunc) {
i.toggleVisibilityHandler = f
}
// ZdwlIpcOutputV2ActiveEvent : Update the selected output.
//
// Indicates if the output is active. Zero is invalid, nonzero is valid.
type ZdwlIpcOutputV2ActiveEvent struct {
Active uint32
}
type ZdwlIpcOutputV2ActiveHandlerFunc func(ZdwlIpcOutputV2ActiveEvent)
// SetActiveHandler : sets handler for ZdwlIpcOutputV2ActiveEvent
func (i *ZdwlIpcOutputV2) SetActiveHandler(f ZdwlIpcOutputV2ActiveHandlerFunc) {
i.activeHandler = f
}
// ZdwlIpcOutputV2TagEvent : Update the state of a tag.
//
// Indicates that a tag has been updated.
type ZdwlIpcOutputV2TagEvent struct {
Tag uint32
State uint32
Clients uint32
Focused uint32
}
type ZdwlIpcOutputV2TagHandlerFunc func(ZdwlIpcOutputV2TagEvent)
// SetTagHandler : sets handler for ZdwlIpcOutputV2TagEvent
func (i *ZdwlIpcOutputV2) SetTagHandler(f ZdwlIpcOutputV2TagHandlerFunc) {
i.tagHandler = f
}
// ZdwlIpcOutputV2LayoutEvent : Update the layout.
//
// Indicates a new layout is selected.
type ZdwlIpcOutputV2LayoutEvent struct {
Layout uint32
}
type ZdwlIpcOutputV2LayoutHandlerFunc func(ZdwlIpcOutputV2LayoutEvent)
// SetLayoutHandler : sets handler for ZdwlIpcOutputV2LayoutEvent
func (i *ZdwlIpcOutputV2) SetLayoutHandler(f ZdwlIpcOutputV2LayoutHandlerFunc) {
i.layoutHandler = f
}
// ZdwlIpcOutputV2TitleEvent : Update the title.
//
// Indicates the title has changed.
type ZdwlIpcOutputV2TitleEvent struct {
Title string
}
type ZdwlIpcOutputV2TitleHandlerFunc func(ZdwlIpcOutputV2TitleEvent)
// SetTitleHandler : sets handler for ZdwlIpcOutputV2TitleEvent
func (i *ZdwlIpcOutputV2) SetTitleHandler(f ZdwlIpcOutputV2TitleHandlerFunc) {
i.titleHandler = f
}
// ZdwlIpcOutputV2AppidEvent : Update the appid.
//
// Indicates the appid has changed.
type ZdwlIpcOutputV2AppidEvent struct {
Appid string
}
type ZdwlIpcOutputV2AppidHandlerFunc func(ZdwlIpcOutputV2AppidEvent)
// SetAppidHandler : sets handler for ZdwlIpcOutputV2AppidEvent
func (i *ZdwlIpcOutputV2) SetAppidHandler(f ZdwlIpcOutputV2AppidHandlerFunc) {
i.appidHandler = f
}
// ZdwlIpcOutputV2LayoutSymbolEvent : Update the current layout symbol
//
// Indicates the layout has changed. Since layout symbols are dynamic.
// As opposed to the zdwl_ipc_manager.layout event, this should take precendence when displaying.
// You can ignore the zdwl_ipc_output.layout event.
type ZdwlIpcOutputV2LayoutSymbolEvent struct {
Layout string
}
type ZdwlIpcOutputV2LayoutSymbolHandlerFunc func(ZdwlIpcOutputV2LayoutSymbolEvent)
// SetLayoutSymbolHandler : sets handler for ZdwlIpcOutputV2LayoutSymbolEvent
func (i *ZdwlIpcOutputV2) SetLayoutSymbolHandler(f ZdwlIpcOutputV2LayoutSymbolHandlerFunc) {
i.layoutSymbolHandler = f
}
// ZdwlIpcOutputV2FrameEvent : The update sequence is done.
//
// Indicates that a sequence of status updates have finished and the client should redraw.
type ZdwlIpcOutputV2FrameEvent struct{}
type ZdwlIpcOutputV2FrameHandlerFunc func(ZdwlIpcOutputV2FrameEvent)
// SetFrameHandler : sets handler for ZdwlIpcOutputV2FrameEvent
func (i *ZdwlIpcOutputV2) SetFrameHandler(f ZdwlIpcOutputV2FrameHandlerFunc) {
i.frameHandler = f
}
// ZdwlIpcOutputV2FullscreenEvent : Update fullscreen status
//
// Indicates if the selected client on this output is fullscreen.
type ZdwlIpcOutputV2FullscreenEvent struct {
IsFullscreen uint32
}
type ZdwlIpcOutputV2FullscreenHandlerFunc func(ZdwlIpcOutputV2FullscreenEvent)
// SetFullscreenHandler : sets handler for ZdwlIpcOutputV2FullscreenEvent
func (i *ZdwlIpcOutputV2) SetFullscreenHandler(f ZdwlIpcOutputV2FullscreenHandlerFunc) {
i.fullscreenHandler = f
}
// ZdwlIpcOutputV2FloatingEvent : Update the floating status
//
// Indicates if the selected client on this output is floating.
type ZdwlIpcOutputV2FloatingEvent struct {
IsFloating uint32
}
type ZdwlIpcOutputV2FloatingHandlerFunc func(ZdwlIpcOutputV2FloatingEvent)
// SetFloatingHandler : sets handler for ZdwlIpcOutputV2FloatingEvent
func (i *ZdwlIpcOutputV2) SetFloatingHandler(f ZdwlIpcOutputV2FloatingHandlerFunc) {
i.floatingHandler = f
}
// ZdwlIpcOutputV2XEvent : Update the x coordinates
//
// Indicates if x coordinates of the selected client.
type ZdwlIpcOutputV2XEvent struct {
X int32
}
type ZdwlIpcOutputV2XHandlerFunc func(ZdwlIpcOutputV2XEvent)
// SetXHandler : sets handler for ZdwlIpcOutputV2XEvent
func (i *ZdwlIpcOutputV2) SetXHandler(f ZdwlIpcOutputV2XHandlerFunc) {
i.xHandler = f
}
// ZdwlIpcOutputV2YEvent : Update the y coordinates
//
// Indicates if y coordinates of the selected client.
type ZdwlIpcOutputV2YEvent struct {
Y int32
}
type ZdwlIpcOutputV2YHandlerFunc func(ZdwlIpcOutputV2YEvent)
// SetYHandler : sets handler for ZdwlIpcOutputV2YEvent
func (i *ZdwlIpcOutputV2) SetYHandler(f ZdwlIpcOutputV2YHandlerFunc) {
i.yHandler = f
}
// ZdwlIpcOutputV2WidthEvent : Update the width
//
// Indicates if width of the selected client.
type ZdwlIpcOutputV2WidthEvent struct {
Width int32
}
type ZdwlIpcOutputV2WidthHandlerFunc func(ZdwlIpcOutputV2WidthEvent)
// SetWidthHandler : sets handler for ZdwlIpcOutputV2WidthEvent
func (i *ZdwlIpcOutputV2) SetWidthHandler(f ZdwlIpcOutputV2WidthHandlerFunc) {
i.widthHandler = f
}
// ZdwlIpcOutputV2HeightEvent : Update the height
//
// Indicates if height of the selected client.
type ZdwlIpcOutputV2HeightEvent struct {
Height int32
}
type ZdwlIpcOutputV2HeightHandlerFunc func(ZdwlIpcOutputV2HeightEvent)
// SetHeightHandler : sets handler for ZdwlIpcOutputV2HeightEvent
func (i *ZdwlIpcOutputV2) SetHeightHandler(f ZdwlIpcOutputV2HeightHandlerFunc) {
i.heightHandler = f
}
// ZdwlIpcOutputV2LastLayerEvent : last map layer.
//
// last map layer.
type ZdwlIpcOutputV2LastLayerEvent struct {
LastLayer string
}
type ZdwlIpcOutputV2LastLayerHandlerFunc func(ZdwlIpcOutputV2LastLayerEvent)
// SetLastLayerHandler : sets handler for ZdwlIpcOutputV2LastLayerEvent
func (i *ZdwlIpcOutputV2) SetLastLayerHandler(f ZdwlIpcOutputV2LastLayerHandlerFunc) {
i.lastLayerHandler = f
}
// ZdwlIpcOutputV2KbLayoutEvent : current keyboard layout.
//
// current keyboard layout.
type ZdwlIpcOutputV2KbLayoutEvent struct {
KbLayout string
}
type ZdwlIpcOutputV2KbLayoutHandlerFunc func(ZdwlIpcOutputV2KbLayoutEvent)
// SetKbLayoutHandler : sets handler for ZdwlIpcOutputV2KbLayoutEvent
func (i *ZdwlIpcOutputV2) SetKbLayoutHandler(f ZdwlIpcOutputV2KbLayoutHandlerFunc) {
i.kbLayoutHandler = f
}
// ZdwlIpcOutputV2KeymodeEvent : current keybind mode.
//
// current keybind mode.
type ZdwlIpcOutputV2KeymodeEvent struct {
Keymode string
}
type ZdwlIpcOutputV2KeymodeHandlerFunc func(ZdwlIpcOutputV2KeymodeEvent)
// SetKeymodeHandler : sets handler for ZdwlIpcOutputV2KeymodeEvent
func (i *ZdwlIpcOutputV2) SetKeymodeHandler(f ZdwlIpcOutputV2KeymodeHandlerFunc) {
i.keymodeHandler = f
}
// ZdwlIpcOutputV2ScalefactorEvent : scale factor of monitor.
//
// scale factor of monitor.
type ZdwlIpcOutputV2ScalefactorEvent struct {
Scalefactor uint32
}
type ZdwlIpcOutputV2ScalefactorHandlerFunc func(ZdwlIpcOutputV2ScalefactorEvent)
// SetScalefactorHandler : sets handler for ZdwlIpcOutputV2ScalefactorEvent
func (i *ZdwlIpcOutputV2) SetScalefactorHandler(f ZdwlIpcOutputV2ScalefactorHandlerFunc) {
i.scalefactorHandler = f
}
func (i *ZdwlIpcOutputV2) Dispatch(opcode uint32, fd int, data []byte) {
switch opcode {
case 0:
if i.toggleVisibilityHandler == nil {
return
}
var e ZdwlIpcOutputV2ToggleVisibilityEvent
i.toggleVisibilityHandler(e)
case 1:
if i.activeHandler == nil {
return
}
var e ZdwlIpcOutputV2ActiveEvent
l := 0
e.Active = client.Uint32(data[l : l+4])
l += 4
i.activeHandler(e)
case 2:
if i.tagHandler == nil {
return
}
var e ZdwlIpcOutputV2TagEvent
l := 0
e.Tag = client.Uint32(data[l : l+4])
l += 4
e.State = client.Uint32(data[l : l+4])
l += 4
e.Clients = client.Uint32(data[l : l+4])
l += 4
e.Focused = client.Uint32(data[l : l+4])
l += 4
i.tagHandler(e)
case 3:
if i.layoutHandler == nil {
return
}
var e ZdwlIpcOutputV2LayoutEvent
l := 0
e.Layout = client.Uint32(data[l : l+4])
l += 4
i.layoutHandler(e)
case 4:
if i.titleHandler == nil {
return
}
var e ZdwlIpcOutputV2TitleEvent
l := 0
titleLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
l += 4
e.Title = client.String(data[l : l+titleLen])
l += titleLen
i.titleHandler(e)
case 5:
if i.appidHandler == nil {
return
}
var e ZdwlIpcOutputV2AppidEvent
l := 0
appidLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
l += 4
e.Appid = client.String(data[l : l+appidLen])
l += appidLen
i.appidHandler(e)
case 6:
if i.layoutSymbolHandler == nil {
return
}
var e ZdwlIpcOutputV2LayoutSymbolEvent
l := 0
layoutLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
l += 4
e.Layout = client.String(data[l : l+layoutLen])
l += layoutLen
i.layoutSymbolHandler(e)
case 7:
if i.frameHandler == nil {
return
}
var e ZdwlIpcOutputV2FrameEvent
i.frameHandler(e)
case 8:
if i.fullscreenHandler == nil {
return
}
var e ZdwlIpcOutputV2FullscreenEvent
l := 0
e.IsFullscreen = client.Uint32(data[l : l+4])
l += 4
i.fullscreenHandler(e)
case 9:
if i.floatingHandler == nil {
return
}
var e ZdwlIpcOutputV2FloatingEvent
l := 0
e.IsFloating = client.Uint32(data[l : l+4])
l += 4
i.floatingHandler(e)
case 10:
if i.xHandler == nil {
return
}
var e ZdwlIpcOutputV2XEvent
l := 0
e.X = int32(client.Uint32(data[l : l+4]))
l += 4
i.xHandler(e)
case 11:
if i.yHandler == nil {
return
}
var e ZdwlIpcOutputV2YEvent
l := 0
e.Y = int32(client.Uint32(data[l : l+4]))
l += 4
i.yHandler(e)
case 12:
if i.widthHandler == nil {
return
}
var e ZdwlIpcOutputV2WidthEvent
l := 0
e.Width = int32(client.Uint32(data[l : l+4]))
l += 4
i.widthHandler(e)
case 13:
if i.heightHandler == nil {
return
}
var e ZdwlIpcOutputV2HeightEvent
l := 0
e.Height = int32(client.Uint32(data[l : l+4]))
l += 4
i.heightHandler(e)
case 14:
if i.lastLayerHandler == nil {
return
}
var e ZdwlIpcOutputV2LastLayerEvent
l := 0
lastLayerLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
l += 4
e.LastLayer = client.String(data[l : l+lastLayerLen])
l += lastLayerLen
i.lastLayerHandler(e)
case 15:
if i.kbLayoutHandler == nil {
return
}
var e ZdwlIpcOutputV2KbLayoutEvent
l := 0
kbLayoutLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
l += 4
e.KbLayout = client.String(data[l : l+kbLayoutLen])
l += kbLayoutLen
i.kbLayoutHandler(e)
case 16:
if i.keymodeHandler == nil {
return
}
var e ZdwlIpcOutputV2KeymodeEvent
l := 0
keymodeLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
l += 4
e.Keymode = client.String(data[l : l+keymodeLen])
l += keymodeLen
i.keymodeHandler(e)
case 17:
if i.scalefactorHandler == nil {
return
}
var e ZdwlIpcOutputV2ScalefactorEvent
l := 0
e.Scalefactor = client.Uint32(data[l : l+4])
l += 4
i.scalefactorHandler(e)
}
}
@@ -0,0 +1,25 @@
package qmlchecks
import (
"os"
"regexp"
"strings"
"testing"
)
func TestLockScreenPasswordFieldBypassesTextInputIME(t *testing.T) {
data, err := os.ReadFile("../../../quickshell/Modules/Lock/LockScreenContent.qml")
if err != nil {
t.Fatalf("read lock screen QML: %v", err)
}
content := string(data)
textInputPasswordField := regexp.MustCompile(`(?s)TextInput\s*\{[^{}]*id:\s*passwordField`)
if textInputPasswordField.MatchString(content) {
t.Fatalf("passwordField must not be a TextInput because TextInput can route physical keyboard input through IME")
}
if !strings.Contains(content, "Keys.onPressed") || !strings.Contains(content, "event.text") {
t.Fatalf("passwordField should handle physical key text manually instead of relying on a text input control")
}
}
+107 -325
View File
@@ -6,7 +6,6 @@ import (
"os"
"os/exec"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
@@ -19,9 +18,9 @@ const (
CompositorHyprland
CompositorSway
CompositorNiri
CompositorDWL
CompositorScroll
CompositorMiracle
CompositorMango
)
var detectedCompositor Compositor = -1
@@ -36,8 +35,14 @@ func DetectCompositor() Compositor {
swaySocket := os.Getenv("SWAYSOCK")
scrollSocket := os.Getenv("SCROLLSOCK")
miracleSocket := os.Getenv("MIRACLESOCK")
mangoSocket := os.Getenv("MANGO_INSTANCE_SIGNATURE")
switch {
case mangoSocket != "":
if _, err := os.Stat(mangoSocket); err == nil {
detectedCompositor = CompositorMango
return detectedCompositor
}
case niriSocket != "":
if _, err := os.Stat(niriSocket); err == nil {
detectedCompositor = CompositorNiri
@@ -63,66 +68,29 @@ func DetectCompositor() Compositor {
return detectedCompositor
}
if detectDWLProtocol() {
detectedCompositor = CompositorDWL
return detectedCompositor
}
detectedCompositor = CompositorUnknown
return detectedCompositor
}
func detectDWLProtocol() bool {
display, err := client.Connect("")
if err != nil {
return false
}
ctx := display.Context()
defer ctx.Close()
registry, err := display.GetRegistry()
if err != nil {
return false
}
found := false
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
if e.Interface == dwl_ipc.ZdwlIpcManagerV2InterfaceName {
found = true
}
})
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
return false
}
return found
}
func SetCompositorDWL() {
detectedCompositor = CompositorDWL
}
type WindowGeometry struct {
X int32
Y int32
Width int32
Height int32
Output string
Scale float64
OutputX int32
OutputY int32
OutputTransform int32
X int32
Y int32
Width int32
Height int32
Output string
Scale float64
OutputX int32
OutputY int32
}
func GetActiveWindow() (*WindowGeometry, error) {
switch DetectCompositor() {
case CompositorHyprland:
return getHyprlandActiveWindow()
case CompositorDWL:
return getDWLActiveWindow()
case CompositorMango:
return getMangoActiveWindow()
default:
return nil, fmt.Errorf("window capture requires Hyprland or DWL")
return nil, fmt.Errorf("window capture requires Hyprland or Mango")
}
}
@@ -285,6 +253,93 @@ func getMiracleFocusedMonitor() string {
return ""
}
type mangoMonitor struct {
Name string `json:"name"`
Active bool `json:"active"`
X int32 `json:"x"`
Y int32 `json:"y"`
Scale float64 `json:"scale"`
}
func getMangoMonitors() []mangoMonitor {
output, err := exec.Command("mmsg", "get", "all-monitors").Output()
if err != nil {
return nil
}
var data struct {
Monitors []mangoMonitor `json:"monitors"`
}
if err := json.Unmarshal(output, &data); err != nil {
return nil
}
return data.Monitors
}
func getMangoFocusedMonitor() string {
for _, m := range getMangoMonitors() {
if m.Active {
return m.Name
}
}
return ""
}
type mangoClient struct {
Monitor string `json:"monitor"`
IsFocused bool `json:"is_focused"`
X int32 `json:"x"`
Y int32 `json:"y"`
Width int32 `json:"width"`
Height int32 `json:"height"`
}
func getMangoActiveWindow() (*WindowGeometry, error) {
output, err := exec.Command("mmsg", "get", "all-clients").Output()
if err != nil {
return nil, fmt.Errorf("mmsg get all-clients: %w", err)
}
var data struct {
Clients []mangoClient `json:"clients"`
}
if err := json.Unmarshal(output, &data); err != nil {
return nil, fmt.Errorf("parse all-clients: %w", err)
}
for _, c := range data.Clients {
if !c.IsFocused {
continue
}
if c.Width <= 0 || c.Height <= 0 {
return nil, fmt.Errorf("no active window")
}
geom := &WindowGeometry{
X: c.X,
Y: c.Y,
Width: c.Width,
Height: c.Height,
Output: c.Monitor,
Scale: 1.0,
}
for _, m := range getMangoMonitors() {
if m.Name != c.Monitor {
continue
}
geom.OutputX = m.X
geom.OutputY = m.Y
if m.Scale > 0 {
geom.Scale = m.Scale
}
break
}
return geom, nil
}
return nil, fmt.Errorf("no focused window")
}
type niriWorkspace struct {
Output string `json:"output"`
IsFocused bool `json:"is_focused"`
@@ -309,121 +364,6 @@ func getNiriFocusedMonitor() string {
return ""
}
var dwlActiveOutput string
func SetDWLActiveOutput(name string) {
dwlActiveOutput = name
}
func getDWLFocusedMonitor() string {
if dwlActiveOutput != "" {
return dwlActiveOutput
}
return queryDWLActiveOutput()
}
func queryDWLActiveOutput() string {
display, err := client.Connect("")
if err != nil {
return ""
}
ctx := display.Context()
defer ctx.Close()
registry, err := display.GetRegistry()
if err != nil {
return ""
}
var dwlManager *dwl_ipc.ZdwlIpcManagerV2
outputs := make(map[uint32]*client.Output)
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
switch e.Interface {
case dwl_ipc.ZdwlIpcManagerV2InterfaceName:
mgr := dwl_ipc.NewZdwlIpcManagerV2(ctx)
if err := registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil {
dwlManager = mgr
}
case client.OutputInterfaceName:
out := client.NewOutput(ctx)
version := e.Version
if version > 4 {
version = 4
}
if err := registry.Bind(e.Name, e.Interface, version, out); err == nil {
outputs[e.Name] = out
}
}
})
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
return ""
}
if dwlManager == nil || len(outputs) == 0 {
return ""
}
outputNames := make(map[uint32]string)
for name, out := range outputs {
n := name
out.SetNameHandler(func(e client.OutputNameEvent) {
outputNames[n] = e.Name
})
}
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
return ""
}
type outputState struct {
name string
active bool
gotFrame bool
}
states := make(map[uint32]*outputState)
for name, out := range outputs {
dwlOut, err := dwlManager.GetOutput(out)
if err != nil {
continue
}
state := &outputState{name: outputNames[name]}
states[name] = state
dwlOut.SetActiveHandler(func(e dwl_ipc.ZdwlIpcOutputV2ActiveEvent) {
state.active = e.Active != 0
})
dwlOut.SetFrameHandler(func(e dwl_ipc.ZdwlIpcOutputV2FrameEvent) {
state.gotFrame = true
})
}
allFramesReceived := func() bool {
for _, s := range states {
if !s.gotFrame {
return false
}
}
return true
}
for !allFramesReceived() {
if err := ctx.Dispatch(); err != nil {
return ""
}
}
for _, state := range states {
if state.active {
return state.name
}
}
return ""
}
func GetFocusedMonitor() string {
switch DetectCompositor() {
case CompositorHyprland:
@@ -436,8 +376,8 @@ func GetFocusedMonitor() string {
return getMiracleFocusedMonitor()
case CompositorNiri:
return getNiriFocusedMonitor()
case CompositorDWL:
return getDWLFocusedMonitor()
case CompositorMango:
return getMangoFocusedMonitor()
}
return ""
}
@@ -534,161 +474,3 @@ func getAllOutputInfos() map[string]*outputInfo {
}
return result
}
func getOutputInfo(outputName string) (*outputInfo, bool) {
infos := getAllOutputInfos()
if infos == nil {
return nil, false
}
info, ok := infos[outputName]
return info, ok
}
func getDWLActiveWindow() (*WindowGeometry, error) {
display, err := client.Connect("")
if err != nil {
return nil, fmt.Errorf("connect: %w", err)
}
ctx := display.Context()
defer ctx.Close()
registry, err := display.GetRegistry()
if err != nil {
return nil, fmt.Errorf("get registry: %w", err)
}
var dwlManager *dwl_ipc.ZdwlIpcManagerV2
outputs := make(map[uint32]*client.Output)
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
switch e.Interface {
case dwl_ipc.ZdwlIpcManagerV2InterfaceName:
mgr := dwl_ipc.NewZdwlIpcManagerV2(ctx)
if err := registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil {
dwlManager = mgr
}
case client.OutputInterfaceName:
out := client.NewOutput(ctx)
version := e.Version
if version > 4 {
version = 4
}
if err := registry.Bind(e.Name, e.Interface, version, out); err == nil {
outputs[e.Name] = out
}
}
})
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
return nil, fmt.Errorf("roundtrip: %w", err)
}
if dwlManager == nil {
return nil, fmt.Errorf("dwl_ipc_manager not available")
}
if len(outputs) == 0 {
return nil, fmt.Errorf("no outputs found")
}
outputNames := make(map[uint32]string)
for name, out := range outputs {
n := name
out.SetNameHandler(func(e client.OutputNameEvent) {
outputNames[n] = e.Name
})
}
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
return nil, fmt.Errorf("roundtrip: %w", err)
}
type dwlOutputState struct {
output *dwl_ipc.ZdwlIpcOutputV2
name string
active bool
x, y int32
w, h int32
scalefactor uint32
gotFrame bool
}
dwlOutputs := make(map[uint32]*dwlOutputState)
for name, out := range outputs {
dwlOut, err := dwlManager.GetOutput(out)
if err != nil {
continue
}
state := &dwlOutputState{output: dwlOut, name: outputNames[name]}
dwlOutputs[name] = state
dwlOut.SetActiveHandler(func(e dwl_ipc.ZdwlIpcOutputV2ActiveEvent) {
state.active = e.Active != 0
})
dwlOut.SetXHandler(func(e dwl_ipc.ZdwlIpcOutputV2XEvent) {
state.x = e.X
})
dwlOut.SetYHandler(func(e dwl_ipc.ZdwlIpcOutputV2YEvent) {
state.y = e.Y
})
dwlOut.SetWidthHandler(func(e dwl_ipc.ZdwlIpcOutputV2WidthEvent) {
state.w = e.Width
})
dwlOut.SetHeightHandler(func(e dwl_ipc.ZdwlIpcOutputV2HeightEvent) {
state.h = e.Height
})
dwlOut.SetScalefactorHandler(func(e dwl_ipc.ZdwlIpcOutputV2ScalefactorEvent) {
state.scalefactor = e.Scalefactor
})
dwlOut.SetFrameHandler(func(e dwl_ipc.ZdwlIpcOutputV2FrameEvent) {
state.gotFrame = true
})
}
allFramesReceived := func() bool {
for _, s := range dwlOutputs {
if !s.gotFrame {
return false
}
}
return true
}
for !allFramesReceived() {
if err := ctx.Dispatch(); err != nil {
return nil, fmt.Errorf("dispatch: %w", err)
}
}
for _, state := range dwlOutputs {
if !state.active {
continue
}
if state.w <= 0 || state.h <= 0 {
return nil, fmt.Errorf("no active window")
}
scale := float64(state.scalefactor) / 100.0
if scale <= 0 {
scale = 1.0
}
geom := &WindowGeometry{
X: state.x,
Y: state.y,
Width: state.w,
Height: state.h,
Output: state.name,
Scale: scale,
}
if info, ok := getOutputInfo(state.name); ok {
geom.OutputX = info.x
geom.OutputY = info.y
geom.OutputTransform = info.transform
}
return geom, nil
}
return nil, fmt.Errorf("no active output found")
}
+4 -4
View File
@@ -156,14 +156,14 @@ func (s *Screenshoter) captureWindow() (*CaptureResult, error) {
switch DetectCompositor() {
case CompositorHyprland:
return s.captureAndCrop(output, region)
case CompositorDWL:
return s.captureDWLWindow(output, region, geom)
case CompositorMango:
return s.captureMangoWindow(output, region, geom)
default:
return s.captureRegionOnOutput(output, region)
}
}
func (s *Screenshoter) captureDWLWindow(output *WaylandOutput, region Region, geom *WindowGeometry) (*CaptureResult, error) {
func (s *Screenshoter) captureMangoWindow(output *WaylandOutput, region Region, geom *WindowGeometry) (*CaptureResult, error) {
result, err := s.captureWholeOutput(output)
if err != nil {
return nil, err
@@ -628,7 +628,7 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio
w := int32(float64(region.Width) * scale)
h := int32(float64(region.Height) * scale)
if DetectCompositor() == CompositorDWL {
if DetectCompositor() == CompositorMango {
scaledOutW := int32(float64(output.width) * scale)
scaledOutH := int32(float64(output.height) * scale)
if localX >= scaledOutW {
+32 -31
View File
@@ -935,7 +935,7 @@ func (m *Manager) CreateHistoryEntryFromPinned(pinnedEntry *Entry) error {
Pinned: false,
}
if err := m.storeEntryWithoutDedup(newEntry); err != nil {
if err := m.storeEntry(newEntry); err != nil {
return err
}
@@ -945,36 +945,6 @@ func (m *Manager) CreateHistoryEntryFromPinned(pinnedEntry *Entry) error {
return nil
}
func (m *Manager) storeEntryWithoutDedup(entry Entry) error {
if m.db == nil {
return fmt.Errorf("database not available")
}
entry.Hash = computeHash(entry.Data)
return m.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("clipboard"))
id, err := b.NextSequence()
if err != nil {
return err
}
entry.ID = id
encoded, err := encodeEntry(entry)
if err != nil {
return err
}
if err := b.Put(itob(id), encoded); err != nil {
return err
}
return m.trimLengthInTx(b)
})
}
func (m *Manager) ClearHistory() {
if m.db == nil {
return
@@ -1653,6 +1623,37 @@ func (m *Manager) UnpinEntry(id uint64) error {
return err
}
if entry.Pinned {
currentKey := itob(id)
var keepKey []byte
var deleteKeys [][]byte
c := b.Cursor()
for k, v := c.Last(); k != nil; k, v = c.Prev() {
if bytes.Equal(k, currentKey) || extractHash(v) != entry.Hash {
continue
}
duplicate, err := decodeEntryMeta(v)
if err == nil && !duplicate.Pinned {
key := append([]byte(nil), k...)
if keepKey == nil {
keepKey = key
} else {
deleteKeys = append(deleteKeys, key)
}
}
}
if keepKey != nil {
for _, key := range deleteKeys {
if err := b.Delete(key); err != nil {
return err
}
}
return b.Delete(currentKey)
}
}
entry.Pinned = false
encoded, err := encodeEntry(entry)
if err != nil {
@@ -14,6 +14,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
bolt "go.etcd.io/bbolt"
mocks_wlcontext "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlcontext"
)
@@ -273,6 +274,110 @@ func TestHandleGetEntry_MissingIDReturnsNullResult(t *testing.T) {
assert.Nil(t, resp.Result)
}
func TestUnpinEntry_KeepsTopUnpinnedDuplicate(t *testing.T) {
m := newTestManagerWithDB(t)
require.NoError(t, m.storeEntry(Entry{
Data: []byte("saved content"),
MimeType: "text/plain;charset=utf-8",
Preview: "saved content",
Size: len("saved content"),
Timestamp: time.Now().Add(-time.Minute).Truncate(time.Second),
IsImage: false,
}))
history := m.GetHistory()
require.Len(t, history, 1)
pinnedID := history[0].ID
require.NoError(t, m.PinEntry(pinnedID))
pinnedEntry, err := m.GetEntry(pinnedID)
require.NoError(t, err)
require.True(t, pinnedEntry.Pinned)
// Bypass storeEntry to simulate legacy duplicate ordinary history entries.
insertLegacyUnpinnedDuplicate := func(timestamp time.Time) Entry {
duplicate := Entry{
Data: pinnedEntry.Data,
MimeType: pinnedEntry.MimeType,
Preview: pinnedEntry.Preview,
Size: pinnedEntry.Size,
Timestamp: timestamp,
IsImage: pinnedEntry.IsImage,
Pinned: false,
}
duplicate.Hash = computeHash(duplicate.Data)
require.NoError(t, m.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("clipboard"))
id, err := b.NextSequence()
if err != nil {
return err
}
duplicate.ID = id
encoded, err := encodeEntry(duplicate)
if err != nil {
return err
}
return b.Put(itob(id), encoded)
}))
return duplicate
}
olderHistoryDuplicate := insertLegacyUnpinnedDuplicate(time.Now().Add(time.Hour))
topHistoryDuplicate := insertLegacyUnpinnedDuplicate(time.Now().Add(-time.Hour))
require.Greater(t, topHistoryDuplicate.ID, olderHistoryDuplicate.ID)
require.True(t, olderHistoryDuplicate.Timestamp.After(topHistoryDuplicate.Timestamp))
history = m.GetHistory()
require.Len(t, history, 3)
require.Equal(t, topHistoryDuplicate.ID, history[0].ID)
require.NoError(t, m.UnpinEntry(pinnedID))
history = m.GetHistory()
require.Len(t, history, 1)
assert.False(t, history[0].Pinned)
assert.Equal(t, pinnedEntry.Hash, history[0].Hash)
assert.Equal(t, topHistoryDuplicate.ID, history[0].ID)
}
func TestCreateHistoryEntryFromPinned_KeepsLatestUnpinnedDuplicate(t *testing.T) {
m := newTestManagerWithDB(t)
require.NoError(t, m.storeEntry(Entry{
Data: []byte("saved content"),
MimeType: "text/plain;charset=utf-8",
Preview: "saved content",
Size: len("saved content"),
Timestamp: time.Now().Add(-time.Minute).Truncate(time.Second),
IsImage: false,
}))
history := m.GetHistory()
require.Len(t, history, 1)
pinnedID := history[0].ID
require.NoError(t, m.PinEntry(pinnedID))
pinnedEntry, err := m.GetEntry(pinnedID)
require.NoError(t, err)
require.True(t, pinnedEntry.Pinned)
require.NoError(t, m.CreateHistoryEntryFromPinned(pinnedEntry))
firstDuplicate := m.GetHistory()[0]
require.NotEqual(t, pinnedID, firstDuplicate.ID)
require.NoError(t, m.CreateHistoryEntryFromPinned(pinnedEntry))
latestDuplicate := m.GetHistory()[0]
history = m.GetHistory()
require.Len(t, history, 2)
assert.Equal(t, latestDuplicate.ID, history[0].ID)
assert.False(t, history[0].Pinned)
assert.Equal(t, pinnedID, history[1].ID)
assert.True(t, history[1].Pinned)
assert.NotEqual(t, firstDuplicate.ID, latestDuplicate.ID)
}
func TestManager_ConcurrentSubscriberAccess(t *testing.T) {
m := &Manager{
subscribers: make(map[string]chan State),
-138
View File
@@ -1,138 +0,0 @@
package dwl
import (
"encoding/json"
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
type SuccessResult struct {
Success bool `json:"success"`
Message string `json:"message"`
}
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
if manager == nil {
models.RespondError(conn, req.ID, "dwl manager not initialized")
return
}
switch req.Method {
case "dwl.getState":
handleGetState(conn, req, manager)
case "dwl.setTags":
handleSetTags(conn, req, manager)
case "dwl.setClientTags":
handleSetClientTags(conn, req, manager)
case "dwl.setLayout":
handleSetLayout(conn, req, manager)
case "dwl.subscribe":
handleSubscribe(conn, req, manager)
default:
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
}
}
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
state := manager.GetState()
models.Respond(conn, req.ID, state)
}
func handleSetTags(conn net.Conn, req models.Request, manager *Manager) {
output, ok := models.Get[string](req, "output")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'output' parameter")
return
}
tagmask, ok := models.Get[float64](req, "tagmask")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'tagmask' parameter")
return
}
toggleTagset, ok := models.Get[float64](req, "toggleTagset")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'toggleTagset' parameter")
return
}
if err := manager.SetTags(output, uint32(tagmask), uint32(toggleTagset)); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "tags set"})
}
func handleSetClientTags(conn net.Conn, req models.Request, manager *Manager) {
output, ok := models.Get[string](req, "output")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'output' parameter")
return
}
andTags, ok := models.Get[float64](req, "andTags")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'andTags' parameter")
return
}
xorTags, ok := models.Get[float64](req, "xorTags")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'xorTags' parameter")
return
}
if err := manager.SetClientTags(output, uint32(andTags), uint32(xorTags)); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "client tags set"})
}
func handleSetLayout(conn net.Conn, req models.Request, manager *Manager) {
output, ok := models.Get[string](req, "output")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'output' parameter")
return
}
index, ok := models.Get[float64](req, "index")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'index' parameter")
return
}
if err := manager.SetLayout(output, uint32(index)); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "layout set"})
}
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID)
initialState := manager.GetState()
if err := json.NewEncoder(conn).Encode(models.Response[State]{
ID: req.ID,
Result: &initialState,
}); err != nil {
return
}
for state := range stateChan {
if err := json.NewEncoder(conn).Encode(models.Response[State]{
Result: &state,
}); err != nil {
return
}
}
}
-522
View File
@@ -1,522 +0,0 @@
package dwl
import (
"fmt"
"time"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc"
)
func NewManager(display wlclient.WaylandDisplay) (*Manager, error) {
m := &Manager{
display: display,
ctx: display.Context(),
cmdq: make(chan cmd, 128),
outputSetupReq: make(chan uint32, 16),
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
layouts: make([]string, 0),
}
if err := m.setupRegistry(); err != nil {
return nil, err
}
m.updateState()
m.notifierWg.Add(1)
go m.notifier()
m.wg.Add(1)
go m.waylandActor()
return m, nil
}
func (m *Manager) post(fn func()) {
select {
case m.cmdq <- cmd{fn: fn}:
default:
log.Warn("DWL actor command queue full, dropping command")
}
}
func (m *Manager) waylandActor() {
defer m.wg.Done()
for {
select {
case <-m.stopChan:
return
case c := <-m.cmdq:
c.fn()
case outputID := <-m.outputSetupReq:
out, exists := m.outputs.Load(outputID)
if !exists {
log.Warnf("DWL: Output %d no longer exists, skipping setup", outputID)
continue
}
if out.ipcOutput != nil {
continue
}
mgr, ok := m.manager.(*dwl_ipc.ZdwlIpcManagerV2)
if !ok || mgr == nil {
log.Errorf("DWL: Manager not available for output %d setup", outputID)
continue
}
log.Infof("DWL: Setting up ipcOutput for dynamically added output %d", outputID)
if err := m.setupOutput(mgr, out.output); err != nil {
log.Errorf("DWL: Failed to setup output %d: %v", outputID, err)
} else {
m.updateState()
}
}
}
}
func (m *Manager) setupRegistry() error {
log.Info("DWL: starting registry setup")
registry, err := m.display.GetRegistry()
if err != nil {
return fmt.Errorf("failed to get registry: %w", err)
}
m.registry = registry
outputs := make([]*wlclient.Output, 0)
outputRegNames := make(map[uint32]uint32)
var dwlMgr *dwl_ipc.ZdwlIpcManagerV2
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
switch e.Interface {
case dwl_ipc.ZdwlIpcManagerV2InterfaceName:
log.Infof("DWL: found %s", dwl_ipc.ZdwlIpcManagerV2InterfaceName)
manager := dwl_ipc.NewZdwlIpcManagerV2(m.ctx)
version := e.Version
if version > 2 {
version = 2
}
if err := registry.Bind(e.Name, e.Interface, version, manager); err == nil {
dwlMgr = manager
log.Info("DWL: manager bound successfully")
// Set handlers immediately after binding, before roundtrips
manager.SetTagsHandler(func(e dwl_ipc.ZdwlIpcManagerV2TagsEvent) {
log.Infof("DWL: Tags count: %d", e.Amount)
m.tagCount = e.Amount
m.updateState()
})
manager.SetLayoutHandler(func(e dwl_ipc.ZdwlIpcManagerV2LayoutEvent) {
log.Infof("DWL: Layout: %s", e.Name)
m.layouts = append(m.layouts, e.Name)
m.updateState()
})
} else {
log.Errorf("DWL: failed to bind manager: %v", err)
}
case "wl_output":
log.Debugf("DWL: found wl_output (name=%d)", e.Name)
output := wlclient.NewOutput(m.ctx)
outState := &outputState{
registryName: e.Name,
output: output,
tags: make([]TagState, 0),
}
output.SetNameHandler(func(ev wlclient.OutputNameEvent) {
log.Debugf("DWL: Output name: %s (registry=%d)", ev.Name, e.Name)
outState.name = ev.Name
})
output.SetDescriptionHandler(func(ev wlclient.OutputDescriptionEvent) {
log.Debugf("DWL: Output description: %s", ev.Description)
})
version := e.Version
if version > 4 {
version = 4
}
if err := registry.Bind(e.Name, e.Interface, version, output); err == nil {
outputID := output.ID()
outState.id = outputID
log.Infof("DWL: Bound wl_output id=%d registry_name=%d", outputID, e.Name)
outputs = append(outputs, output)
outputRegNames[outputID] = e.Name
m.outputs.Store(outputID, outState)
if m.manager != nil {
select {
case m.outputSetupReq <- outputID:
log.Debugf("DWL: Queued setup for output %d", outputID)
default:
log.Warnf("DWL: Setup queue full, output %d will not be initialized", outputID)
}
}
} else {
log.Errorf("DWL: Failed to bind wl_output: %v", err)
}
}
})
registry.SetGlobalRemoveHandler(func(e wlclient.RegistryGlobalRemoveEvent) {
m.post(func() {
var outToRelease *outputState
m.outputs.Range(func(id uint32, out *outputState) bool {
if out.registryName == e.Name {
log.Infof("DWL: Output %d removed", id)
outToRelease = out
m.outputs.Delete(id)
return false
}
return true
})
if outToRelease != nil {
if ipcOut, ok := outToRelease.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2); ok && ipcOut != nil {
m.wlMutex.Lock()
ipcOut.Release()
m.wlMutex.Unlock()
log.Debugf("DWL: Released ipcOutput for removed output %d", outToRelease.id)
}
m.updateState()
}
})
})
if err := m.display.Roundtrip(); err != nil {
return fmt.Errorf("first roundtrip failed: %w", err)
}
if err := m.display.Roundtrip(); err != nil {
return fmt.Errorf("second roundtrip failed: %w", err)
}
if dwlMgr == nil {
log.Info("DWL: manager not found in registry")
return fmt.Errorf("dwl_ipc_manager_v2 not available")
}
m.manager = dwlMgr
for _, output := range outputs {
if err := m.setupOutput(dwlMgr, output); err != nil {
log.Warnf("DWL: Failed to setup output %d: %v", output.ID(), err)
}
}
if err := m.display.Roundtrip(); err != nil {
return fmt.Errorf("final roundtrip failed: %w", err)
}
log.Info("DWL: registry setup complete")
return nil
}
func (m *Manager) setupOutput(manager *dwl_ipc.ZdwlIpcManagerV2, output *wlclient.Output) error {
m.wlMutex.Lock()
ipcOutput, err := manager.GetOutput(output)
m.wlMutex.Unlock()
if err != nil {
return fmt.Errorf("failed to get dwl output: %w", err)
}
outState, exists := m.outputs.Load(output.ID())
if !exists {
return fmt.Errorf("output state not found for id %d", output.ID())
}
outState.ipcOutput = ipcOutput
ipcOutput.SetActiveHandler(func(e dwl_ipc.ZdwlIpcOutputV2ActiveEvent) {
outState.active = e.Active
})
ipcOutput.SetTagHandler(func(e dwl_ipc.ZdwlIpcOutputV2TagEvent) {
updated := false
for i, tag := range outState.tags {
if tag.Tag == e.Tag {
outState.tags[i] = TagState{
Tag: e.Tag,
State: e.State,
Clients: e.Clients,
Focused: e.Focused,
}
updated = true
break
}
}
if !updated {
outState.tags = append(outState.tags, TagState{
Tag: e.Tag,
State: e.State,
Clients: e.Clients,
Focused: e.Focused,
})
}
m.updateState()
})
ipcOutput.SetLayoutHandler(func(e dwl_ipc.ZdwlIpcOutputV2LayoutEvent) {
outState.layout = e.Layout
})
ipcOutput.SetTitleHandler(func(e dwl_ipc.ZdwlIpcOutputV2TitleEvent) {
outState.title = e.Title
})
ipcOutput.SetAppidHandler(func(e dwl_ipc.ZdwlIpcOutputV2AppidEvent) {
outState.appID = e.Appid
})
ipcOutput.SetLayoutSymbolHandler(func(e dwl_ipc.ZdwlIpcOutputV2LayoutSymbolEvent) {
outState.layoutSymbol = e.Layout
})
ipcOutput.SetKbLayoutHandler(func(e dwl_ipc.ZdwlIpcOutputV2KbLayoutEvent) {
outState.kbLayout = e.KbLayout
})
ipcOutput.SetKeymodeHandler(func(e dwl_ipc.ZdwlIpcOutputV2KeymodeEvent) {
outState.keymode = e.Keymode
})
ipcOutput.SetFrameHandler(func(e dwl_ipc.ZdwlIpcOutputV2FrameEvent) {
m.updateState()
})
return nil
}
func (m *Manager) updateState() {
outputs := make(map[string]*OutputState)
activeOutput := ""
m.outputs.Range(func(key uint32, out *outputState) bool {
name := out.name
if name == "" {
name = fmt.Sprintf("output-%d", out.id)
}
tagsCopy := make([]TagState, len(out.tags))
copy(tagsCopy, out.tags)
outputs[name] = &OutputState{
Name: name,
Active: out.active,
Tags: tagsCopy,
Layout: out.layout,
LayoutSymbol: out.layoutSymbol,
Title: out.title,
AppID: out.appID,
KbLayout: out.kbLayout,
Keymode: out.keymode,
}
if out.active != 0 {
activeOutput = name
}
return true
})
newState := State{
Outputs: outputs,
TagCount: m.tagCount,
Layouts: m.layouts,
ActiveOutput: activeOutput,
}
m.stateMutex.Lock()
m.state = &newState
m.stateMutex.Unlock()
m.notifySubscribers()
}
func (m *Manager) notifier() {
defer m.notifierWg.Done()
const minGap = 100 * time.Millisecond
timer := time.NewTimer(minGap)
timer.Stop()
var pending bool
for {
select {
case <-m.stopChan:
timer.Stop()
return
case <-m.dirty:
if pending {
continue
}
pending = true
timer.Reset(minGap)
case <-timer.C:
if !pending {
continue
}
currentState := m.GetState()
if m.lastNotified != nil && !stateChanged(m.lastNotified, &currentState) {
pending = false
continue
}
m.subscribers.Range(func(key string, ch chan State) bool {
select {
case ch <- currentState:
default:
log.Warn("DWL: subscriber channel full, dropping update")
}
return true
})
stateCopy := currentState
m.lastNotified = &stateCopy
pending = false
}
}
}
func (m *Manager) ensureOutputSetup(out *outputState) error {
if out.ipcOutput != nil {
return nil
}
return fmt.Errorf("output not yet initialized - setup in progress, retry in a moment")
}
func (m *Manager) SetTags(outputName string, tagmask uint32, toggleTagset uint32) error {
availableOutputs := make([]string, 0)
var targetOut *outputState
m.outputs.Range(func(key uint32, out *outputState) bool {
name := out.name
if name == "" {
name = fmt.Sprintf("output-%d", out.id)
}
availableOutputs = append(availableOutputs, name)
if name == outputName {
targetOut = out
return false
}
return true
})
if targetOut == nil {
return fmt.Errorf("output not found: %s (available: %v)", outputName, availableOutputs)
}
if err := m.ensureOutputSetup(targetOut); err != nil {
return fmt.Errorf("failed to setup output %s: %w", outputName, err)
}
ipcOut, ok := targetOut.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2)
if !ok {
return fmt.Errorf("output %s has invalid ipcOutput type", outputName)
}
m.wlMutex.Lock()
err := ipcOut.SetTags(tagmask, toggleTagset)
m.wlMutex.Unlock()
return err
}
func (m *Manager) SetClientTags(outputName string, andTags uint32, xorTags uint32) error {
var targetOut *outputState
m.outputs.Range(func(key uint32, out *outputState) bool {
name := out.name
if name == "" {
name = fmt.Sprintf("output-%d", out.id)
}
if name == outputName {
targetOut = out
return false
}
return true
})
if targetOut == nil {
return fmt.Errorf("output not found: %s", outputName)
}
if err := m.ensureOutputSetup(targetOut); err != nil {
return fmt.Errorf("failed to setup output %s: %w", outputName, err)
}
ipcOut, ok := targetOut.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2)
if !ok {
return fmt.Errorf("output %s has invalid ipcOutput type", outputName)
}
m.wlMutex.Lock()
err := ipcOut.SetClientTags(andTags, xorTags)
m.wlMutex.Unlock()
return err
}
func (m *Manager) SetLayout(outputName string, index uint32) error {
var targetOut *outputState
m.outputs.Range(func(key uint32, out *outputState) bool {
name := out.name
if name == "" {
name = fmt.Sprintf("output-%d", out.id)
}
if name == outputName {
targetOut = out
return false
}
return true
})
if targetOut == nil {
return fmt.Errorf("output not found: %s", outputName)
}
if err := m.ensureOutputSetup(targetOut); err != nil {
return fmt.Errorf("failed to setup output %s: %w", outputName, err)
}
ipcOut, ok := targetOut.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2)
if !ok {
return fmt.Errorf("output %s has invalid ipcOutput type", outputName)
}
m.wlMutex.Lock()
err := ipcOut.SetLayout(index)
m.wlMutex.Unlock()
return err
}
func (m *Manager) Close() {
close(m.stopChan)
m.wg.Wait()
m.notifierWg.Wait()
m.subscribers.Range(func(key string, ch chan State) bool {
close(ch)
m.subscribers.Delete(key)
return true
})
m.outputs.Range(func(key uint32, out *outputState) bool {
if ipcOut, ok := out.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2); ok {
ipcOut.Release()
}
m.outputs.Delete(key)
return true
})
if mgr, ok := m.manager.(*dwl_ipc.ZdwlIpcManagerV2); ok {
mgr.Release()
}
}
-366
View File
@@ -1,366 +0,0 @@
package dwl
import (
"errors"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient"
)
func TestStateChanged_BothNil(t *testing.T) {
assert.True(t, stateChanged(nil, nil))
}
func TestStateChanged_OneNil(t *testing.T) {
s := &State{TagCount: 9}
assert.True(t, stateChanged(s, nil))
assert.True(t, stateChanged(nil, s))
}
func TestStateChanged_TagCountDiffers(t *testing.T) {
a := &State{TagCount: 9, Outputs: make(map[string]*OutputState), Layouts: []string{}}
b := &State{TagCount: 10, Outputs: make(map[string]*OutputState), Layouts: []string{}}
assert.True(t, stateChanged(a, b))
}
func TestStateChanged_LayoutLengthDiffers(t *testing.T) {
a := &State{TagCount: 9, Layouts: []string{"tile"}, Outputs: make(map[string]*OutputState)}
b := &State{TagCount: 9, Layouts: []string{"tile", "monocle"}, Outputs: make(map[string]*OutputState)}
assert.True(t, stateChanged(a, b))
}
func TestStateChanged_ActiveOutputDiffers(t *testing.T) {
a := &State{TagCount: 9, ActiveOutput: "eDP-1", Outputs: make(map[string]*OutputState), Layouts: []string{}}
b := &State{TagCount: 9, ActiveOutput: "HDMI-A-1", Outputs: make(map[string]*OutputState), Layouts: []string{}}
assert.True(t, stateChanged(a, b))
}
func TestStateChanged_OutputCountDiffers(t *testing.T) {
a := &State{
TagCount: 9,
Outputs: map[string]*OutputState{"eDP-1": {}},
Layouts: []string{},
}
b := &State{
TagCount: 9,
Outputs: map[string]*OutputState{},
Layouts: []string{},
}
assert.True(t, stateChanged(a, b))
}
func TestStateChanged_OutputFieldsDiffer(t *testing.T) {
a := &State{
TagCount: 9,
Layouts: []string{},
Outputs: map[string]*OutputState{
"eDP-1": {Active: 1, Layout: 0, Title: "Firefox"},
},
}
b := &State{
TagCount: 9,
Layouts: []string{},
Outputs: map[string]*OutputState{
"eDP-1": {Active: 0, Layout: 0, Title: "Firefox"},
},
}
assert.True(t, stateChanged(a, b))
b.Outputs["eDP-1"].Active = 1
b.Outputs["eDP-1"].Layout = 1
assert.True(t, stateChanged(a, b))
b.Outputs["eDP-1"].Layout = 0
b.Outputs["eDP-1"].Title = "Code"
assert.True(t, stateChanged(a, b))
}
func TestStateChanged_TagsDiffer(t *testing.T) {
a := &State{
TagCount: 9,
Layouts: []string{},
Outputs: map[string]*OutputState{
"eDP-1": {Tags: []TagState{{Tag: 1, State: 1, Clients: 2, Focused: 1}}},
},
}
b := &State{
TagCount: 9,
Layouts: []string{},
Outputs: map[string]*OutputState{
"eDP-1": {Tags: []TagState{{Tag: 1, State: 2, Clients: 2, Focused: 1}}},
},
}
assert.True(t, stateChanged(a, b))
b.Outputs["eDP-1"].Tags[0].State = 1
b.Outputs["eDP-1"].Tags[0].Clients = 3
assert.True(t, stateChanged(a, b))
}
func TestStateChanged_Equal(t *testing.T) {
a := &State{
TagCount: 9,
ActiveOutput: "eDP-1",
Layouts: []string{"tile", "monocle"},
Outputs: map[string]*OutputState{
"eDP-1": {
Name: "eDP-1",
Active: 1,
Layout: 0,
LayoutSymbol: "[]=",
Title: "Firefox",
AppID: "firefox",
KbLayout: "us",
Keymode: "",
Tags: []TagState{{Tag: 1, State: 1, Clients: 2, Focused: 1}},
},
},
}
b := &State{
TagCount: 9,
ActiveOutput: "eDP-1",
Layouts: []string{"tile", "monocle"},
Outputs: map[string]*OutputState{
"eDP-1": {
Name: "eDP-1",
Active: 1,
Layout: 0,
LayoutSymbol: "[]=",
Title: "Firefox",
AppID: "firefox",
KbLayout: "us",
Keymode: "",
Tags: []TagState{{Tag: 1, State: 1, Clients: 2, Focused: 1}},
},
},
}
assert.False(t, stateChanged(a, b))
}
func TestManager_ConcurrentGetState(t *testing.T) {
m := &Manager{
state: &State{
TagCount: 9,
Layouts: []string{"tile"},
Outputs: map[string]*OutputState{"eDP-1": {Name: "eDP-1"}},
},
}
var wg sync.WaitGroup
const goroutines = 50
const iterations = 100
for i := 0; i < goroutines/2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < iterations; j++ {
s := m.GetState()
_ = s.TagCount
_ = s.Outputs
}
}()
}
for i := 0; i < goroutines/2; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
for j := 0; j < iterations; j++ {
m.stateMutex.Lock()
m.state = &State{
TagCount: uint32(j % 10),
Layouts: []string{"tile", "monocle"},
Outputs: map[string]*OutputState{"eDP-1": {Active: uint32(j % 2)}},
}
m.stateMutex.Unlock()
}
}(i)
}
wg.Wait()
}
func TestManager_ConcurrentSubscriberAccess(t *testing.T) {
m := &Manager{
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
}
var wg sync.WaitGroup
const goroutines = 20
for i := 0; i < goroutines; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
subID := string(rune('a' + id))
ch := m.Subscribe(subID)
assert.NotNil(t, ch)
time.Sleep(time.Millisecond)
m.Unsubscribe(subID)
}(i)
}
wg.Wait()
}
func TestManager_SyncmapOutputsConcurrentAccess(t *testing.T) {
m := &Manager{}
var wg sync.WaitGroup
const goroutines = 30
const iterations = 50
for i := 0; i < goroutines; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
key := uint32(id)
for j := 0; j < iterations; j++ {
state := &outputState{
id: key,
name: "test-output",
active: uint32(j % 2),
tags: []TagState{{Tag: uint32(j), State: 1}},
}
m.outputs.Store(key, state)
if loaded, ok := m.outputs.Load(key); ok {
assert.Equal(t, key, loaded.id)
}
m.outputs.Range(func(k uint32, v *outputState) bool {
_ = v.name
_ = v.active
return true
})
}
m.outputs.Delete(key)
}(i)
}
wg.Wait()
}
func TestManager_NotifySubscribersNonBlocking(t *testing.T) {
m := &Manager{
dirty: make(chan struct{}, 1),
}
for i := 0; i < 10; i++ {
m.notifySubscribers()
}
assert.Len(t, m.dirty, 1)
}
func TestManager_PostQueueFull(t *testing.T) {
m := &Manager{
cmdq: make(chan cmd, 2),
stopChan: make(chan struct{}),
}
m.post(func() {})
m.post(func() {})
m.post(func() {})
m.post(func() {})
assert.Len(t, m.cmdq, 2)
}
func TestManager_GetStateNilState(t *testing.T) {
m := &Manager{}
s := m.GetState()
assert.NotNil(t, s.Outputs)
assert.NotNil(t, s.Layouts)
assert.Equal(t, uint32(0), s.TagCount)
}
func TestTagState_Fields(t *testing.T) {
tag := TagState{
Tag: 1,
State: 2,
Clients: 3,
Focused: 1,
}
assert.Equal(t, uint32(1), tag.Tag)
assert.Equal(t, uint32(2), tag.State)
assert.Equal(t, uint32(3), tag.Clients)
assert.Equal(t, uint32(1), tag.Focused)
}
func TestOutputState_Fields(t *testing.T) {
out := OutputState{
Name: "eDP-1",
Active: 1,
Tags: []TagState{{Tag: 1}},
Layout: 0,
LayoutSymbol: "[]=",
Title: "Firefox",
AppID: "firefox",
KbLayout: "us",
Keymode: "",
}
assert.Equal(t, "eDP-1", out.Name)
assert.Equal(t, uint32(1), out.Active)
assert.Len(t, out.Tags, 1)
assert.Equal(t, "[]=", out.LayoutSymbol)
}
func TestStateChanged_NewOutputAppears(t *testing.T) {
a := &State{
TagCount: 9,
Layouts: []string{},
Outputs: map[string]*OutputState{
"eDP-1": {Name: "eDP-1"},
},
}
b := &State{
TagCount: 9,
Layouts: []string{},
Outputs: map[string]*OutputState{
"eDP-1": {Name: "eDP-1"},
"HDMI-A-1": {Name: "HDMI-A-1"},
},
}
assert.True(t, stateChanged(a, b))
}
func TestStateChanged_TagsLengthDiffers(t *testing.T) {
a := &State{
TagCount: 9,
Layouts: []string{},
Outputs: map[string]*OutputState{
"eDP-1": {Tags: []TagState{{Tag: 1}}},
},
}
b := &State{
TagCount: 9,
Layouts: []string{},
Outputs: map[string]*OutputState{
"eDP-1": {Tags: []TagState{{Tag: 1}, {Tag: 2}}},
},
}
assert.True(t, stateChanged(a, b))
}
func TestNewManager_GetRegistryError(t *testing.T) {
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
mockDisplay.EXPECT().Context().Return(nil)
mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry"))
_, err := NewManager(mockDisplay)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to get registry")
}
-176
View File
@@ -1,176 +0,0 @@
package dwl
import (
"sync"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
type TagState struct {
Tag uint32 `json:"tag"`
State uint32 `json:"state"`
Clients uint32 `json:"clients"`
Focused uint32 `json:"focused"`
}
type OutputState struct {
Name string `json:"name"`
Active uint32 `json:"active"`
Tags []TagState `json:"tags"`
Layout uint32 `json:"layout"`
LayoutSymbol string `json:"layoutSymbol"`
Title string `json:"title"`
AppID string `json:"appId"`
KbLayout string `json:"kbLayout"`
Keymode string `json:"keymode"`
}
type State struct {
Outputs map[string]*OutputState `json:"outputs"`
TagCount uint32 `json:"tagCount"`
Layouts []string `json:"layouts"`
ActiveOutput string `json:"activeOutput"`
}
type cmd struct {
fn func()
}
type Manager struct {
display wlclient.WaylandDisplay
ctx *wlclient.Context
registry *wlclient.Registry
manager any
outputs syncmap.Map[uint32, *outputState]
tagCount uint32
layouts []string
wlMutex sync.Mutex
cmdq chan cmd
outputSetupReq chan uint32
stopChan chan struct{}
wg sync.WaitGroup
subscribers syncmap.Map[string, chan State]
dirty chan struct{}
notifierWg sync.WaitGroup
lastNotified *State
stateMutex sync.RWMutex
state *State
}
type outputState struct {
id uint32
registryName uint32
output *wlclient.Output
ipcOutput any
name string
active uint32
tags []TagState
layout uint32
layoutSymbol string
title string
appID string
kbLayout string
keymode string
}
func (m *Manager) GetState() State {
m.stateMutex.RLock()
defer m.stateMutex.RUnlock()
if m.state == nil {
return State{
Outputs: make(map[string]*OutputState),
Layouts: []string{},
TagCount: 0,
}
}
stateCopy := *m.state
return stateCopy
}
func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 64)
m.subscribers.Store(id, ch)
return ch
}
func (m *Manager) Unsubscribe(id string) {
if val, ok := m.subscribers.LoadAndDelete(id); ok {
close(val)
}
}
func (m *Manager) notifySubscribers() {
select {
case m.dirty <- struct{}{}:
default:
}
}
func stateChanged(old, new *State) bool {
if old == nil || new == nil {
return true
}
if old.TagCount != new.TagCount {
return true
}
if len(old.Layouts) != len(new.Layouts) {
return true
}
if old.ActiveOutput != new.ActiveOutput {
return true
}
if len(old.Outputs) != len(new.Outputs) {
return true
}
for name, newOut := range new.Outputs {
oldOut, exists := old.Outputs[name]
if !exists {
return true
}
if oldOut.Active != newOut.Active {
return true
}
if oldOut.Layout != newOut.Layout {
return true
}
if oldOut.LayoutSymbol != newOut.LayoutSymbol {
return true
}
if oldOut.Title != newOut.Title {
return true
}
if oldOut.AppID != newOut.AppID {
return true
}
if oldOut.KbLayout != newOut.KbLayout {
return true
}
if oldOut.Keymode != newOut.Keymode {
return true
}
if len(oldOut.Tags) != len(newOut.Tags) {
return true
}
for i, newTag := range newOut.Tags {
if i >= len(oldOut.Tags) {
return true
}
oldTag := oldOut.Tags[i]
if oldTag.Tag != newTag.Tag || oldTag.State != newTag.State ||
oldTag.Clients != newTag.Clients || oldTag.Focused != newTag.Focused {
return true
}
}
}
return false
}
-10
View File
@@ -11,7 +11,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups"
serverDbus "github.com/AvengeMedia/DankMaterialShell/core/internal/server/dbus"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/location"
@@ -125,15 +124,6 @@ func RouteRequest(conn net.Conn, req models.Request) {
return
}
if strings.HasPrefix(req.Method, "dwl.") {
if dwlManager == nil {
models.RespondError(conn, req.ID, "dwl manager not initialized")
return
}
dwl.HandleRequest(conn, req, dwlManager)
return
}
if strings.HasPrefix(req.Method, "brightness.") {
if brightnessManager == nil {
models.RespondError(conn, req.ID, "brightness manager not initialized")
+1 -87
View File
@@ -22,7 +22,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups"
serverDbus "github.com/AvengeMedia/DankMaterialShell/core/internal/server/dbus"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/location"
@@ -39,7 +38,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
const APIVersion = 24
const APIVersion = 25
var CLIVersion = "dev"
@@ -66,7 +65,6 @@ var bluezManager *bluez.Manager
var appPickerManager *apppicker.Manager
var cupsManager *cups.Manager
var tailscaleManager *tailscale.Manager
var dwlManager *dwl.Manager
var brightnessManager *brightness.Manager
var wlrOutputManager *wlroutput.Manager
var evdevManager *evdev.Manager
@@ -252,30 +250,6 @@ func InitializeCupsManager() error {
return nil
}
func InitializeDwlManager() error {
log.Info("Attempting to initialize DWL IPC...")
if wlContext == nil {
ctx, err := wlcontext.New()
if err != nil {
log.Errorf("Failed to create shared Wayland context: %v", err)
return err
}
wlContext = ctx
}
manager, err := dwl.NewManager(wlContext.Display())
if err != nil {
log.Debug("Failed to initialize dwl manager: %v", err)
return err
}
dwlManager = manager
log.Info("DWL IPC initialized successfully")
return nil
}
func InitializeBrightnessManager() error {
manager, err := brightness.NewManager()
if err != nil {
@@ -468,10 +442,6 @@ func getCapabilities() Capabilities {
caps = append(caps, "tailscale")
}
if dwlManager != nil {
caps = append(caps, "dwl")
}
if brightnessManager != nil {
caps = append(caps, "brightness")
}
@@ -538,10 +508,6 @@ func getServerInfo() ServerInfo {
caps = append(caps, "tailscale")
}
if dwlManager != nil {
caps = append(caps, "dwl")
}
if brightnessManager != nil {
caps = append(caps, "brightness")
}
@@ -1046,38 +1012,6 @@ func handleSubscribe(conn net.Conn, req models.Request) {
}()
}
if shouldSubscribe("dwl") && dwlManager != nil {
wg.Add(1)
dwlChan := dwlManager.Subscribe(clientID + "-dwl")
go func() {
defer wg.Done()
defer dwlManager.Unsubscribe(clientID + "-dwl")
initialState := dwlManager.GetState()
select {
case eventChan <- ServiceEvent{Service: "dwl", Data: initialState}:
case <-stopChan:
return
}
for {
select {
case state, ok := <-dwlChan:
if !ok {
return
}
select {
case eventChan <- ServiceEvent{Service: "dwl", Data: state}:
case <-stopChan:
return
}
case <-stopChan:
return
}
}
}()
}
if shouldSubscribe("brightness") && brightnessManager != nil {
wg.Add(2)
brightnessStateChan := brightnessManager.Subscribe(clientID + "-brightness-state")
@@ -1333,9 +1267,6 @@ func cleanupManagers() {
if cupsManager != nil {
cupsManager.Close()
}
if dwlManager != nil {
dwlManager.Close()
}
if brightnessManager != nil {
brightnessManager.Close()
}
@@ -1502,19 +1433,6 @@ func Start(printDocs bool) error {
log.Info(" cups.resumePrinter - Resume printer (params: printerName)")
log.Info(" cups.cancelJob - Cancel job (params: printerName, jobID)")
log.Info(" cups.purgeJobs - Cancel all jobs (params: printerName)")
log.Info("DWL:")
log.Info(" dwl.getState - Get current dwl state (tags, windows, layouts, keyboard)")
log.Info(" dwl.setTags - Set active tags (params: output, tagmask, toggleTagset)")
log.Info(" dwl.setClientTags - Set focused client tags (params: output, andTags, xorTags)")
log.Info(" dwl.setLayout - Set layout (params: output, index)")
log.Info(" dwl.subscribe - Subscribe to dwl state changes (streaming)")
log.Info(" Output state includes:")
log.Info(" - tags : Tag states (active, clients, focused)")
log.Info(" - layoutSymbol : Current layout name")
log.Info(" - title : Focused window title")
log.Info(" - appId : Focused window app ID")
log.Info(" - kbLayout : Current keyboard layout")
log.Info(" - keymode : Current keybind mode")
log.Info("Brightness:")
log.Info(" brightness.getState - Get current brightness state for all devices")
log.Info(" brightness.setBrightness - Set device brightness (params: device, percent)")
@@ -1691,10 +1609,6 @@ func Start(printDocs bool) error {
log.Debugf("AppPicker manager unavailable: %v", err)
}
if err := InitializeDwlManager(); err != nil {
log.Debugf("DWL manager unavailable: %v", err)
}
if err := InitializeWlrOutputManager(); err != nil {
log.Debugf("WlrOutput manager unavailable: %v", err)
}
+2 -2
View File
@@ -201,7 +201,7 @@ func (m Model) viewInstallComplete() string {
wm := m.selectedWindowManager()
// mango launches DMS via `exec_once=dms run` (not a systemd session target)
// mango launches DMS via `exec-once=dms run` (not a systemd session target)
loginHint := "If you do not have a greeter, login with \"niri-session\" or \"Hyprland\""
switch wm {
case deps.WindowManagerNiri:
@@ -223,7 +223,7 @@ func (m Model) viewInstallComplete() string {
b.WriteString(labelStyle.Render("Troubleshooting:") + "\n")
if wm == deps.WindowManagerMango {
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("remove 'exec_once=dms run' from ~/.config/mango/config.conf") + "\n")
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("remove 'exec-once=dms run' from ~/.config/mango/config.conf") + "\n")
b.WriteString(labelStyle.Render(" View logs: ") + cmdStyle.Render("qs -p ~/.config/quickshell/dms log") + "\n")
} else {
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("systemctl --user disable dms") + "\n")
+4
View File
@@ -11,6 +11,10 @@ Singleton {
readonly property int durMed: 450
readonly property int durLong: 600
// Navigation feedback stays responsive even when ambient shell motion is slow.
readonly property int settingsNavigationStateDuration: 180
readonly property int settingsNavigationRippleDuration: 200
readonly property int slidePx: 80
readonly property var emphasized: [0.05, 0.00, 0.133333, 0.06, 0.166667, 0.40, 0.208333, 0.82, 0.25, 1.00, 1.00, 1.00]
+111
View File
@@ -0,0 +1,111 @@
pragma ComponentBehavior: Bound
import QtQuick
Item {
id: root
required property var modalHandle
required property string claimPrefix
property string surfaceKind: "modal"
property string screenName: ""
property bool enabled: false
property bool active: false
property bool presented: false
property bool dockBlocked: false
property string dockSide: ""
property alias claimId: lease.claimId
property alias claimedScreenName: lease.claimedScreenName
signal recoveryRequested
visible: false
function _isCurrentModal(name) {
return !!name && ModalManager.isCurrentModal(modalHandle, name);
}
ConnectedSurfaceLease {
id: lease
claimPrefix: root.claimPrefix
screenName: root.screenName
enabled: root.enabled
active: root.active
presented: root.presented
dockBlocked: root.dockBlocked
dockSide: root.dockSide
isCurrentOwner: function(name) {
return root._isCurrentModal(name);
}
hasOwner: function(name, ownerId) {
return ConnectedModeState.hasModalOwner(name, ownerId);
}
statePresent: function(name, ownerId) {
return ConnectedModeState.hasModalOwner(name, ownerId) && ConnectedModeState.hasSurfaceDescriptor(name, root.surfaceKind, ownerId);
}
claimState: function(name, state, ownerId) {
return ConnectedModeState.claimModalState(name, state, ownerId);
}
ensureState: function(name, state, ownerId) {
return ConnectedModeState.ensureModalState(name, state, ownerId);
}
releaseState: function(name, ownerId) {
return ConnectedModeState.clearModalState(name, ownerId);
}
updateAnimationState: function(name, ownerId, animX, animY) {
return ConnectedModeState.setModalAnim(name, animX, animY, ownerId);
}
updateBodyState: function(name, ownerId, bodyX, bodyY, bodyW, bodyH) {
return ConnectedModeState.setModalBody(name, bodyX, bodyY, bodyW, bodyH, ownerId);
}
requestDockRetract: function(ownerId, name, side) {
return ConnectedModeState.requestDockRetract(ownerId, name, side);
}
releaseDockRetract: function(ownerId) {
return ConnectedModeState.releaseDockRetract(ownerId);
}
onRecoveryRequested: root.recoveryRequested()
}
function publish(state) {
return lease.publish(Object.assign({}, state, {
"kind": root.surfaceKind,
"screenName": root.screenName,
"presented": root.presented,
"dockRetractSide": root.dockBlocked ? root.dockSide : ""
}), false);
}
function updateAnim(animX, animY) {
return lease.updateAnim(animX, animY);
}
function updateBody(bodyX, bodyY, bodyW, bodyH) {
return lease.updateBody(bodyX, bodyY, bodyW, bodyH);
}
function release() {
return lease.release();
}
Connections {
target: ModalManager
function onModalChanged() {
lease.requestRecovery();
}
}
Connections {
target: ConnectedModeState
function onModalOwnersChanged() {
lease.checkOwnershipRecovery();
}
function onModalStatesChanged() {
lease.checkStateRecovery();
}
function onSurfaceDescriptorsChanged() {
lease.checkStateRecovery();
}
}
}
+243 -40
View File
@@ -3,10 +3,123 @@ pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import "ConnectedSurfaceDescriptor.js" as SurfaceDescriptor
Singleton {
id: root
property var surfaceDescriptors: ({})
function _surfaceSlot(kind) {
return SurfaceDescriptor.slotForKind(kind);
}
function surfaceDescriptor(screenName, kind) {
const slot = _surfaceSlot(kind);
const screenDescriptors = screenName ? surfaceDescriptors[screenName] : null;
const descriptor = screenDescriptors && screenDescriptors[slot] ? screenDescriptors[slot] : SurfaceDescriptor.empty(kind, screenName);
let bodyRect = descriptor.bodyRect;
let animationOffset = descriptor.animationOffset;
if (slot === "popout" && popoutScreen === screenName) {
bodyRect = {
"x": popoutBodyX,
"y": popoutBodyY,
"width": popoutBodyW,
"height": popoutBodyH
};
animationOffset = {
"x": popoutAnimX,
"y": popoutAnimY
};
} else if (slot === "modal" && modalStates[screenName]) {
const modal = modalStates[screenName];
bodyRect = {
"x": modal.bodyX,
"y": modal.bodyY,
"width": modal.bodyW,
"height": modal.bodyH
};
animationOffset = {
"x": modal.animX,
"y": modal.animY
};
} else if (slot === "dock" && dockStates[screenName]) {
const dock = dockStates[screenName];
const slide = dockSlides[screenName] || {
"x": dock.slideX,
"y": dock.slideY
};
bodyRect = {
"x": dock.bodyX,
"y": dock.bodyY,
"width": dock.bodyW,
"height": dock.bodyH
};
animationOffset = {
"x": slide.x,
"y": slide.y
};
} else if (slot === "notification" && notificationStates[screenName]) {
const notification = notificationStates[screenName];
bodyRect = {
"x": notification.bodyX,
"y": notification.bodyY,
"width": notification.bodyW,
"height": notification.bodyH
};
}
return SurfaceDescriptor.normalize({
"bodyRect": bodyRect,
"animationOffset": animationOffset
}, descriptor);
}
function hasSurfaceDescriptor(screenName, kind, ownerId) {
const descriptor = surfaceDescriptor(screenName, kind);
return descriptor.phase !== "hidden" && (!ownerId || descriptor.ownerId === ownerId);
}
function _setSurfaceDescriptor(screenName, slotKind, state, ownerId) {
if (!screenName || !state)
return false;
const slot = _surfaceSlot(slotKind);
const currentScreen = surfaceDescriptors[screenName] || {};
const previous = currentScreen[slot] || SurfaceDescriptor.empty(state.kind || slotKind, screenName);
let normalized = SurfaceDescriptor.normalize(Object.assign({}, state, {
"ownerId": ownerId !== undefined ? ownerId : previous.ownerId,
"screenName": screenName,
"revision": previous.revision
}), previous);
if (SurfaceDescriptor.same(previous, normalized))
return true;
normalized = SurfaceDescriptor.withRevision(normalized, previous.revision + 1);
const nextScreen = _cloneDict(currentScreen);
nextScreen[slot] = normalized;
const next = _cloneDict(surfaceDescriptors);
next[screenName] = nextScreen;
surfaceDescriptors = next;
return true;
}
function _clearSurfaceDescriptor(screenName, kind, ownerId) {
if (!screenName)
return false;
const slot = _surfaceSlot(kind);
const currentScreen = surfaceDescriptors[screenName];
const current = currentScreen ? currentScreen[slot] : null;
if (!current || (ownerId && current.ownerId !== ownerId))
return false;
const nextScreen = _cloneDict(currentScreen);
delete nextScreen[slot];
const next = _cloneDict(surfaceDescriptors);
if (Object.keys(nextScreen).length > 0)
next[screenName] = nextScreen;
else
delete next[screenName];
surfaceDescriptors = next;
return true;
}
readonly property var emptyDockState: ({
"reveal": false,
"barSide": "bottom",
@@ -18,7 +131,6 @@ Singleton {
"slideY": 0
})
// Popout state (updated by DankPopout when connectedFrameModeActive)
property string popoutOwnerId: ""
property bool popoutVisible: false
property string popoutBarSide: "top"
@@ -32,12 +144,12 @@ Singleton {
property bool popoutOmitStartConnector: false
property bool popoutOmitEndConnector: false
// Dock state (updated by Dock when connectedFrameModeActive), keyed by screen.name
property var dockStates: ({})
// Dock slide offsets hot-path updates separated from full geometry state
property var dockSlides: ({})
property var surfaceRevisions: ({})
function _cloneDict(src) {
const next = {};
for (const k in src)
@@ -45,16 +157,33 @@ Singleton {
return next;
}
function _bumpSurfaceRevision(screenName) {
if (!screenName)
return;
const next = _cloneDict(surfaceRevisions);
next[screenName] = Number(next[screenName] || 0) + 1;
surfaceRevisions = next;
}
function hasPopoutOwner(claimId) {
return !!claimId && popoutOwnerId === claimId;
}
function claimPopout(claimId, state) {
if (!claimId)
if (!claimId || !state)
return false;
const previousScreen = popoutScreen;
popoutOwnerId = claimId;
return updatePopout(claimId, state);
const ok = updatePopout(claimId, state);
if (ok) {
if (previousScreen && previousScreen !== popoutScreen) {
_clearSurfaceDescriptor(previousScreen, "popout");
_bumpSurfaceRevision(previousScreen);
}
_bumpSurfaceRevision(popoutScreen);
}
return ok;
}
function updatePopout(claimId, state) {
@@ -84,6 +213,21 @@ Singleton {
if (state.omitEndConnector !== undefined)
popoutOmitEndConnector = !!state.omitEndConnector;
_setSurfaceDescriptor(popoutScreen, "popout", Object.assign({}, state, {
"kind": "popout",
"screenName": popoutScreen,
"visible": popoutVisible,
"presented": state.presented !== undefined ? !!state.presented : popoutVisible,
"barSide": popoutBarSide,
"bodyX": popoutBodyX,
"bodyY": popoutBodyY,
"bodyW": popoutBodyW,
"bodyH": popoutBodyH,
"animX": popoutAnimX,
"animY": popoutAnimY,
"omitStartConnector": popoutOmitStartConnector,
"omitEndConnector": popoutOmitEndConnector
}), claimId);
return true;
}
@@ -91,6 +235,7 @@ Singleton {
if (!hasPopoutOwner(claimId))
return false;
const releasedScreen = popoutScreen;
popoutOwnerId = "";
popoutVisible = false;
popoutBarSide = "top";
@@ -103,6 +248,8 @@ Singleton {
popoutScreen = "";
popoutOmitStartConnector = false;
popoutOmitEndConnector = false;
_clearSurfaceDescriptor(releasedScreen, "popout", claimId);
_bumpSurfaceRevision(releasedScreen);
return true;
}
@@ -172,12 +319,23 @@ Singleton {
return false;
const normalized = _normalizeDockState(state);
if (_sameDockState(dockStates[screenName], normalized))
return true;
const next = _cloneDict(dockStates);
next[screenName] = normalized;
dockStates = next;
const descriptorState = Object.assign({}, state, normalized, {
"kind": "dock",
"screenName": screenName,
"visible": normalized.reveal,
"presented": normalized.reveal,
"phase": normalized.reveal ? (state.phase || "open") : "hidden"
});
const previous = dockStates[screenName] || emptyDockState;
const stateChanged = !_sameDockState(dockStates[screenName], normalized);
if (stateChanged) {
const next = _cloneDict(dockStates);
next[screenName] = normalized;
dockStates = next;
}
_setSurfaceDescriptor(screenName, "dock", descriptorState, "dock:" + screenName);
if (!!previous.reveal !== !!normalized.reveal)
_bumpSurfaceRevision(screenName);
return true;
}
@@ -188,13 +346,14 @@ Singleton {
const next = _cloneDict(dockStates);
delete next[screenName];
dockStates = next;
_clearSurfaceDescriptor(screenName, "dock");
// Also clear corresponding slide
if (dockSlides[screenName]) {
const nextSlides = _cloneDict(dockSlides);
delete nextSlides[screenName];
dockSlides = nextSlides;
}
_bumpSurfaceRevision(screenName);
return true;
}
@@ -258,12 +417,22 @@ Singleton {
return false;
const normalized = _normalizeNotificationState(state);
if (_sameNotificationState(notificationStates[screenName], normalized))
return true;
const next = _cloneDict(notificationStates);
next[screenName] = normalized;
notificationStates = next;
const descriptorState = Object.assign({}, state, normalized, {
"kind": "notification",
"screenName": screenName,
"presented": normalized.visible,
"phase": normalized.visible ? (state.phase || "open") : "hidden"
});
const previous = notificationStates[screenName] || emptyNotificationState;
const stateChanged = !_sameNotificationState(notificationStates[screenName], normalized);
if (stateChanged) {
const next = _cloneDict(notificationStates);
next[screenName] = normalized;
notificationStates = next;
}
_setSurfaceDescriptor(screenName, "notification", descriptorState, "notification:" + screenName);
if (!!previous.visible !== !!normalized.visible)
_bumpSurfaceRevision(screenName);
return true;
}
@@ -274,10 +443,11 @@ Singleton {
const next = _cloneDict(notificationStates);
delete next[screenName];
notificationStates = next;
_clearSurfaceDescriptor(screenName, "notification");
_bumpSurfaceRevision(screenName);
return true;
}
// DankModal / DankLauncherV2Modal State
readonly property var emptyModalState: ({
"visible": false,
"barSide": "bottom",
@@ -330,52 +500,77 @@ Singleton {
modalOwners = nextOwners;
}
const normalized = _normalizeModalState(state);
if (_sameModalState(modalStates[screenName], normalized))
return true;
const next = _cloneDict(modalStates);
next[screenName] = normalized;
modalStates = next;
_setSurfaceDescriptor(screenName, "modal", Object.assign({}, state, normalized, {
"kind": state.kind || "modal",
"screenName": screenName
}), ownerId || "");
_bumpSurfaceRevision(screenName);
return true;
}
function updateModalState(screenName, state, ownerId) {
if (!screenName || !state)
return false;
if (ownerId && modalOwners[screenName] && modalOwners[screenName] !== ownerId)
if (ownerId && modalOwners[screenName] !== ownerId)
return false;
const normalized = _normalizeModalState(state);
if (_sameModalState(modalStates[screenName], normalized))
return true;
const next = _cloneDict(modalStates);
next[screenName] = normalized;
modalStates = next;
const descriptorState = Object.assign({}, state, normalized, {
"kind": state.kind || (surfaceDescriptor(screenName, "modal").kind || "modal"),
"screenName": screenName
});
if (!_sameModalState(modalStates[screenName], normalized)) {
const next = _cloneDict(modalStates);
next[screenName] = normalized;
modalStates = next;
}
_setSurfaceDescriptor(screenName, "modal", descriptorState, ownerId || modalOwners[screenName] || "");
return true;
}
function setModalState(screenName, state) {
return updateModalState(screenName, state, null);
function hasModalOwner(screenName, ownerId) {
return !!screenName && !!ownerId && modalOwners[screenName] === ownerId;
}
function ensureModalState(screenName, state, ownerId) {
if (!screenName || !state || !ownerId)
return false;
const currentOwner = modalOwners[screenName] || "";
if (currentOwner && currentOwner !== ownerId)
return false;
if (!currentOwner)
return claimModalState(screenName, state, ownerId);
return updateModalState(screenName, state, ownerId);
}
function clearModalState(screenName, ownerId) {
if (!screenName || !modalStates[screenName])
if (!screenName)
return false;
if (ownerId && modalOwners[screenName] && modalOwners[screenName] !== ownerId)
if (ownerId && modalOwners[screenName] !== ownerId)
return false;
if (!modalStates[screenName] && !modalOwners[screenName])
return false;
const next = _cloneDict(modalStates);
delete next[screenName];
modalStates = next;
if (modalStates[screenName]) {
const next = _cloneDict(modalStates);
delete next[screenName];
modalStates = next;
}
if (modalOwners[screenName]) {
const nextOwners = _cloneDict(modalOwners);
delete nextOwners[screenName];
modalOwners = nextOwners;
}
_clearSurfaceDescriptor(screenName, "modal", ownerId);
_bumpSurfaceRevision(screenName);
return true;
}
function setModalAnim(screenName, animX, animY, ownerId) {
if (ownerId && modalOwners[screenName] && modalOwners[screenName] !== ownerId)
if (ownerId && modalOwners[screenName] !== ownerId)
return false;
const cur = screenName ? modalStates[screenName] : null;
if (!cur)
@@ -394,7 +589,7 @@ Singleton {
}
function setModalBody(screenName, bodyX, bodyY, bodyW, bodyH, ownerId) {
if (ownerId && modalOwners[screenName] && modalOwners[screenName] !== ownerId)
if (ownerId && modalOwners[screenName] !== ownerId)
return false;
const cur = screenName ? modalStates[screenName] : null;
if (!cur)
@@ -453,9 +648,6 @@ Singleton {
return false;
}
// Prune state for screens that are no longer connected. Stale entries
// accumulate across hotplug cycles otherwise Frame's per-screen
// FrameInstance doesn't notice when its peer dicts go orphan.
function _pruneToLiveScreens() {
const live = {};
const screens = Quickshell.screens || [];
@@ -492,6 +684,12 @@ Singleton {
const nextModalOwners = pruneKeyed(modalOwners);
if (nextModalOwners !== null)
modalOwners = nextModalOwners;
const nextSurfaceRevisions = pruneKeyed(surfaceRevisions);
if (nextSurfaceRevisions !== null)
surfaceRevisions = nextSurfaceRevisions;
const nextDescriptors = pruneKeyed(surfaceDescriptors);
if (nextDescriptors !== null)
surfaceDescriptors = nextDescriptors;
let retractChanged = false;
const nextRetract = {};
@@ -512,7 +710,12 @@ Singleton {
Connections {
target: Quickshell
function onScreensChanged() {
root._pruneToLiveScreens();
screenPruneAction.schedule();
}
}
DeferredAction {
id: screenPruneAction
onTriggered: root._pruneToLiveScreens()
}
}
@@ -0,0 +1,159 @@
.pragma library
var VALID_KINDS = {
"popout": true,
"modal": true,
"launcher": true,
"dock": true,
"notification": true
};
var VALID_PHASES = {
"opening": true,
"open": true,
"closing": true,
"hidden": true,
"recovering": true
};
function _number(value, fallback) {
var n = Number(value);
return isNaN(n) ? fallback : n;
}
function _bool(value, fallback) {
return value === undefined ? fallback : !!value;
}
function _kind(value, fallback) {
if (VALID_KINDS[value])
return value;
return VALID_KINDS[fallback] ? fallback : "modal";
}
function _defaultBarSide(kind) {
return kind === "popout" || kind === "notification" ? "top" : "bottom";
}
function _barSide(value, fallback) {
if (value === "top" || value === "bottom" || value === "left" || value === "right")
return value;
return fallback;
}
function slotForKind(kind) {
return kind === "launcher" ? "modal" : _kind(kind, "modal");
}
function inferPhase(visible, presented, requestedPhase) {
if (VALID_PHASES[requestedPhase])
return requestedPhase;
if (!visible && !presented)
return "hidden";
if (!visible && presented)
return "closing";
return "open";
}
function normalize(input, defaults) {
var source = input || {};
var base = defaults || {};
var kind = _kind(source.kind, base.kind);
var defaultSide = _defaultBarSide(kind);
var sourceRect = source.bodyRect || {};
var baseRect = base.bodyRect || {};
var sourceOffset = source.animationOffset || {};
var baseOffset = base.animationOffset || {};
var visible = _bool(source.visible !== undefined ? source.visible : source.reveal, _bool(base.visible !== undefined ? base.visible : base.reveal, false));
var presented = _bool(source.presented, _bool(base.presented, visible));
var bodyRect = {
"x": _number(sourceRect.x !== undefined ? sourceRect.x : source.bodyX, _number(baseRect.x !== undefined ? baseRect.x : base.bodyX, 0)),
"y": _number(sourceRect.y !== undefined ? sourceRect.y : source.bodyY, _number(baseRect.y !== undefined ? baseRect.y : base.bodyY, 0)),
"width": Math.max(0, _number(sourceRect.width !== undefined ? sourceRect.width : source.bodyW, _number(baseRect.width !== undefined ? baseRect.width : base.bodyW, 0))),
"height": Math.max(0, _number(sourceRect.height !== undefined ? sourceRect.height : source.bodyH, _number(baseRect.height !== undefined ? baseRect.height : base.bodyH, 0)))
};
var animationOffset = {
"x": _number(sourceOffset.x !== undefined ? sourceOffset.x : (source.animX !== undefined ? source.animX : source.slideX), _number(baseOffset.x !== undefined ? baseOffset.x : (base.animX !== undefined ? base.animX : base.slideX), 0)),
"y": _number(sourceOffset.y !== undefined ? sourceOffset.y : (source.animY !== undefined ? source.animY : source.slideY), _number(baseOffset.y !== undefined ? baseOffset.y : (base.animY !== undefined ? base.animY : base.slideY), 0))
};
var screenName = source.screenName !== undefined ? source.screenName : (source.screen !== undefined ? source.screen : (base.screenName !== undefined ? base.screenName : base.screen));
var opacity = Math.max(0, Math.min(1, _number(source.opacity, _number(base.opacity, 1))));
return {
"ownerId": String(source.ownerId !== undefined ? source.ownerId : (base.ownerId || "")),
"kind": kind,
"screenName": String(screenName || ""),
"phase": inferPhase(visible, presented, source.phase !== undefined ? source.phase : base.phase),
"visible": visible,
"presented": presented,
"barSide": _barSide(source.barSide, _barSide(base.barSide, defaultSide)),
"bodyRect": bodyRect,
"animationOffset": animationOffset,
"scale": Math.max(0, _number(source.scale, _number(base.scale, 1))),
"opacity": opacity,
"omitStartConnector": _bool(source.omitStartConnector, _bool(base.omitStartConnector, false)),
"omitEndConnector": _bool(source.omitEndConnector, _bool(base.omitEndConnector, false)),
"dockRetractSide": String(source.dockRetractSide !== undefined ? source.dockRetractSide : (base.dockRetractSide || "")),
"revision": Math.max(0, Math.floor(_number(source.revision, _number(base.revision, 0))))
};
}
function empty(kind, screenName) {
return normalize({
"kind": kind,
"screenName": screenName || "",
"phase": "hidden",
"visible": false,
"presented": false
});
}
function withRevision(descriptor, revision) {
var next = normalize(descriptor);
next.revision = Math.max(0, Math.floor(_number(revision, next.revision)));
return next;
}
function withAnimationOffset(descriptor, x, y) {
var next = normalize(descriptor);
next.animationOffset = {
"x": x === undefined ? next.animationOffset.x : _number(x, next.animationOffset.x),
"y": y === undefined ? next.animationOffset.y : _number(y, next.animationOffset.y)
};
return next;
}
function withBodyRect(descriptor, x, y, width, height) {
var next = normalize(descriptor);
next.bodyRect = {
"x": x === undefined ? next.bodyRect.x : _number(x, next.bodyRect.x),
"y": y === undefined ? next.bodyRect.y : _number(y, next.bodyRect.y),
"width": width === undefined ? next.bodyRect.width : Math.max(0, _number(width, next.bodyRect.width)),
"height": height === undefined ? next.bodyRect.height : Math.max(0, _number(height, next.bodyRect.height))
};
return next;
}
function same(a, b, threshold) {
if (!a || !b)
return false;
var epsilon = threshold === undefined ? 0.5 : Math.max(0, Number(threshold));
return a.ownerId === b.ownerId
&& a.kind === b.kind
&& a.screenName === b.screenName
&& a.phase === b.phase
&& a.visible === b.visible
&& a.presented === b.presented
&& a.barSide === b.barSide
&& Math.abs(a.bodyRect.x - b.bodyRect.x) < epsilon
&& Math.abs(a.bodyRect.y - b.bodyRect.y) < epsilon
&& Math.abs(a.bodyRect.width - b.bodyRect.width) < epsilon
&& Math.abs(a.bodyRect.height - b.bodyRect.height) < epsilon
&& Math.abs(a.animationOffset.x - b.animationOffset.x) < epsilon
&& Math.abs(a.animationOffset.y - b.animationOffset.y) < epsilon
&& Math.abs(a.scale - b.scale) < 0.0001
&& Math.abs(a.opacity - b.opacity) < 0.0001
&& a.omitStartConnector === b.omitStartConnector
&& a.omitEndConnector === b.omitEndConnector
&& a.dockRetractSide === b.dockRetractSide;
}
@@ -0,0 +1,232 @@
.pragma library
function _number(value, fallback) {
var n = Number(value);
return isNaN(n) ? fallback : n;
}
function snap(value, dpr) {
var scale = dpr || 1;
return Math.round(_number(value, 0) * scale) / scale;
}
function isHorizontal(side) {
return side === "top" || side === "bottom";
}
function isVertical(side) {
return side === "left" || side === "right";
}
function bodyRect(descriptor, dpr) {
var source = descriptor && descriptor.bodyRect ? descriptor.bodyRect : descriptor || {};
return {
"x": snap(source.x !== undefined ? source.x : source.bodyX, dpr),
"y": snap(source.y !== undefined ? source.y : source.bodyY, dpr),
"width": Math.max(0, snap(source.width !== undefined ? source.width : source.bodyW, dpr)),
"height": Math.max(0, snap(source.height !== undefined ? source.height : source.bodyH, dpr))
};
}
function animatedBodyRect(descriptor, dpr) {
var rect = bodyRect(descriptor, dpr);
var offset = descriptor && descriptor.animationOffset ? descriptor.animationOffset : descriptor || {};
var side = descriptor && descriptor.barSide ? descriptor.barSide : "bottom";
var dx = isVertical(side) ? Math.max(-rect.width, Math.min(_number(offset.x !== undefined ? offset.x : offset.animX, 0), rect.width)) : 0;
var dy = isHorizontal(side) ? Math.max(-rect.height, Math.min(_number(offset.y !== undefined ? offset.y : offset.animY, 0), rect.height)) : 0;
return {
"x": snap(rect.x + (side === "right" ? dx : 0), dpr),
"y": snap(rect.y + (side === "bottom" ? dy : 0), dpr),
"width": Math.max(0, snap(rect.width - Math.abs(dx), dpr)),
"height": Math.max(0, snap(rect.height - Math.abs(dy), dpr)),
"dx": snap(dx, dpr),
"dy": snap(dy, dpr)
};
}
function translatedBodyRect(descriptor, dpr) {
var rect = bodyRect(descriptor, dpr);
var offset = descriptor && descriptor.animationOffset ? descriptor.animationOffset : {};
return {
"x": snap(rect.x + _number(offset.x, 0), dpr),
"y": snap(rect.y + _number(offset.y, 0), dpr),
"width": rect.width,
"height": rect.height
};
}
function connectorRadii(descriptor, rect, connectedRadius, surfaceRadius, dpr, nearIncludesSurface) {
var side = descriptor && descriptor.barSide ? descriptor.barSide : "bottom";
var horizontal = isHorizontal(side);
var extent = horizontal ? rect.height : rect.width;
var crossSize = horizontal ? rect.width : rect.height;
var nearLimit = nearIncludesSurface ? Math.min(connectedRadius, surfaceRadius, extent, crossSize / 2) : Math.min(connectedRadius, extent, crossSize / 2);
var farLimit = Math.min(connectedRadius, surfaceRadius, crossSize / 2);
var near = snap(Math.max(0, nearLimit), dpr);
var far = snap(Math.max(0, farLimit), dpr);
var omitStart = !!(descriptor && descriptor.omitStartConnector);
var omitEnd = !!(descriptor && descriptor.omitEndConnector);
return {
"near": near,
"far": far,
"start": omitStart ? 0 : near,
"end": omitEnd ? 0 : near,
"farStart": omitStart ? far : 0,
"farEnd": omitEnd ? far : 0,
"farExtent": Math.max(omitStart ? far : 0, omitEnd ? far : 0)
};
}
function _connectorWidth(side, spacing, radius) {
return isVertical(side) ? spacing + radius : radius;
}
function _connectorHeight(side, spacing, radius) {
return isVertical(side) ? radius : spacing + radius;
}
function connectorRect(side, rect, placement, spacing, radius, dpr) {
var width = _connectorWidth(side, spacing, radius);
var height = _connectorHeight(side, spacing, radius);
var seamX = isVertical(side) ? (side === "left" ? rect.x : rect.x + rect.width) : (placement === "left" ? rect.x : rect.x + rect.width);
var seamY = side === "top" ? rect.y : (side === "bottom" ? rect.y + rect.height : (placement === "left" ? rect.y : rect.y + rect.height));
var x = isVertical(side) ? (side === "left" ? seamX : seamX - width) : (placement === "left" ? seamX - width : seamX);
var y = side === "top" ? seamY : (side === "bottom" ? seamY - height : (placement === "left" ? seamY - height : seamY));
return {
"x": snap(x, dpr),
"y": snap(y, dpr),
"width": Math.max(0, snap(width, dpr)),
"height": Math.max(0, snap(height, dpr))
};
}
function farConnectorRect(side, rect, placement, radius, dpr) {
var x;
var y;
if (isHorizontal(side)) {
x = placement === "left" ? rect.x : rect.x + rect.width - radius;
y = side === "top" ? rect.y + rect.height : rect.y - radius;
} else {
x = side === "left" ? rect.x + rect.width : rect.x - radius;
y = placement === "left" ? rect.y : rect.y + rect.height - radius;
}
return {
"x": snap(x, dpr),
"y": snap(y, dpr),
"width": Math.max(0, snap(radius, dpr)),
"height": Math.max(0, snap(radius, dpr))
};
}
function farBodyCapRect(side, rect, placement, radius, dpr) {
var x;
var y;
if (isHorizontal(side)) {
x = placement === "left" ? rect.x : rect.x + rect.width - radius;
y = side === "top" ? rect.y + rect.height - radius : rect.y;
} else {
x = side === "left" ? rect.x + rect.width - radius : rect.x;
y = placement === "left" ? rect.y : rect.y + rect.height - radius;
}
return {
"x": snap(x, dpr),
"y": snap(y, dpr),
"width": Math.max(0, snap(radius, dpr)),
"height": Math.max(0, snap(radius, dpr))
};
}
function chromeBounds(rect, side, startRadius, endRadius, farExtent, dpr) {
var horizontal = isHorizontal(side);
var bodyOffsetX = horizontal ? startRadius : (side === "right" ? farExtent : 0);
var bodyOffsetY = horizontal ? (side === "bottom" ? farExtent : 0) : startRadius;
return {
"x": snap(rect.x - bodyOffsetX, dpr),
"y": snap(rect.y - bodyOffsetY, dpr),
"width": Math.max(0, snap(horizontal ? rect.width + startRadius + endRadius : rect.width + farExtent, dpr)),
"height": Math.max(0, snap(horizontal ? rect.height + farExtent : rect.height + startRadius + endRadius, dpr)),
"bodyOffsetX": snap(bodyOffsetX, dpr),
"bodyOffsetY": snap(bodyOffsetY, dpr)
};
}
function fillBounds(rect, side, seamOverlap, dpr) {
var overlapX = isHorizontal(side) ? seamOverlap : 0;
var overlapY = isVertical(side) ? seamOverlap : 0;
return {
"x": snap(rect.x - overlapX, dpr),
"y": snap(rect.y - overlapY, dpr),
"width": Math.max(0, snap(rect.width + overlapX * 2, dpr)),
"height": Math.max(0, snap(rect.height + overlapY * 2, dpr))
};
}
function clipEnvelope(rect, side, radii, seamOverlap, dpr) {
var fill = fillBounds(rect, side, seamOverlap, dpr);
var chrome = chromeBounds(fill, side, radii.start, radii.end, radii.farExtent, dpr);
return {
"x": chrome.x,
"y": chrome.y,
"width": chrome.width,
"height": chrome.height,
"bodyX": snap(fill.x - chrome.x, dpr),
"bodyY": snap(fill.y - chrome.y, dpr),
"bodyWidth": fill.width,
"bodyHeight": fill.height
};
}
function blurRegions(descriptor, rect, radii, dpr) {
var side = descriptor.barSide;
var regions = [bodyRect(rect, dpr)];
if (radii.start > 0)
regions.push(connectorRect(side, rect, "left", 0, radii.start, dpr));
if (radii.end > 0)
regions.push(connectorRect(side, rect, "right", 0, radii.end, dpr));
if (radii.farStart > 0) {
regions.push(farConnectorRect(side, rect, "left", radii.farStart, dpr));
regions.push(farBodyCapRect(side, rect, "left", radii.farStart, dpr));
}
if (radii.farEnd > 0) {
regions.push(farConnectorRect(side, rect, "right", radii.farEnd, dpr));
regions.push(farBodyCapRect(side, rect, "right", radii.farEnd, dpr));
}
return regions;
}
function unionBounds(rects, padding, dpr) {
var minX = Infinity;
var minY = Infinity;
var maxX = -Infinity;
var maxY = -Infinity;
for (var i = 0; i < rects.length; i++) {
var rect = rects[i];
if (!rect || rect.width <= 0 || rect.height <= 0)
continue;
minX = Math.min(minX, rect.x);
minY = Math.min(minY, rect.y);
maxX = Math.max(maxX, rect.x + rect.width);
maxY = Math.max(maxY, rect.y + rect.height);
}
if (minX === Infinity)
return {"x": 0, "y": 0, "width": 0, "height": 0};
var pad = Math.max(0, _number(padding, 0));
return {
"x": snap(minX - pad, dpr),
"y": snap(minY - pad, dpr),
"width": Math.max(0, snap(maxX - minX + pad * 2, dpr)),
"height": Math.max(0, snap(maxY - minY + pad * 2, dpr))
};
}
function shadowSourceBounds(descriptor, rect, radii, padding, dpr) {
return unionBounds(blurRegions(descriptor, rect, radii, dpr), padding, dpr);
}
function stableEqual(a, b, dpr) {
if (!a || !b)
return false;
var threshold = 0.5 / (dpr || 1);
return Math.abs(a.x - b.x) < threshold && Math.abs(a.y - b.y) < threshold && Math.abs(a.width - b.width) < threshold && Math.abs(a.height - b.height) < threshold;
}
+176
View File
@@ -0,0 +1,176 @@
pragma ComponentBehavior: Bound
import QtQuick
Item {
id: root
required property string claimPrefix
required property var isCurrentOwner
required property var hasOwner
required property var claimState
required property var ensureState
required property var releaseState
property var statePresent: null
property var updateAnimationState: null
property var updateBodyState: null
property var requestDockRetract: null
property var releaseDockRetract: null
property string screenName: ""
property bool enabled: false
property bool active: false
property bool presented: false
property bool dockBlocked: false
property string dockSide: ""
property bool renewTokenOnRecovery: true
property string claimId: ""
property string claimedScreenName: ""
property int _claimSerial: 0
signal recoveryRequested
visible: false
function _nextClaimId() {
_claimSerial += 1;
return claimPrefix + ":" + (new Date()).getTime() + ":" + _claimSerial + ":" + Math.floor(Math.random() * 1000000);
}
function _isCurrent(name) {
return !!name && !!isCurrentOwner && !!isCurrentOwner(name);
}
function _hasOwner(name, ownerId) {
return !!name && !!ownerId && !!hasOwner && !!hasOwner(name, ownerId);
}
function _hasState(name, ownerId) {
return !statePresent || !!statePresent(name, ownerId);
}
function _shouldRecover() {
return active && enabled && _isCurrent(screenName);
}
function requestRecovery() {
if (!_shouldRecover())
return false;
recoveryRequested();
return true;
}
function checkOwnershipRecovery() {
if (!_shouldRecover())
return false;
if (claimedScreenName === screenName && _hasOwner(screenName, claimId))
return false;
recoveryRequested();
return true;
}
function checkStateRecovery() {
if (!_shouldRecover())
return false;
if (claimedScreenName === screenName && _hasOwner(screenName, claimId) && _hasState(screenName, claimId))
return false;
recoveryRequested();
return true;
}
function checkRecovery() {
return checkStateRecovery();
}
function beginClaim() {
if (claimId && releaseDockRetract)
releaseDockRetract(claimId);
claimId = _nextClaimId();
claimedScreenName = "";
return claimId;
}
function _syncDockRetract() {
if (!claimId)
return;
if (dockBlocked && presented && dockSide && requestDockRetract)
requestDockRetract(claimId, screenName, dockSide);
else if (releaseDockRetract)
releaseDockRetract(claimId);
}
function publish(state, forceClaim) {
if (!enabled || !screenName || !state) {
release();
return false;
}
if (claimedScreenName && claimedScreenName !== screenName)
release();
const current = _isCurrent(screenName);
let claiming = !!forceClaim || !claimId;
if (claiming && !current)
return false;
if (!claimId)
beginClaim();
let published = claiming ? claimState(screenName, state, claimId) : ensureState(screenName, state, claimId);
if (!published && !claiming && current) {
if (renewTokenOnRecovery) {
beginClaim();
} else if (releaseDockRetract) {
releaseDockRetract(claimId);
}
published = claimState(screenName, state, claimId);
}
if (!published)
return false;
claimedScreenName = screenName;
_syncDockRetract();
return true;
}
function updateAnim(animX, animY) {
if (!enabled || !claimId || !claimedScreenName || !updateAnimationState)
return false;
if (!_hasOwner(claimedScreenName, claimId)) {
requestRecovery();
return false;
}
return updateAnimationState(claimedScreenName, claimId, animX, animY);
}
function updateBody(bodyX, bodyY, bodyW, bodyH) {
if (!enabled || !claimId || !claimedScreenName || !updateBodyState)
return false;
if (!_hasOwner(claimedScreenName, claimId)) {
requestRecovery();
return false;
}
return updateBodyState(claimedScreenName, claimId, bodyX, bodyY, bodyW, bodyH);
}
function release() {
if (!claimId) {
claimedScreenName = "";
return false;
}
const releasedClaimId = claimId;
const releasedScreenName = claimedScreenName;
claimId = "";
claimedScreenName = "";
if (releaseDockRetract)
releaseDockRetract(releasedClaimId);
if (releasedScreenName)
return !!releaseState(releasedScreenName, releasedClaimId);
return false;
}
Component.onDestruction: release()
}
+21 -30
View File
@@ -1,7 +1,6 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Effects
import qs.Common
Item {
@@ -19,7 +18,11 @@ Item {
property real bottomRightRadius: targetRadius
property color borderColor: "transparent"
property real borderWidth: 0
property bool useCustomSource: false
property real sourceX: 0
property real sourceY: 0
property real sourceWidth: width
property real sourceHeight: height
property bool shadowEnabled: Theme.elevationEnabled
property real shadowBlurPx: level && level.blurPx !== undefined ? level.blurPx : 0
@@ -28,36 +31,24 @@ Item {
property real shadowOffsetY: Theme.elevationOffsetYFor(level, direction, fallbackOffset)
property color shadowColor: Theme.elevationShadowColor(level)
property real shadowOpacity: 1
property real blurMax: Theme.elevationBlurMax
property alias sourceRect: sourceRect
readonly property var _ambient: Theme.elevationAmbient(level)
readonly property real _pad: shadowEnabled ? Math.ceil(Math.max(shadowBlurPx + shadowSpreadPx + Math.max(Math.abs(shadowOffsetX), Math.abs(shadowOffsetY)), _ambient.blurPx + _ambient.spreadPx) + 2) : 0
layer.enabled: shadowEnabled
layer.effect: MultiEffect {
autoPaddingEnabled: true
shadowEnabled: true
blurEnabled: false
maskEnabled: false
shadowBlur: Math.max(0, Math.min(1, root.shadowBlurPx / Math.max(1, root.blurMax)))
shadowScale: 1 + (2 * root.shadowSpreadPx) / Math.max(1, Math.min(root.width, root.height))
shadowHorizontalOffset: root.shadowOffsetX
shadowVerticalOffset: root.shadowOffsetY
blurMax: root.blurMax
shadowColor: root.shadowColor
shadowOpacity: root.shadowOpacity
}
Rectangle {
id: sourceRect
ShaderEffect {
anchors.fill: parent
visible: !root.useCustomSource
topLeftRadius: root.topLeftRadius
topRightRadius: root.topRightRadius
bottomLeftRadius: root.bottomLeftRadius
bottomRightRadius: root.bottomRightRadius
color: root.targetColor
border.color: root.borderColor
border.width: root.borderWidth
anchors.margins: -root._pad
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/elevation_rect.frag.qsb")
property real widthPx: width
property real heightPx: height
property real borderWidth: root.borderWidth
property vector4d rectPx: Qt.vector4d(root._pad + root.sourceX, root._pad + root.sourceY, root.sourceWidth, root.sourceHeight)
property vector4d cornerRadius: Qt.vector4d(root.topLeftRadius, root.topRightRadius, root.bottomRightRadius, root.bottomLeftRadius)
property vector4d fillColor: Qt.vector4d(root.targetColor.r, root.targetColor.g, root.targetColor.b, root.targetColor.a)
property vector4d borderColor: Qt.vector4d(root.borderColor.r, root.borderColor.g, root.borderColor.b, root.borderColor.a)
property vector4d shadowColor: Qt.vector4d(root.shadowColor.r, root.shadowColor.g, root.shadowColor.b, root.shadowEnabled ? root.shadowColor.a * root.shadowOpacity : 0)
property vector4d shadowParam: Qt.vector4d(Math.max(0, root.shadowBlurPx), root.shadowSpreadPx, root.shadowOffsetX, root.shadowOffsetY)
property vector4d ambientParam: Qt.vector4d(root._ambient.blurPx, root._ambient.spreadPx, root.shadowEnabled ? root._ambient.alpha * root.shadowOpacity : 0, 0)
}
}
+44
View File
@@ -0,0 +1,44 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Services
// Manages keyboard focus policy for popouts, modals, and Hyprland focus grabs
Singleton {
id: root
function keyboardFocus(active, customFocus) {
if (PopoutManager.screenshotActive)
return WlrKeyboardFocus.None;
if (customFocus !== null && customFocus !== undefined)
return customFocus;
if (!active)
return WlrKeyboardFocus.None;
if (CompositorService.useHyprlandFocusGrab)
return WlrKeyboardFocus.OnDemand;
return WlrKeyboardFocus.Exclusive;
}
function wantsGrab(active, customFocus) {
return CompositorService.useHyprlandFocusGrab && keyboardFocus(active, customFocus) === WlrKeyboardFocus.OnDemand;
}
property list<var> barWindows: []
function registerBarWindow(window) {
if (!window || barWindows.indexOf(window) !== -1)
return;
barWindows = barWindows.concat([window]);
}
function unregisterBarWindow(window) {
const idx = barWindows.indexOf(window);
if (idx === -1)
return;
const next = barWindows.slice();
next.splice(idx, 1);
barWindows = next;
}
}
+5
View File
@@ -26,6 +26,11 @@ Singleton {
});
}
function isCurrentModal(modal, screenName) {
const name = screenName || modal?.effectiveScreen?.name || "unknown";
return currentModalsByScreen[name] === modal;
}
function closeModal(modal) {
const screenName = modal.effectiveScreen?.name ?? "unknown";
if (currentModalsByScreen[screenName] === modal) {
+169 -9
View File
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import qs.Common
Singleton {
id: root
@@ -16,8 +17,76 @@ Singleton {
signal popoutOpening
signal popoutChanged
property real hoverCursorGlobalX: 0
property real hoverCursorGlobalY: 0
function updateHoverCursor(gx, gy) {
hoverCursorGlobalX = gx;
hoverCursorGlobalY = gy;
}
function cursorOverBar(gx, gy, padding) {
const pad = padding !== undefined ? padding : 16;
const bars = KeyboardFocus.barWindows || [];
for (let i = 0; i < bars.length; i++) {
const w = bars[i];
if (!w?.visible)
continue;
if (typeof w.containsGlobalPoint === "function") {
if (w.containsGlobalPoint(gx, gy, pad))
return true;
continue;
}
const item = w.contentItem;
if (!item || typeof item.mapToItem !== "function")
continue;
const topLeft = item.mapToItem(null, 0, 0);
if (!topLeft)
continue;
if (gx >= topLeft.x - pad && gx < topLeft.x + item.width + pad && gy >= topLeft.y - pad && gy < topLeft.y + item.height + pad)
return true;
}
return false;
}
function _isPopoutPresented(popout) {
if (!popout)
return false;
try {
if (popout.dashVisible !== undefined)
return !!popout.dashVisible;
if (popout.notificationHistoryVisible !== undefined)
return !!popout.notificationHistoryVisible;
return !!(popout.shouldBeVisible || popout.isClosing);
} catch (e) {
return false;
}
}
function _openPopout(popout) {
if (popout.dashVisible !== undefined) {
if (popout.dashVisible && !popout.shouldBeVisible && !popout.isClosing)
popout.dashVisible = false;
popout.dashVisible = true;
return;
}
if (popout.notificationHistoryVisible !== undefined) {
popout.notificationHistoryVisible = true;
return;
}
popout.open();
}
function _closePopout(popout) {
try {
if (popout?.hoverDismissEnabled) {
if (typeof popout.closeFromHoverDismiss === "function") {
popout.closeFromHoverDismiss();
return;
}
}
if (popout.hoverDismissEnabled !== undefined)
popout.hoverDismissEnabled = false;
switch (true) {
case popout.dashVisible !== undefined:
popout.dashVisible = false;
@@ -89,7 +158,26 @@ Singleton {
continue;
_closePopout(popout);
}
currentPopoutsByScreen = {};
// Keep map entries until each popout's close animation finishes (hidePopout).
}
function closePopoutForScreen(screen) {
if (!screen)
return;
const screenName = screen.name;
const popout = currentPopoutsByScreen[screenName];
if (!popout || _isStale(popout)) {
currentPopoutsByScreen[screenName] = null;
currentPopoutTriggers[screenName] = null;
return;
}
_closePopout(popout);
}
function cancelHoverDismiss(screen) {
const popout = getActivePopout(screen);
if (popout?.cancelHoverDismiss)
popout.cancelHoverDismiss();
}
function getActivePopout(screen) {
@@ -98,9 +186,16 @@ Singleton {
return currentPopoutsByScreen[screen.name] || null;
}
function isCurrentPopout(popout, screenName) {
const name = screenName || popout?.screen?.name || "";
return !!name && currentPopoutsByScreen[name] === popout;
}
function requestPopout(popout, tabIndex, triggerSource) {
if (!popout || !popout.screen)
return;
if (popout.hoverDismissEnabled !== undefined)
popout.hoverDismissEnabled = false;
screenshotActive = false;
const screenName = popout.screen.name;
const currentPopout = currentPopoutsByScreen[screenName];
@@ -176,16 +271,81 @@ Singleton {
ModalManager.closeAllModalsExcept(null);
}
if (movedFromOtherScreen) {
popout.open();
} else {
if (popout.dashVisible !== undefined) {
popout.dashVisible = true;
} else if (popout.notificationHistoryVisible !== undefined) {
popout.notificationHistoryVisible = true;
_openPopout(popout);
}
function requestHoverPopout(popout, tabIndex, triggerSource) {
if (!popout || !popout.screen)
return;
screenshotActive = false;
const screenName = popout.screen.name;
const currentPopout = currentPopoutsByScreen[screenName];
const triggerId = triggerSource !== undefined ? triggerSource : tabIndex;
const willOpen = !(currentPopout === popout && _isPopoutPresented(popout) && triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId);
if (willOpen)
popoutOpening();
let movedFromOtherScreen = false;
for (const otherScreenName in currentPopoutsByScreen) {
if (otherScreenName === screenName)
continue;
const otherPopout = currentPopoutsByScreen[otherScreenName];
if (!otherPopout)
continue;
if (_isStale(otherPopout)) {
currentPopoutsByScreen[otherScreenName] = null;
currentPopoutTriggers[otherScreenName] = null;
continue;
}
if (otherPopout === popout) {
movedFromOtherScreen = true;
currentPopoutsByScreen[otherScreenName] = null;
currentPopoutTriggers[otherScreenName] = null;
continue;
}
_closePopout(otherPopout);
}
if (currentPopout && currentPopout !== popout) {
if (_isStale(currentPopout)) {
currentPopoutsByScreen[screenName] = null;
currentPopoutTriggers[screenName] = null;
} else {
popout.open();
_closePopout(currentPopout);
}
}
if (currentPopout === popout && _isPopoutPresented(popout) && !movedFromOtherScreen) {
if (triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId)
return;
if (tabIndex !== undefined && popout.currentTabIndex !== undefined)
popout.currentTabIndex = tabIndex;
if (popout.updateSurfacePosition)
popout.updateSurfacePosition();
currentPopoutTriggers[screenName] = triggerId;
if (popout.hoverDismissEnabled !== undefined)
popout.hoverDismissEnabled = true;
return;
}
currentPopoutTriggers[screenName] = triggerId;
currentPopoutsByScreen[screenName] = popout;
popoutChanged();
if (tabIndex !== undefined && popout.currentTabIndex !== undefined)
popout.currentTabIndex = tabIndex;
if (currentPopout !== popout)
ModalManager.closeAllModalsExcept(null);
if (popout.hoverDismissEnabled !== undefined)
popout.hoverDismissEnabled = true;
_openPopout(popout);
}
}
+19 -10
View File
@@ -108,6 +108,7 @@ Singleton {
}
property bool clipboardEnterToPaste: false
property var clipboardVisibleEntryActions: ["pin", "edit", "delete"]
property var launcherPluginVisibility: ({})
@@ -177,6 +178,7 @@ Singleton {
property int mangoLayoutGapsOverride: -1
property int mangoLayoutRadiusOverride: -1
property int mangoLayoutBorderSize: -1
property bool mangoTrackpadNaturalScrolling: true
property int firstDayOfWeek: -1
property bool showWeekNumber: false
@@ -488,9 +490,6 @@ Singleton {
"hideOnTouch": false,
"inactiveTimeout": 0
},
"dwl": {
"cursorHideTimeout": 0
},
"mango": {
"cursorHideTimeout": 0
}
@@ -517,6 +516,8 @@ Singleton {
property bool notepadUseMonospace: true
property string notepadFontFamily: ""
property real notepadFontSize: 14
property real notificationSummaryFontSize: Spec.SPEC.notificationSummaryFontSize.def
property real notificationBodyFontSize: Spec.SPEC.notificationBodyFontSize.def
property bool notepadShowLineNumbers: false
property real notepadTransparencyOverride: -1
property real notepadLastCustomTransparency: 0.7
@@ -697,6 +698,7 @@ Singleton {
property int notificationTimeoutNormal: 5000
property int notificationTimeoutCritical: 0
property bool notificationCompactMode: false
property bool notificationShowTimeoutBar: false
property bool notificationDedupeEnabled: true
property int notificationPopupPosition: SettingsData.Position.Top
property int notificationAnimationSpeed: SettingsData.AnimationSpeed.Short
@@ -808,7 +810,8 @@ Singleton {
"shadowOpacity": 60,
"shadowColorMode": "default",
"shadowCustomColor": "#000000",
"clickThrough": false
"clickThrough": false,
"hoverPopouts": false
}
]
@@ -1223,8 +1226,6 @@ Singleton {
NiriService.generateNiriLayoutConfig();
if (CompositorService.isHyprland && typeof HyprlandService !== "undefined")
HyprlandService.generateLayoutConfig();
if (CompositorService.isDwl && typeof DwlService !== "undefined")
DwlService.generateLayoutConfig();
if (CompositorService.isMango && typeof MangoService !== "undefined")
MangoService.generateLayoutConfig();
}
@@ -1651,6 +1652,15 @@ Singleton {
};
}
function effectiveBarConfigForRender(config, usesFrameBarChrome) {
if (!config || !connectedFrameModeActive || usesFrameBarChrome)
return config;
const backup = connectedFrameBarStyleBackups[config.id];
if (!backup)
return config;
return Object.assign({}, config, backup);
}
// Single entry point for connected-mode settings state.
// !active restore backups
function _reconcileConnectedFrameBarStyles() {
@@ -2240,6 +2250,9 @@ Singleton {
function getFilteredScreens(componentId) {
var prefs = screenPreferences && screenPreferences[componentId] || ["all"];
if (componentId === "wallpaper" && Array.isArray(prefs) && prefs.length === 0) {
return [];
}
if (!prefs || prefs.length === 0 || prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all")) {
return Quickshell.screens;
}
@@ -2447,10 +2460,6 @@ Singleton {
HyprlandService.generateCursorConfig();
return;
}
if (CompositorService.isDwl && typeof DwlService !== "undefined") {
DwlService.generateCursorConfig();
return;
}
if (CompositorService.isMango && typeof MangoService !== "undefined") {
MangoService.generateCursorConfig();
return;
+25
View File
@@ -0,0 +1,25 @@
pragma Singleton
import QtQuick
import Quickshell
import qs.Common
Singleton {
id: root
property string selectedBarId: "default"
function normalizeSelectedBar() {
if (SettingsData.getBarConfig(selectedBarId))
return;
selectedBarId = SettingsData.barConfigs[0]?.id ?? "default";
}
Connections {
target: SettingsData
function onBarConfigsChanged() {
root.normalizeSelectedBar();
}
}
}
+10
View File
@@ -911,6 +911,16 @@ Singleton {
}
return Qt.rgba(r, g, b, alpha);
}
function elevationAmbient(level) {
const blur = (level && level.blurPx !== undefined) ? Math.max(0, level.blurPx) : 0;
const alpha = ((level && level.alpha !== undefined) ? level.alpha : 0.3) * 0.5;
return {
blurPx: blur * 1.75,
spreadPx: 1,
alpha: alpha
};
}
function elevationTintOpacity(level) {
if (!level)
return 0;
+6 -6
View File
@@ -361,7 +361,7 @@ Singleton {
}
function launchGreeterAutoLoginSyncTerminalFallback(details) {
ToastService.showWarning(I18n.tr("Opening terminal to update greetd"), I18n.tr("DMS needs administrator access. The terminal closes automatically when done.") + (details ? "\n\n" + details : ""), "dms greeter sync --autologin-only", "greeter-autologin-sync");
ToastService.showWarning(I18n.tr("Opening terminal to update greetd"), I18n.tr("DMS needs administrator access. The terminal closes automatically when done.") + (details ? "\n\n" + details : ""), "dms greeter sync --autologin", "greeter-autologin-sync");
greeterAutoLoginSyncTerminalFallbackStderr = "";
greeterAutoLoginSyncTerminalFallbackProcess.running = true;
}
@@ -530,7 +530,7 @@ Singleton {
}
property var greeterAutoLoginSyncProcess: Process {
command: ["dms", "greeter", "sync", "--yes", "--autologin-only"]
command: ["dms", "greeter", "sync", "--yes", "--autologin"]
running: false
stdout: StdioCollector {
@@ -570,7 +570,7 @@ Singleton {
onExited: exitCode => {
const enabling = root.settingsRoot && root.settingsRoot.greeterAutoLogin;
if (exitCode === 0) {
ToastService.showWarning(enabling ? I18n.tr("Applying auto-login on startup") : I18n.tr("Disabling auto-login on startup"), "", "dms greeter sync --autologin-only", "greeter-autologin-sync");
ToastService.showWarning(enabling ? I18n.tr("Applying auto-login on startup...") : I18n.tr("Disabling auto-login on startup..."), "", "dms greeter sync --autologin", "greeter-autologin-sync");
root.greeterAutoLoginSyncProcess.running = true;
return;
}
@@ -580,7 +580,7 @@ Singleton {
}
property var greeterAutoLoginSyncTerminalFallbackProcess: Process {
command: ["dms", "greeter", "sync", "--terminal", "--yes", "--autologin-only"]
command: ["dms", "greeter", "sync", "--terminal", "--yes", "--autologin"]
running: false
stderr: StdioCollector {
@@ -592,7 +592,7 @@ Singleton {
root.greeterAutoLoginSyncSuccessToast("");
} else {
let details = (root.greeterAutoLoginSyncTerminalFallbackStderr || "").trim();
ToastService.showError(I18n.tr("Couldn't open a terminal for the auto-login update.") + " (exit " + exitCode + ")", details, "dms greeter sync --autologin-only", "greeter-autologin-sync");
ToastService.showError(I18n.tr("Couldn't open a terminal for the auto-login update.") + " (exit " + exitCode + ")", details, "dms greeter sync --autologin", "greeter-autologin-sync");
}
root.finishGreeterAutoLoginSync();
}
@@ -645,7 +645,7 @@ Singleton {
onExited: exitCode => {
const err = (root.authApplySudoProbeStderr || "").trim();
if (exitCode === 0) {
ToastService.showInfo(I18n.tr("Applying authentication changes"), "", "", "auth-sync");
ToastService.showInfo(I18n.tr("Applying authentication changes..."), "", "", "auth-sync");
root.authApplyProcess.running = true;
return;
}
+7 -1
View File
@@ -33,6 +33,7 @@ var SPEC = {
mangoLayoutGapsOverride: { def: -1, onChange: "updateCompositorLayout" },
mangoLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
mangoTrackpadNaturalScrolling: { def: true, onChange: "updateCompositorCursor" },
firstDayOfWeek: { def: -1 },
showWeekNumber: { def: false },
@@ -237,7 +238,7 @@ var SPEC = {
qt6ctAvailable: { def: false, persist: false },
gtkAvailable: { def: false, persist: false },
cursorSettings: { def: { theme: "System Default", size: 24, niri: { hideWhenTyping: false, hideAfterInactiveMs: 0 }, hyprland: { hideOnKeyPress: false, hideOnTouch: false, inactiveTimeout: 0 }, dwl: { cursorHideTimeout: 0 } }, onChange: "updateCompositorCursor" },
cursorSettings: { def: { theme: "System Default", size: 24, niri: { hideWhenTyping: false, hideAfterInactiveMs: 0 }, hyprland: { hideOnKeyPress: false, hideOnTouch: false, inactiveTimeout: 0 }, dwl: { cursorHideTimeout: 0 }, mango: { cursorHideTimeout: 0 } }, onChange: "updateCompositorCursor" },
availableCursorThemes: { def: ["System Default"], persist: false },
systemDefaultCursorTheme: { def: "", persist: false },
@@ -259,6 +260,8 @@ var SPEC = {
notepadUseMonospace: { def: true },
notepadFontFamily: { def: "" },
notepadFontSize: { def: 14 },
notificationSummaryFontSize: { def: 0 },
notificationBodyFontSize: { def: 0 },
notepadShowLineNumbers: { def: false },
notepadTransparencyOverride: { def: -1 },
notepadLastCustomTransparency: { def: 0.7 },
@@ -405,6 +408,7 @@ var SPEC = {
notificationTimeoutNormal: { def: 5000 },
notificationTimeoutCritical: { def: 0 },
notificationCompactMode: { def: false },
notificationShowTimeoutBar: { def: false },
notificationDedupeEnabled: { def: true },
notificationPopupPosition: { def: 0 },
notificationAnimationSpeed: { def: 1 },
@@ -568,6 +572,7 @@ var SPEC = {
builtInPluginSettings: { def: {} },
clipboardEnterToPaste: { def: false },
clipboardVisibleEntryActions: { def: ["pin", "edit", "delete"] },
launcherPluginVisibility: { def: {} },
launcherPluginOrder: { def: [] },
@@ -593,6 +598,7 @@ function getValidKeys() {
function set(root, key, value, saveFn, hooks) {
if (!(key in SPEC)) return;
if (value === undefined || value === null) value = SPEC[key].def;
root[key] = value;
var hookName = SPEC[key].onChange;
if (hookName && hooks && hooks[hookName]) {
+22 -21
View File
@@ -64,27 +64,15 @@ Item {
}
}
property bool wallpaperSurfacesLoaded: true
Loader {
id: blurredWallpaperBackgroundLoader
active: root.wallpaperSurfacesLoaded && SettingsData.blurredWallpaperLayer && CompositorService.isNiri
active: SettingsData.blurredWallpaperLayer && CompositorService.isNiri
asynchronous: false
sourceComponent: BlurredWallpaperBackground {}
}
DeferredAction {
id: wallpaperSurfaceReloadAction
onTriggered: root.wallpaperSurfacesLoaded = true
}
Loader {
id: wallpaperBackgroundLoader
active: root.wallpaperSurfacesLoaded
asynchronous: false
sourceComponent: WallpaperBackground {}
}
WallpaperBackground {}
DesktopWidgetLayer {}
@@ -328,6 +316,16 @@ Item {
}
property bool hadRealScreen: true
property var previousRealScreenNames: []
function _getRealScreenNames() {
const names = [];
for (let i = 0; i < Quickshell.screens.length; i++) {
if (Quickshell.screens[i].name.length > 0)
names.push(Quickshell.screens[i].name);
}
return names;
}
function _hasRealScreen() {
for (let i = 0; i < Quickshell.screens.length; i++) {
@@ -353,14 +351,20 @@ Item {
target: Quickshell
function onScreensChanged() {
const hasReal = root._hasRealScreen();
const currentNames = root._getRealScreenNames();
log.info("Screens changed:", Quickshell.screens.length,
Quickshell.screens.map(s => "'" + s.name + "'").join(","),
"hasReal:", hasReal, "hadReal:", root.hadRealScreen);
if (!root.hadRealScreen && hasReal) {
log.info("Real screen reappeared after placeholder state, triggering surface recovery");
const fullReconnect = !root.hadRealScreen && hasReal;
const partialReconnect = root.previousRealScreenNames.length > 0
&& currentNames.some(name => !root.previousRealScreenNames.includes(name));
if (fullReconnect || partialReconnect) {
log.info("Screen reconnect detected, triggering surface recovery",
"full:", fullReconnect, "partial:", partialReconnect);
root.triggerSurfaceRecovery("screen-reconnect");
}
root.hadRealScreen = hasReal;
root.previousRealScreenNames = currentNames;
}
}
@@ -382,11 +386,6 @@ Item {
frameSurfaceReloadAction.schedule();
}
if (root.wallpaperSurfacesLoaded) {
root.wallpaperSurfacesLoaded = false;
wallpaperSurfaceReloadAction.schedule();
}
root.dockEnabled = false;
Qt.callLater(() => {
root.dockEnabled = true;
@@ -1124,6 +1123,7 @@ Item {
id: powerMenuModal
onPowerActionRequested: (action, title, message) => {
PopoutService.closeControlCenter();
switch (action) {
case "logout":
SessionService.logout();
@@ -1144,6 +1144,7 @@ Item {
}
onLockRequested: {
PopoutService.closeControlCenter();
lock.activate();
}
+1 -4
View File
@@ -337,9 +337,6 @@ Item {
const focusedWs = I3.workspaces.values.find(ws => ws.focused === true);
return focusedWs?.monitor?.name || "";
}
if (CompositorService.isDwl && DwlService.activeOutput) {
return DwlService.activeOutput;
}
if (CompositorService.isMango && MangoService.activeOutput) {
return MangoService.activeOutput;
}
@@ -947,7 +944,7 @@ Item {
function tabs(): string {
if (!PopoutService.settingsModal)
return "wallpaper\ntheme\ntypography\ntime_weather\nsounds\ndankbar\ndankbar_settings\ndankbar_widgets\nworkspaces\nmedia_player\nnotifications\nosd\nrunning_apps\nupdater\ndock\nlauncher\nkeybinds\ndisplays\nnetwork\nprinters\nlock_screen\npower_sleep\nplugins\nabout";
return "wallpaper\ntheme\ntypography\ntime_weather\nsounds\ndankbar\ndankbar_settings\ndankbar_appearance\ndankbar_widgets\nframe\nworkspaces\ncompositor\nmedia_player\nnotifications\nosd\nrunning_apps\nupdater\ndock\nlauncher\nkeybinds\ndisplays\nnetwork\nprinters\nlock_screen\npower_sleep\nplugins\nabout";
var modal = PopoutService.settingsModal;
var ids = [];
var structure = modal.sidebar?.categoryStructure ?? [];
@@ -1,5 +1,4 @@
import QtQuick
import Quickshell.Hyprland
import qs.Common
import qs.Modals.Common
import qs.Services
@@ -11,11 +10,6 @@ DankModal {
layerNamespace: "dms:bluetooth-pairing"
HyprlandFocusGrab {
windows: [root.contentWindow]
active: root.useHyprlandFocusGrab && root.shouldHaveFocus
}
property string deviceName: ""
property string deviceAddress: ""
property string requestType: ""
@@ -7,7 +7,6 @@ Item {
id: clipboardContent
required property var modal
required property var clearConfirmDialog
property alias searchField: searchField
property alias clipboardListView: clipboardListView
@@ -33,14 +32,7 @@ Item {
pinnedCount: modal.pinnedCount
onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints
onTabChanged: tabName => modal.activeTab = tabName
onClearAllClicked: {
const hasPinned = modal.pinnedCount > 0;
const message = hasPinned ? I18n.tr("This will delete all unpinned entries. %1 pinned entries will be kept.").arg(modal.pinnedCount) : I18n.tr("This will permanently delete all clipboard history.");
clearConfirmDialog.show(I18n.tr("Clear History?"), message, function () {
modal.clearAll();
modal.hide();
}, function () {});
}
onClearAllClicked: modal.confirmClearAll()
onCloseClicked: modal.hide()
}
@@ -128,7 +120,7 @@ Item {
}
StyledText {
text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No recent clipboard entries found") : I18n.tr("Connecting to clipboard service")
text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No recent clipboard entries found") : I18n.tr("Connecting to clipboard service...")
anchors.centerIn: parent
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
@@ -149,8 +141,8 @@ Item {
listView: clipboardListView
onCopyRequested: clipboardContent.modal.copyEntry(modelData)
onDeleteRequested: clipboardContent.modal.deleteEntry(modelData)
onPinRequested: clipboardContent.modal.pinEntry(modelData)
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
onPinRequested: targetEntry => clipboardContent.modal.pinEntry(targetEntry)
onUnpinRequested: targetEntry => clipboardContent.modal.unpinEntry(targetEntry)
onEditRequested: clipboardContent.modal.editEntry(modelData)
}
}
@@ -202,7 +194,7 @@ Item {
}
StyledText {
text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No saved clipboard entries") : I18n.tr("Connecting to clipboard service")
text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No saved clipboard entries") : I18n.tr("Connecting to clipboard service...")
anchors.centerIn: parent
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
@@ -223,8 +215,8 @@ Item {
listView: savedListView
onCopyRequested: clipboardContent.modal.copyEntry(modelData)
onDeleteRequested: clipboardContent.modal.deletePinnedEntry(modelData)
onPinRequested: clipboardContent.modal.pinEntry(modelData)
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
onPinRequested: targetEntry => clipboardContent.modal.pinEntry(targetEntry)
onUnpinRequested: targetEntry => clipboardContent.modal.unpinEntry(targetEntry)
onEditRequested: clipboardContent.modal.editEntry(modelData)
}
}
+44 -8
View File
@@ -15,13 +15,21 @@ Rectangle {
signal copyRequested
signal deleteRequested
signal pinRequested
signal unpinRequested
signal pinRequested(var targetEntry)
signal unpinRequested(var targetEntry)
signal editRequested
readonly property string entryType: modal ? modal.getEntryType(entry) : "text"
readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : ""
readonly property bool hasPinnedDuplicate: !entry.pinned && ClipboardService.hashedPinnedEntry(entry.hash)
readonly property var pinnedDuplicateEntry: !entry.pinned ? ClipboardService.getPinnedEntryByHash(entry.hash) : null
readonly property bool hasPinnedDuplicate: pinnedDuplicateEntry !== null
readonly property bool effectivePinned: entry.pinned || hasPinnedDuplicate
readonly property var visibleEntryActions: SettingsData.clipboardVisibleEntryActions || ["pin", "edit", "delete"]
readonly property bool showPinAction: visibleEntryActions.includes("pin")
readonly property bool showEditAction: visibleEntryActions.includes("edit")
readonly property bool showDeleteAction: visibleEntryActions.includes("delete")
readonly property bool showPinnedIndicator: hasPinnedDuplicate && !showPinAction
readonly property bool showAnyAction: showPinAction || showEditAction || showDeleteAction || showPinnedIndicator
radius: Theme.cornerRadius
color: {
@@ -62,19 +70,46 @@ Rectangle {
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
visible: root.showAnyAction
Item {
width: 40
height: 40
visible: root.showPinnedIndicator
// Status indicator only; the Pin action remains hidden.
DankIcon {
anchors.centerIn: parent
name: "push_pin"
size: Theme.iconSize - 6
color: Theme.primary
}
}
DankActionButton {
iconName: "push_pin"
iconSize: Theme.iconSize - 6
iconColor: (entry.pinned || hasPinnedDuplicate) ? Theme.primary : Theme.surfaceText
backgroundColor: (entry.pinned || hasPinnedDuplicate) ? Theme.primarySelected : "transparent"
onClicked: entry.pinned ? unpinRequested() : pinRequested()
visible: root.showPinAction
onClicked: {
if (entry.pinned) {
unpinRequested(entry);
return;
}
if (pinnedDuplicateEntry) {
unpinRequested(pinnedDuplicateEntry);
return;
}
pinRequested(entry);
}
}
DankActionButton {
iconName: "edit"
iconSize: Theme.iconSize - 6
iconColor: Theme.surfaceText
visible: root.showEditAction
onClicked: {
if (entryType === "image") {
@@ -88,6 +123,7 @@ Rectangle {
iconName: "close"
iconSize: Theme.iconSize - 6
iconColor: Theme.surfaceText
visible: root.showDeleteAction
onClicked: deleteRequested()
}
}
@@ -95,8 +131,8 @@ Rectangle {
Item {
anchors.left: indexBadge.right
anchors.leftMargin: Theme.spacingM
anchors.right: actionButtons.left
anchors.rightMargin: Theme.spacingM
anchors.right: root.showAnyAction ? actionButtons.left : parent.right
anchors.rightMargin: root.showAnyAction ? Theme.spacingM : Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
// height: contentColumn.implicitHeight
height: ClipboardConstants.itemHeight
@@ -157,8 +193,8 @@ Rectangle {
MouseArea {
id: mouseArea
anchors.left: parent.left
anchors.right: actionButtons.left
anchors.rightMargin: Theme.spacingS
anchors.right: root.showAnyAction ? actionButtons.left : parent.right
anchors.rightMargin: root.showAnyAction ? Theme.spacingS : 0
anchors.top: parent.top
anchors.bottom: parent.bottom
hoverEnabled: true
@@ -50,7 +50,7 @@ Item {
iconSize: Theme.iconSize - 4
iconColor: header.activeTab === "saved" ? Theme.primary : Theme.surfaceText
backgroundColor: header.activeTab === "saved" ? Theme.primarySelected : "transparent"
visible: header.pinnedCount > 0
visible: header.pinnedCount > 0 || header.activeTab === "saved"
tooltipText: header.activeTab === "saved" ? I18n.tr("Recent") : I18n.tr("Saved")
onClicked: tabChanged(header.activeTab === "saved" ? "recents" : "saved")
}
@@ -36,9 +36,18 @@ FocusScope {
signal instantCloseRequested
onActiveTabChanged: {
if (activeTab === "saved" && pinnedCount === 0) {
activeTab = "recents";
return;
}
ClipboardService.selectedIndex = 0;
ClipboardService.keyboardNavigationActive = false;
}
onPinnedCountChanged: {
if (activeTab === "saved" && pinnedCount === 0) {
activeTab = "recents";
}
}
onSearchTextChanged: ClipboardService.searchText = searchText
function hide() {
@@ -73,6 +82,15 @@ FocusScope {
ClipboardService.clearAll();
}
function confirmClearAll() {
const hasPinned = pinnedCount > 0;
const message = hasPinned ? I18n.tr("This will delete all unpinned entries. %1 pinned entries will be kept.").arg(pinnedCount) : I18n.tr("This will permanently delete all clipboard history.");
clearConfirmDialog.show(I18n.tr("Clear History?"), message, function () {
clearAll();
hide();
}, function () {});
}
function getEntryPreview(entry) {
return ClipboardService.getEntryPreview(entry);
}
@@ -126,7 +144,6 @@ FocusScope {
id: historyContent
anchors.fill: parent
modal: root
clearConfirmDialog: root.clearConfirmDialog
}
}
@@ -1,7 +1,6 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell.Hyprland
import qs.Common
import qs.Modals.Clipboard
import qs.Modals.Common
@@ -12,11 +11,6 @@ DankModal {
layerNamespace: "dms:clipboard"
HyprlandFocusGrab {
windows: [clipboardHistoryModal.contentWindow]
active: clipboardHistoryModal.useHyprlandFocusGrab && clipboardHistoryModal.shouldHaveFocus
}
function toggle() {
if (shouldBeVisible) {
hide();
@@ -64,6 +58,7 @@ DankModal {
readonly property bool clipboardAvailable: ClipboardService.clipboardAvailable
visible: false
keepContentLoaded: true
modalWidth: ClipboardConstants.modalWidth
modalHeight: ClipboardConstants.modalHeight
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
@@ -82,22 +77,35 @@ DankModal {
id: clearConfirmDialog
confirmButtonText: I18n.tr("Clear All")
confirmButtonColor: Theme.primary
onVisibleChanged: {
if (visible) {
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
clipboardHistoryModal.shouldHaveFocus = false;
selectedButton = 0;
keyboardNavigation = true;
return;
}
Qt.callLater(function () {
if (!clipboardHistoryModal.shouldBeVisible) {
return;
}
clipboardHistoryModal.shouldHaveFocus = true;
clipboardHistoryModal.shouldHaveFocus = Qt.binding(() => clipboardHistoryModal.shouldBeVisible);
clipboardHistoryModal.modalFocusScope.forceActiveFocus();
if (clipboardHistoryModal.contentLoader.item?.searchField) {
clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus();
}
});
}
Connections {
target: clearConfirmDialog.modalFocusScope.Keys
function onPressed(event) {
if (!clearConfirmDialog.shouldBeVisible || event.key !== Qt.Key_Backtab) {
return;
}
clearConfirmDialog.selectedButton = clearConfirmDialog.selectedButton === -1 ? 1 : (clearConfirmDialog.selectedButton - 1 + 2) % 2;
clearConfirmDialog.keyboardNavigation = true;
event.accepted = true;
}
}
}
content: Component {
@@ -1,6 +1,7 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell.Wayland
import qs.Common
import qs.Modals.Clipboard
import qs.Modals.Common
@@ -95,6 +96,35 @@ DankPopout {
id: clearConfirmDialog
confirmButtonText: I18n.tr("Clear All")
confirmButtonColor: Theme.primary
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
root.customKeyboardFocus = WlrKeyboardFocus.None;
selectedButton = 0;
keyboardNavigation = true;
return;
}
root.customKeyboardFocus = null;
Qt.callLater(function () {
if (!root.shouldBeVisible || !root.contentLoader.item) {
return;
}
root.contentLoader.item.forceActiveFocus();
if (root.contentLoader.item.searchField) {
root.contentLoader.item.searchField.forceActiveFocus();
}
});
}
Connections {
target: clearConfirmDialog.modalFocusScope.Keys
function onPressed(event) {
if (!clearConfirmDialog.shouldBeVisible || event.key !== Qt.Key_Backtab) {
return;
}
clearConfirmDialog.selectedButton = clearConfirmDialog.selectedButton === -1 ? 1 : (clearConfirmDialog.selectedButton - 1 + 2) % 2;
clearConfirmDialog.keyboardNavigation = true;
event.accepted = true;
}
}
}
content: Component {
@@ -59,8 +59,13 @@ QtObject {
return;
}
const selectedEntry = entries[ClipboardService.selectedIndex];
if (modal.activeTab === "saved") {
if (selectedEntry.pinned) {
modal.unpinEntry(selectedEntry);
return;
}
const pinnedDuplicate = ClipboardService.getPinnedEntryByHash(selectedEntry.hash);
if (pinnedDuplicate) {
modal.unpinEntry(pinnedDuplicate);
} else {
modal.pinEntry(selectedEntry);
}
@@ -120,8 +125,6 @@ QtObject {
if (!ClipboardService.keyboardNavigationActive) {
ClipboardService.keyboardNavigationActive = true;
ClipboardService.selectedIndex = 0;
} else if (ClipboardService.selectedIndex === 0) {
ClipboardService.keyboardNavigationActive = false;
} else {
selectPrevious();
}
@@ -150,8 +153,6 @@ QtObject {
if (!ClipboardService.keyboardNavigationActive) {
ClipboardService.keyboardNavigationActive = true;
ClipboardService.selectedIndex = 0;
} else if (ClipboardService.selectedIndex === 0) {
ClipboardService.keyboardNavigationActive = false;
} else {
selectPrevious();
}
@@ -179,8 +180,7 @@ QtObject {
if (event.modifiers & Qt.ShiftModifier) {
switch (event.key) {
case Qt.Key_Delete:
modal.clearAll();
modal.hide();
modal.confirmClearAll();
event.accepted = true;
return;
case Qt.Key_Return:
+7 -3
View File
@@ -1,4 +1,5 @@
import QtQuick
import Quickshell.Hyprland
import qs.Common
import qs.Services
@@ -52,8 +53,13 @@ Item {
focus: true
anchors.fill: parent
}
// Hyprland OnDemand grab delivers keyboard focus to the modal content surface.
HyprlandFocusGrab {
windows: root.contentWindow ? [root.contentWindow] : []
active: KeyboardFocus.wantsGrab(root.shouldHaveFocus, root.customKeyboardFocus)
}
readonly property var contentWindow: impl.item ? impl.item.contentWindow : null
readonly property var clickCatcher: impl.item ? impl.item.clickCatcher : null
readonly property var effectiveScreen: impl.item ? impl.item.effectiveScreen : null
readonly property real screenWidth: impl.item ? impl.item.screenWidth : 1920
readonly property real screenHeight: impl.item ? impl.item.screenHeight : 1080
@@ -96,8 +102,6 @@ Item {
}
}
// Defer Loader source-component swap until impl is fully closed; avoids
// tearing down a modal mid-animation when frame mode is toggled.
function _maybeResolveBackend() {
if (_resolvedBackend === _desiredBackend)
return;
+287 -380
View File
@@ -31,7 +31,6 @@ Item {
property bool closeOnBackgroundClick: true
property string animationType: "scale"
// Opposite side from the launcher by default; subclasses may override
property string preferredConnectedBarSide: SettingsData.frameModalEmergeSide
readonly property bool frameConnectedMode: SettingsData.frameEnabled && Theme.isConnectedEffect && !!effectiveScreen && SettingsData.isScreenInPreferences(effectiveScreen, SettingsData.frameScreenPreferences)
@@ -87,16 +86,13 @@ Item {
property real frozenMotionOffsetX: 0
property real frozenMotionOffsetY: 0
readonly property alias contentWindow: contentWindow
readonly property alias clickCatcher: clickCatcher
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
readonly property bool useBackground: false
readonly property bool useSingleWindow: CompositorService.isHyprland
signal opened
signal dialogClosed
signal backgroundClicked
// Coalesce per-channel dirty bits; one ConnectedModeState write per tick.
Timer {
id: _syncTimer
interval: 0
@@ -105,52 +101,65 @@ Item {
property bool animationsEnabled: true
property string _chromeClaimId: ""
property bool _fullSyncPending: false
function _nextChromeClaimId() {
return layerNamespace + ":modal:" + (new Date()).getTime() + ":" + Math.floor(Math.random() * 1000);
}
function _currentScreenName() {
return effectiveScreen ? effectiveScreen.name : "";
}
function _publishModalChromeState(isClaim) {
const screenName = _currentScreenName();
if (!screenName)
return;
ConnectedModalChrome {
id: modalChrome
modalHandle: root.modalHandle
claimPrefix: root.layerNamespace + ":modal"
surfaceKind: "modal"
screenName: root._currentScreenName()
enabled: root.frameOwnsConnectedChrome
active: root.shouldBeVisible
presented: root.shouldBeVisible || contentWindow.visible
dockBlocked: root._dockBlocksEmergence
dockSide: root.resolvedConnectedBarSide
onRecoveryRequested: root._queueFullSync()
}
function _publishModalChromeState() {
const presented = shouldBeVisible || contentWindow.visible;
const phase = !presented ? "hidden" : (!shouldBeVisible && contentWindow.visible ? "closing" : (!contentWindow.visible ? "opening" : "open"));
const bodyRect = {
"x": alignedX,
"y": alignedY,
"width": alignedWidth,
"height": alignedHeight
};
const animationOffset = {
"x": modalContainer ? modalContainer.animX : 0,
"y": modalContainer ? modalContainer.animY : 0
};
const state = {
"visible": shouldBeVisible || contentWindow.visible,
"kind": "modal",
"screenName": root._currentScreenName(),
"phase": phase,
"visible": presented,
"presented": presented,
"barSide": resolvedConnectedBarSide,
"bodyRect": bodyRect,
"animationOffset": animationOffset,
"scale": 1,
"opacity": Theme.connectedSurfaceColor.a,
"bodyX": alignedX,
"bodyY": alignedY,
"bodyW": alignedWidth,
"bodyH": alignedHeight,
"animX": modalContainer ? modalContainer.animX : 0,
"animY": modalContainer ? modalContainer.animY : 0,
"animX": animationOffset.x,
"animY": animationOffset.y,
"omitStartConnector": false,
"omitEndConnector": false
"omitEndConnector": false,
"dockRetractSide": root._dockBlocksEmergence ? resolvedConnectedBarSide : ""
};
if (isClaim)
ConnectedModeState.claimModalState(screenName, state, _chromeClaimId);
else
ConnectedModeState.updateModalState(screenName, state, _chromeClaimId);
return modalChrome.publish(state);
}
function _syncModalChromeState() {
if (!frameOwnsConnectedChrome) {
_releaseModalChrome();
return;
}
const isClaim = !_chromeClaimId;
if (!_chromeClaimId)
_chromeClaimId = _nextChromeClaimId();
_publishModalChromeState(isClaim);
if (_dockBlocksEmergence && (shouldBeVisible || contentWindow.visible))
ConnectedModeState.requestDockRetract(_chromeClaimId, _currentScreenName(), resolvedConnectedBarSide);
else
ConnectedModeState.releaseDockRetract(_chromeClaimId);
_publishModalChromeState();
}
property bool _animSyncQueued: false
@@ -187,32 +196,21 @@ Item {
}
function _syncModalAnim() {
if (!frameOwnsConnectedChrome || !_chromeClaimId)
if (!frameOwnsConnectedChrome)
return;
const screenName = _currentScreenName();
if (!screenName || !modalContainer)
if (!modalContainer)
return;
ConnectedModeState.setModalAnim(screenName, modalContainer.animX, modalContainer.animY, _chromeClaimId);
modalChrome.updateAnim(modalContainer.animX, modalContainer.animY);
}
function _syncModalBody() {
if (!frameOwnsConnectedChrome || !_chromeClaimId)
if (!frameOwnsConnectedChrome)
return;
const screenName = _currentScreenName();
if (!screenName)
return;
ConnectedModeState.setModalBody(screenName, alignedX, alignedY, alignedWidth, alignedHeight, _chromeClaimId);
modalChrome.updateBody(alignedX, alignedY, alignedWidth, alignedHeight);
}
function _releaseModalChrome() {
if (!_chromeClaimId)
return;
ConnectedModeState.releaseDockRetract(_chromeClaimId);
const claimId = _chromeClaimId;
_chromeClaimId = "";
const screenName = _currentScreenName();
if (screenName)
ConnectedModeState.clearModalState(screenName, claimId);
modalChrome.release();
}
onFrameOwnsConnectedChromeChanged: _syncModalChromeState()
@@ -223,8 +221,6 @@ Item {
onAlignedWidthChanged: _queueBodySync()
onAlignedHeightChanged: _queueBodySync()
Component.onDestruction: _releaseModalChrome()
Connections {
target: contentWindow
function onVisibleChanged() {
@@ -244,22 +240,16 @@ Item {
const focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen) {
contentWindow.screen = focusedScreen;
if (!useSingleWindow)
clickCatcher.screen = focusedScreen;
}
ModalManager.openModal(modalHandle);
if (Theme.isDirectionalEffect || root.useBackground) {
if (!useSingleWindow)
clickCatcher.visible = true;
contentWindow.visible = true;
}
ModalManager.openModal(modalHandle);
Qt.callLater(() => {
animationsEnabled = true;
shouldBeVisible = true;
if (!useSingleWindow && !clickCatcher.visible)
clickCatcher.visible = true;
if (!contentWindow.visible)
contentWindow.visible = true;
opened();
@@ -286,8 +276,6 @@ Item {
ModalManager.closeModal(modalHandle);
closeTimer.stop();
contentWindow.visible = false;
if (!useSingleWindow)
clickCatcher.visible = false;
dialogClosed();
Qt.callLater(() => animationsEnabled = true);
}
@@ -317,13 +305,15 @@ Item {
break;
}
}
if (screenStillExists)
if (screenStillExists) {
if (root.shouldBeVisible)
root._queueFullSync();
return;
}
root._releaseModalChrome();
const newScreen = CompositorService.getFocusedScreen();
if (newScreen) {
contentWindow.screen = newScreen;
if (!useSingleWindow)
clickCatcher.screen = newScreen;
}
}
}
@@ -335,29 +325,12 @@ Item {
if (shouldBeVisible)
return;
contentWindow.visible = false;
if (!useSingleWindow)
clickCatcher.visible = false;
dialogClosed();
}
}
// shadowRenderPadding is zeroed when frame owns the chrome
// Wayland then clips any content translating past
readonly property var shadowLevel: Theme.elevationLevel3
readonly property real shadowFallbackOffset: 6
readonly property real shadowRenderPadding: (!frameOwnsConnectedChrome && root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
readonly property real shadowMotionPadding: {
if (frameOwnsConnectedChrome)
return 0;
if (animationType === "slide")
return 30;
if (Theme.isDirectionalEffect)
return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.9);
if (Theme.isDepthEffect)
return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.35);
return Math.max(0, animationOffset);
}
readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
@@ -367,7 +340,6 @@ Item {
return SettingsData.frameEdgeInsetForSide(effectiveScreen, side);
}
// frameEdgeInsetForSide is the full inset; do not add frameBarSize
readonly property real _connectedAlignedX: {
switch (resolvedConnectedBarSide) {
case "top":
@@ -430,57 +402,6 @@ Item {
}
})(), dpr)
PanelWindow {
id: clickCatcher
visible: false
color: "transparent"
WlrLayershell.namespace: root.layerNamespace + ":clickcatcher"
WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
anchors {
top: true
left: true
right: true
bottom: true
}
mask: Region {
item: Rectangle {
x: root.alignedX
y: root.alignedY
width: root.alignedWidth
height: root.alignedHeight
}
intersection: Intersection.Xor
}
MouseArea {
anchors.fill: parent
enabled: !root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible
onClicked: root.backgroundClicked()
}
Rectangle {
anchors.fill: parent
z: -1
color: "black"
opacity: (!root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: opacity > 0
Behavior on opacity {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
NumberAnimation {
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
}
}
PanelWindow {
id: contentWindow
visible: false
@@ -490,8 +411,8 @@ Item {
targetWindow: contentWindow
blurEnabled: root.effectiveBlurEnabled && !root.frameOwnsConnectedChrome
readonly property real s: Math.min(1, modalContainer.scaleValue)
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr)
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
blurX: connectedReveal.x + modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr)
blurY: connectedReveal.y + modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
blurWidth: (root.shouldBeVisible && !root.frameOwnsConnectedChrome) ? modalContainer.width * s : 0
blurHeight: (root.shouldBeVisible && !root.frameOwnsConnectedChrome) ? modalContainer.height * s : 0
blurRadius: root.effectiveCornerRadius
@@ -505,36 +426,15 @@ Item {
"error": true
})
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: {
if (customKeyboardFocus !== null)
return customKeyboardFocus;
if (!shouldHaveFocus)
return WlrKeyboardFocus.None;
if (root.useHyprlandFocusGrab)
return WlrKeyboardFocus.OnDemand;
return WlrKeyboardFocus.Exclusive;
}
WlrLayershell.keyboardFocus: KeyboardFocus.keyboardFocus(shouldHaveFocus, customKeyboardFocus)
anchors {
left: true
top: true
right: root.useSingleWindow
bottom: root.useSingleWindow
right: true
bottom: true
}
readonly property real actualMarginLeft: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
readonly property real actualMarginTop: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
WlrLayershell.margins {
left: actualMarginLeft
top: actualMarginTop
right: 0
bottom: 0
}
implicitWidth: root.useSingleWindow ? 0 : root.alignedWidth + (shadowBuffer * 2)
implicitHeight: root.useSingleWindow ? 0 : root.alignedHeight + (shadowBuffer * 2)
onVisibleChanged: {
if (visible)
return;
@@ -546,7 +446,7 @@ Item {
MouseArea {
anchors.fill: parent
enabled: root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible
enabled: root.closeOnBackgroundClick && root.shouldBeVisible
z: -2
onClicked: root.backgroundClicked()
}
@@ -555,7 +455,7 @@ Item {
anchors.fill: parent
z: -1
color: "black"
opacity: (root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
opacity: root.useBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: opacity > 0
Behavior on opacity {
@@ -569,249 +469,256 @@ Item {
}
Item {
id: modalContainer
x: (root.useSingleWindow ? root.alignedX : (root.alignedX - contentWindow.actualMarginLeft)) + Theme.snap(animX, root.dpr)
y: (root.useSingleWindow ? root.alignedY : (root.alignedY - contentWindow.actualMarginTop)) + Theme.snap(animY, root.dpr)
id: connectedReveal
// Clip to final footprint while frame-owned chrome grows from the bar edge.
x: root.alignedX
y: root.alignedY
width: root.alignedWidth
height: root.alignedHeight
MouseArea {
anchors.fill: parent
enabled: root.useSingleWindow && root.shouldBeVisible
hoverEnabled: false
acceptedButtons: Qt.AllButtons
onPressed: mouse.accepted = true
onClicked: mouse.accepted = true
z: -1
}
readonly property bool slide: root.animationType === "slide"
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
readonly property real directionalTravel: Math.max(root.animationOffset, Math.max(root.alignedWidth, root.alignedHeight) * 0.8)
readonly property real depthTravel: Math.max(root.animationOffset * 0.8, 36)
readonly property real customAnchorX: root.alignedX + root.alignedWidth * 0.5
readonly property real customAnchorY: root.alignedY + root.alignedHeight * 0.5
readonly property real customDistLeft: customAnchorX
readonly property real customDistRight: root.screenWidth - customAnchorX
readonly property real customDistTop: customAnchorY
readonly property real customDistBottom: root.screenHeight - customAnchorY
// Connected emergence: travel from the resolved bar edge, matching DankPopout cadence.
readonly property real connectedEmergenceTravelX: Math.max(root.animationOffset, root.alignedWidth + Theme.spacingL)
readonly property real connectedEmergenceTravelY: Math.max(root.animationOffset, root.alignedHeight + Theme.spacingL)
readonly property real offsetX: {
if (root.frameOwnsConnectedChrome) {
switch (root.resolvedConnectedBarSide) {
case "left":
return -connectedEmergenceTravelX;
case "right":
return connectedEmergenceTravelX;
}
return 0;
}
if (slide && !directionalEffect && !depthEffect)
return 15;
if (directionalEffect) {
switch (root.positioning) {
case "top-right":
return 0;
case "custom":
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
return -directionalTravel;
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
return directionalTravel;
return 0;
default:
return 0;
}
}
if (depthEffect) {
switch (root.positioning) {
case "top-right":
return 0;
case "custom":
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
return -depthTravel;
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
return depthTravel;
return 0;
default:
return 0;
}
}
return 0;
}
readonly property real offsetY: {
if (root.frameOwnsConnectedChrome) {
switch (root.resolvedConnectedBarSide) {
case "top":
return -connectedEmergenceTravelY;
case "bottom":
return connectedEmergenceTravelY;
}
return 0;
}
if (slide && !directionalEffect && !depthEffect)
return -30;
if (directionalEffect) {
switch (root.positioning) {
case "top-right":
return -Math.max(directionalTravel * 0.65, 96);
case "custom":
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
return -directionalTravel;
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
return directionalTravel;
return 0;
default:
// Default to sliding down from top when centered
return -Math.max(directionalTravel, root.screenHeight * 0.24);
}
}
if (depthEffect) {
switch (root.positioning) {
case "top-right":
return -depthTravel * 0.75;
case "custom":
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
return -depthTravel;
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
return depthTravel;
return depthTravel * 0.45;
default:
return -depthTravel;
}
}
return root.animationOffset;
}
readonly property real computedScaleCollapsed: root.animationScaleCollapsed
// openProgress: 0 = closed (at frozenMotionOffset, scaleCollapsed), 1 = open (at 0, scale 1).
QtObject {
id: morph
property real openProgress: root.shouldBeVisible ? 1 : 0
Behavior on openProgress {
enabled: root.animationsEnabled
NumberAnimation {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
}
readonly property real animX: root.frozenMotionOffsetX * (1 - morph.openProgress)
readonly property real animY: root.frozenMotionOffsetY * (1 - morph.openProgress)
readonly property real scaleValue: computedScaleCollapsed + (1.0 - computedScaleCollapsed) * morph.openProgress
onAnimXChanged: if (root.frameOwnsConnectedChrome)
root._queueAnimSync()
onAnimYChanged: if (root.frameOwnsConnectedChrome)
root._queueAnimSync()
clip: root.frameOwnsConnectedChrome
Item {
id: contentContainer
anchors.centerIn: parent
width: parent.width
height: parent.height
clip: false
id: modalContainer
x: Theme.snap(animX, root.dpr)
y: Theme.snap(animY, root.dpr)
width: root.alignedWidth
height: root.alignedHeight
MouseArea {
anchors.fill: parent
enabled: root.shouldBeVisible
hoverEnabled: false
acceptedButtons: Qt.AllButtons
onPressed: mouse.accepted = true
onClicked: mouse.accepted = true
z: -1
}
readonly property bool slide: root.animationType === "slide"
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
readonly property real directionalTravel: Math.max(root.animationOffset, Math.max(root.alignedWidth, root.alignedHeight) * 0.8)
readonly property real depthTravel: Math.max(root.animationOffset * 0.8, 36)
readonly property real customAnchorX: root.alignedX + root.alignedWidth * 0.5
readonly property real customAnchorY: root.alignedY + root.alignedHeight * 0.5
readonly property real customDistLeft: customAnchorX
readonly property real customDistRight: root.screenWidth - customAnchorX
readonly property real customDistTop: customAnchorY
readonly property real customDistBottom: root.screenHeight - customAnchorY
readonly property real connectedEmergenceTravelX: Math.max(root.animationOffset, root.alignedWidth + Theme.spacingL)
readonly property real connectedEmergenceTravelY: Math.max(root.animationOffset, root.alignedHeight + Theme.spacingL)
readonly property real offsetX: {
if (root.frameOwnsConnectedChrome) {
switch (root.resolvedConnectedBarSide) {
case "left":
return -connectedEmergenceTravelX;
case "right":
return connectedEmergenceTravelX;
}
return 0;
}
if (slide && !directionalEffect && !depthEffect)
return 15;
if (directionalEffect) {
switch (root.positioning) {
case "top-right":
return 0;
case "custom":
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
return -directionalTravel;
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
return directionalTravel;
return 0;
default:
return 0;
}
}
if (depthEffect) {
switch (root.positioning) {
case "top-right":
return 0;
case "custom":
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
return -depthTravel;
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
return depthTravel;
return 0;
default:
return 0;
}
}
return 0;
}
readonly property real offsetY: {
if (root.frameOwnsConnectedChrome) {
switch (root.resolvedConnectedBarSide) {
case "top":
return -connectedEmergenceTravelY;
case "bottom":
return connectedEmergenceTravelY;
}
return 0;
}
if (slide && !directionalEffect && !depthEffect)
return -30;
if (directionalEffect) {
switch (root.positioning) {
case "top-right":
return -Math.max(directionalTravel * 0.65, 96);
case "custom":
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
return -directionalTravel;
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
return directionalTravel;
return 0;
default:
return -Math.max(directionalTravel, root.screenHeight * 0.24);
}
}
if (depthEffect) {
switch (root.positioning) {
case "top-right":
return -depthTravel * 0.75;
case "custom":
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
return -depthTravel;
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
return depthTravel;
return depthTravel * 0.45;
default:
return -depthTravel;
}
}
return root.animationOffset;
}
readonly property real computedScaleCollapsed: root.animationScaleCollapsed
QtObject {
id: morph
property real openProgress: root.shouldBeVisible ? 1 : 0
Behavior on openProgress {
enabled: root.animationsEnabled
NumberAnimation {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
}
readonly property real animX: root.frozenMotionOffsetX * (1 - morph.openProgress)
readonly property real animY: root.frozenMotionOffsetY * (1 - morph.openProgress)
readonly property real scaleValue: computedScaleCollapsed + (1.0 - computedScaleCollapsed) * morph.openProgress
onAnimXChanged: if (root.frameOwnsConnectedChrome)
root._queueAnimSync()
onAnimYChanged: if (root.frameOwnsConnectedChrome)
root._queueAnimSync()
Item {
id: animatedContent
anchors.fill: parent
id: contentContainer
anchors.centerIn: parent
width: parent.width
height: parent.height
clip: false
property real publishedOpacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (root.shouldBeVisible ? 1 : 0)
opacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (root.shouldBeVisible ? 1 : 0)
scale: modalContainer.scaleValue
transformOrigin: Item.Center
Behavior on opacity {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
NumberAnimation {
duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on publishedOpacity {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
NumberAnimation {
duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
ElevationShadow {
id: modalShadowLayer
Item {
id: animatedContent
anchors.fill: parent
level: root.shadowLevel
fallbackOffset: root.shadowFallbackOffset
targetRadius: root.effectiveCornerRadius
targetColor: root.frameOwnsConnectedChrome ? "transparent" : root.effectiveBackgroundColor
borderColor: root.frameOwnsConnectedChrome ? "transparent" : root.effectiveBorderColor
borderWidth: root.frameOwnsConnectedChrome ? 0 : root.effectiveBorderWidth
shadowEnabled: !root.frameOwnsConnectedChrome && root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
}
Rectangle {
anchors.fill: parent
radius: root.effectiveCornerRadius
color: "transparent"
border.color: (root.connectedSurfaceOverride || root.frameOwnsConnectedChrome) ? "transparent" : BlurService.borderColor
border.width: (root.connectedSurfaceOverride || root.frameOwnsConnectedChrome) ? 0 : BlurService.borderWidth
z: 100
}
FocusScope {
anchors.fill: parent
focus: root.shouldBeVisible
clip: false
Item {
id: directContentWrapper
property real publishedOpacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (root.shouldBeVisible ? 1 : 0)
opacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (root.shouldBeVisible ? 1 : 0)
scale: modalContainer.scaleValue
transformOrigin: Item.Center
Behavior on opacity {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
NumberAnimation {
duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on publishedOpacity {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
NumberAnimation {
duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
ElevationShadow {
id: modalShadowLayer
anchors.fill: parent
visible: root.directContent !== null
focus: true
level: root.shadowLevel
fallbackOffset: root.shadowFallbackOffset
targetRadius: root.effectiveCornerRadius
targetColor: root.frameOwnsConnectedChrome ? "transparent" : root.effectiveBackgroundColor
borderColor: root.frameOwnsConnectedChrome ? "transparent" : root.effectiveBorderColor
borderWidth: root.frameOwnsConnectedChrome ? 0 : root.effectiveBorderWidth
shadowEnabled: !root.frameOwnsConnectedChrome && root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
}
Rectangle {
anchors.fill: parent
radius: root.effectiveCornerRadius
color: "transparent"
border.color: (root.connectedSurfaceOverride || root.frameOwnsConnectedChrome) ? "transparent" : BlurService.borderColor
border.width: (root.connectedSurfaceOverride || root.frameOwnsConnectedChrome) ? 0 : BlurService.borderWidth
z: 100
}
FocusScope {
anchors.fill: parent
focus: root.shouldBeVisible
clip: false
Component.onCompleted: {
if (root.directContent) {
root.directContent.parent = directContentWrapper;
root.directContent.anchors.fill = directContentWrapper;
Qt.callLater(() => root.directContent.forceActiveFocus());
}
}
Item {
id: directContentWrapper
anchors.fill: parent
visible: root.directContent !== null
focus: true
clip: false
Connections {
target: root
function onDirectContentChanged() {
Component.onCompleted: {
if (root.directContent) {
root.directContent.parent = directContentWrapper;
root.directContent.anchors.fill = directContentWrapper;
Qt.callLater(() => root.directContent.forceActiveFocus());
}
}
Connections {
target: root
function onDirectContentChanged() {
if (root.directContent) {
root.directContent.parent = directContentWrapper;
root.directContent.anchors.fill = directContentWrapper;
Qt.callLater(() => root.directContent.forceActiveFocus());
}
}
}
}
}
Loader {
id: contentLoader
anchors.fill: parent
active: root.directContent === null && (root.keepContentLoaded || root.shouldBeVisible || contentWindow.visible)
asynchronous: false
focus: true
clip: false
visible: root.directContent === null
Loader {
id: contentLoader
anchors.fill: parent
active: root.directContent === null && (root.keepContentLoaded || root.shouldBeVisible || contentWindow.visible)
asynchronous: false
focus: true
clip: false
visible: root.directContent === null
onLoaded: {
if (item) {
Qt.callLater(() => item.forceActiveFocus());
onLoaded: {
if (item) {
Qt.callLater(() => item.forceActiveFocus());
}
}
}
}
@@ -205,6 +205,7 @@ Item {
id: clickCatcher
visible: false
color: "transparent"
updatesEnabled: false
WlrLayershell.namespace: root.layerNamespace + ":clickcatcher"
WlrLayershell.layer: WlrLayershell.Top
@@ -259,15 +260,7 @@ Item {
"error": true
})
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: {
if (customKeyboardFocus !== null)
return customKeyboardFocus;
if (!shouldHaveFocus)
return WlrKeyboardFocus.None;
if (root.useHyprlandFocusGrab)
return WlrKeyboardFocus.OnDemand;
return WlrKeyboardFocus.Exclusive;
}
WlrLayershell.keyboardFocus: KeyboardFocus.keyboardFocus(shouldHaveFocus, customKeyboardFocus)
anchors {
left: true
@@ -1,6 +1,5 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import qs.Common
import qs.Modals.Common
@@ -13,11 +12,6 @@ DankModal {
layerNamespace: "dms:color-picker"
HyprlandFocusGrab {
windows: [root.contentWindow]
active: root.useHyprlandFocusGrab && root.shouldHaveFocus
}
property string pickerTitle: I18n.tr("Choose Color")
property color selectedColor: SessionData.recentColors.length > 0 ? SessionData.recentColors[0] : Theme.primary
property var onColorSelectedCallback: null
@@ -30,7 +30,6 @@ Item {
property string _pendingMode: ""
readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose
// Animation state matches DankPopout/DankModal pattern
property bool animationsEnabled: true
property bool _motionActive: false
property real _frozenMotionX: 0
@@ -108,8 +107,6 @@ Item {
return SettingsData.frameEdgeInsetForSide(effectiveScreen, side);
}
// frameEdgeInsetForSide is the full inset; do not add frameBarSize.
// Positions the modal flush to the emerge side, centered on the cross axis.
readonly property var _connectedModalPos: {
const fallback = {
"x": (screenWidth - modalWidth) / 2,
@@ -175,8 +172,6 @@ Item {
readonly property int effectiveBorderWidth: connectedSurfaceOverride ? 0 : borderWidth
readonly property bool effectiveBlurEnabled: Theme.connectedSurfaceBlurEnabled
// Shadow padding for the content window (render padding only, no motion padding).
// Zeroed when frame owns the chrome and Wayland clips past the bar edge
readonly property var shadowLevel: Theme.elevationLevel3
readonly property real shadowFallbackOffset: 6
readonly property real shadowRenderPadding: (!frameOwnsConnectedChrome && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
@@ -203,81 +198,76 @@ Item {
}
readonly property real contentSurfaceHeight: launcherArcExtenderActive ? _connectedChromeHeight : alignedHeight
// For directional/depth: window extends from screen top (content slides within)
// For standard: small window tightly around the modal + shadow padding
readonly property bool _needsExtendedWindow: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) || Theme.isDepthEffect
// Content window geometry
readonly property real _cwMarginLeft: Theme.snap(alignedX - shadowPad, dpr)
readonly property real _cwMarginTop: launcherArcExtenderActive ? _connectedChromeY : (_needsExtendedWindow ? 0 : Theme.snap(alignedY - shadowPad, dpr))
readonly property real _cwWidth: alignedWidth + shadowPad * 2
readonly property real _cwHeight: {
if (launcherArcExtenderActive)
return _connectedChromeHeight;
if (Theme.isDirectionalEffect && !Theme.isConnectedEffect)
return screenHeight + shadowPad;
if (Theme.isDepthEffect)
return alignedY + alignedHeight + shadowPad;
return alignedHeight + shadowPad * 2;
}
// Where the content container sits inside the content window
readonly property real _ccX: shadowPad
readonly property real _ccY: launcherArcExtenderActive ? 0 : (_needsExtendedWindow ? alignedY : shadowPad)
readonly property real _ccX: _connectedChromeX
readonly property real _ccY: _connectedChromeY
signal dialogClosed
// Coalesce per-channel dirty bits; one ConnectedModeState write per tick.
Timer {
id: _syncTimer
interval: 0
onTriggered: root._flushSync()
}
property string _chromeClaimId: ""
property bool _fullSyncPending: false
function _nextChromeClaimId() {
return "dms:launcher-v2:" + (new Date()).getTime() + ":" + Math.floor(Math.random() * 1000);
}
function _currentScreenName() {
return effectiveScreen ? effectiveScreen.name : "";
}
function _publishModalChromeState(isClaim) {
const screenName = _currentScreenName();
if (!screenName)
return;
ConnectedModalChrome {
id: modalChrome
modalHandle: root.modalHandle
claimPrefix: "dms:launcher-v2"
surfaceKind: "launcher"
screenName: root._currentScreenName()
enabled: root.frameOwnsConnectedChrome
active: root.spotlightOpen
presented: root.spotlightOpen || contentWindow.visible
dockBlocked: root._dockBlocksEmergence
dockSide: root.resolvedConnectedBarSide
onRecoveryRequested: root._queueFullSync()
}
function _publishModalChromeState() {
const presented = spotlightOpen || contentWindow.visible;
const phase = !presented ? "hidden" : (isClosing ? "closing" : (!contentWindow.visible ? "opening" : "open"));
const bodyRect = {
"x": _connectedChromeX,
"y": _connectedChromeY,
"width": _connectedChromeWidth,
"height": _connectedChromeHeight
};
const animationOffset = {
"x": contentContainer ? contentContainer.animX : 0,
"y": contentContainer ? contentContainer.animY : 0
};
const state = {
"visible": spotlightOpen || contentWindow.visible,
"kind": "launcher",
"screenName": root._currentScreenName(),
"phase": phase,
"visible": presented,
"presented": presented,
"barSide": resolvedConnectedBarSide,
"bodyRect": bodyRect,
"animationOffset": animationOffset,
"scale": 1,
"opacity": Theme.connectedSurfaceColor.a,
"bodyX": _connectedChromeX,
"bodyY": _connectedChromeY,
"bodyW": _connectedChromeWidth,
"bodyH": _connectedChromeHeight,
"animX": contentContainer ? contentContainer.animX : 0,
"animY": contentContainer ? contentContainer.animY : 0,
"animX": animationOffset.x,
"animY": animationOffset.y,
"omitStartConnector": false,
"omitEndConnector": false
"omitEndConnector": false,
"dockRetractSide": root._dockBlocksEmergence ? resolvedConnectedBarSide : ""
};
if (isClaim)
ConnectedModeState.claimModalState(screenName, state, _chromeClaimId);
else
ConnectedModeState.updateModalState(screenName, state, _chromeClaimId);
return modalChrome.publish(state);
}
function _syncModalChromeState() {
if (!frameOwnsConnectedChrome) {
_releaseModalChrome();
return;
}
const isClaim = !_chromeClaimId;
if (!_chromeClaimId)
_chromeClaimId = _nextChromeClaimId();
_publishModalChromeState(isClaim);
if (_dockBlocksEmergence && (spotlightOpen || contentWindow.visible))
ConnectedModeState.requestDockRetract(_chromeClaimId, _currentScreenName(), resolvedConnectedBarSide);
else
ConnectedModeState.releaseDockRetract(_chromeClaimId);
_publishModalChromeState();
}
property bool _animSyncQueued: false
@@ -314,32 +304,21 @@ Item {
}
function _syncModalAnim() {
if (!frameOwnsConnectedChrome || !_chromeClaimId)
if (!frameOwnsConnectedChrome)
return;
const screenName = _currentScreenName();
if (!screenName || !contentContainer)
if (!contentContainer)
return;
ConnectedModeState.setModalAnim(screenName, contentContainer.animX, contentContainer.animY, _chromeClaimId);
modalChrome.updateAnim(contentContainer.animX, contentContainer.animY);
}
function _syncModalBody() {
if (!frameOwnsConnectedChrome || !_chromeClaimId)
if (!frameOwnsConnectedChrome)
return;
const screenName = _currentScreenName();
if (!screenName)
return;
ConnectedModeState.setModalBody(screenName, _connectedChromeX, _connectedChromeY, _connectedChromeWidth, _connectedChromeHeight, _chromeClaimId);
modalChrome.updateBody(_connectedChromeX, _connectedChromeY, _connectedChromeWidth, _connectedChromeHeight);
}
function _releaseModalChrome() {
if (!_chromeClaimId)
return;
ConnectedModeState.releaseDockRetract(_chromeClaimId);
const claimId = _chromeClaimId;
_chromeClaimId = "";
const screenName = _currentScreenName();
if (screenName)
ConnectedModeState.clearModalState(screenName, claimId);
modalChrome.release();
}
onFrameOwnsConnectedChromeChanged: _syncModalChromeState()
@@ -351,8 +330,6 @@ Item {
onAlignedWidthChanged: _queueBodySync()
onAlignedHeightChanged: _queueBodySync()
Component.onDestruction: _releaseModalChrome()
Connections {
target: contentWindow
function onVisibleChanged() {
@@ -381,8 +358,6 @@ Item {
return;
contentVisible = true;
spotlightContent.closeTransientUi?.();
// NOTE: forceActiveFocus() is deliberately NOT called here.
// It is deferred to after animation starts to avoid compositor IPC stalls.
if (spotlightContent.searchField) {
spotlightContent.searchField.text = query;
@@ -420,40 +395,29 @@ Item {
isClosing = false;
openedFromOverview = false;
// Disable animations so the snap is instant
animationsEnabled = false;
// Freeze the collapsed offsets (they depend on height which could change)
_frozenMotionX = contentContainer ? contentContainer.collapsedMotionX : 0;
_frozenMotionY = contentContainer ? contentContainer.collapsedMotionY : (Theme.isDirectionalEffect ? Math.max(root.screenHeight - root._ccY + root.shadowPad, Theme.effectAnimOffset * 1.1) : -Theme.effectAnimOffset);
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen) {
backgroundWindow.screen = focusedScreen;
contentWindow.screen = focusedScreen;
}
// _motionActive = false ensures motionX/Y snap to frozen collapsed position
_motionActive = false;
// Make windows visible but do NOT request keyboard focus yet
ModalManager.openModal(modalHandle);
spotlightOpen = true;
backgroundWindow.visible = true;
contentWindow.visible = true;
if (useHyprlandFocusGrab)
focusGrab.active = true;
// Load content and initialize (but no forceActiveFocus that's deferred)
_ensureContentLoadedAndInitialize(query || "", mode || "");
// Frame 1: enable animations and trigger enter motion
// Defer focus until after enter motion starts (avoids compositor IPC stalls).
Qt.callLater(() => {
root.animationsEnabled = true;
root._motionActive = true;
// Frame 2: request keyboard focus + activate search field
// Double-deferred to avoid compositor IPC competing with animation frames
Qt.callLater(() => {
root.keyboardActive = true;
if (root.spotlightContent && root.spotlightContent.searchField)
@@ -476,16 +440,13 @@ Item {
spotlightContent?.closeTransientUi?.();
openedFromOverview = false;
isClosing = true;
// For directional effects, defer contentVisible=false so content stays rendered during exit slide
if (!Theme.isDirectionalEffect)
contentVisible = false;
// Trigger exit animation Behaviors will animate motionX/Y to frozen collapsed position
_motionActive = false;
keyboardActive = false;
spotlightOpen = false;
focusGrab.active = false;
ModalManager.closeModal(modalHandle);
closeCleanupTimer.start();
}
@@ -522,7 +483,6 @@ Item {
isClosing = false;
contentVisible = false;
contentWindow.visible = false;
backgroundWindow.visible = false;
if (root.unloadContentOnClose)
launcherContentLoader.active = false;
dialogClosed();
@@ -541,7 +501,7 @@ Item {
HyprlandFocusGrab {
id: focusGrab
windows: [contentWindow]
active: false
active: root.useHyprlandFocusGrab && root.spotlightOpen
onCleared: {
if (spotlightOpen) {
@@ -579,15 +539,18 @@ Item {
}
}
if (!needsReset)
if (!needsReset) {
if (root.spotlightOpen)
root._queueFullSync();
return;
}
const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0];
if (!newScreen)
return;
root._releaseModalChrome();
root._windowEnabled = false;
backgroundWindow.screen = newScreen;
contentWindow.screen = newScreen;
Qt.callLater(() => {
root._windowEnabled = true;
@@ -595,73 +558,6 @@ Item {
}
}
PanelWindow {
id: backgroundWindow
visible: false
color: "transparent"
readonly property real _topMargin: contentContainer.dockTop ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 0 ? Theme.px(42, root.dpr) : 0)
readonly property real _bottomMargin: contentContainer.dockBottom ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 1 ? Theme.px(42, root.dpr) : 0)
readonly property real _leftMargin: contentContainer.dockLeft ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 2 ? Theme.px(42, root.dpr) : 0)
readonly property real _rightMargin: contentContainer.dockRight ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 3 ? Theme.px(42, root.dpr) : 0)
WlrLayershell.namespace: "dms:spotlight:bg"
WlrLayershell.layer: root.effectiveLauncherLayer
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
WlrLayershell.margins {
top: backgroundWindow._topMargin
bottom: backgroundWindow._bottomMargin
left: backgroundWindow._leftMargin
right: backgroundWindow._rightMargin
}
anchors {
top: true
bottom: true
left: true
right: true
}
mask: Region {
item: (spotlightOpen || isClosing) ? bgFullScreenMask : null
Region {
item: bgContentHole
intersection: Intersection.Subtract
}
}
Item {
id: bgFullScreenMask
anchors.fill: parent
}
Item {
id: bgContentHole
visible: false
x: root._cwMarginLeft + contentContainer.x - backgroundWindow._leftMargin
y: root._cwMarginTop + contentContainer.y - backgroundWindow._topMargin
width: root.alignedWidth
height: root.contentSurfaceHeight
}
Rectangle {
id: backgroundDarken
anchors.fill: parent
color: "black"
opacity: 0
visible: false
}
MouseArea {
anchors.fill: parent
enabled: spotlightOpen
onClicked: root.hide()
}
}
PanelWindow {
id: contentWindow
visible: false
@@ -681,23 +577,31 @@ Item {
WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: root.effectiveLauncherLayer
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: PopoutManager.screenshotActive ? WlrKeyboardFocus.None : (keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None)
WlrLayershell.keyboardFocus: KeyboardFocus.keyboardFocus(keyboardActive, null)
anchors {
left: true
top: true
right: true
bottom: true
}
WlrLayershell.margins {
left: root._cwMarginLeft
top: root._cwMarginTop
}
implicitWidth: root._cwWidth
implicitHeight: root._cwHeight
mask: Region {
item: contentInputMask
item: (root.spotlightOpen || root.isClosing) ? dismissArea : contentInputMask
Region {
item: (root.spotlightOpen || root.isClosing) ? contentInputMask : null
}
}
Item {
id: dismissArea
visible: false
anchors.fill: parent
anchors.topMargin: contentContainer.dockTop ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 0 ? Theme.px(42, root.dpr) : 0)
anchors.bottomMargin: contentContainer.dockBottom ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 1 ? Theme.px(42, root.dpr) : 0)
anchors.leftMargin: contentContainer.dockLeft ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 2 ? Theme.px(42, root.dpr) : 0)
anchors.rightMargin: contentContainer.dockRight ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 3 ? Theme.px(42, root.dpr) : 0)
}
Item {
@@ -709,16 +613,31 @@ Item {
height: root.contentSurfaceHeight
}
MouseArea {
anchors.fill: dismissArea
enabled: root.spotlightOpen
z: -2
onClicked: root.hide()
}
Item {
id: contentContainer
// For directional/depth: contentContainer is at alignedY from window top (window starts at screen top)
// For standard: contentContainer is at shadowPad from window top (window starts near modal)
x: root._ccX
y: root._ccY
width: root.alignedWidth
height: root.contentSurfaceHeight
MouseArea {
anchors.fill: parent
enabled: root.spotlightOpen
hoverEnabled: false
acceptedButtons: Qt.AllButtons
onPressed: mouse.accepted = true
onClicked: mouse.accepted = true
z: -1
}
readonly property int dockEdge: typeof SettingsData !== "undefined" ? SettingsData.dockPosition : 1
readonly property bool dockTop: dockEdge === 0
readonly property bool dockBottom: dockEdge === 1
@@ -773,7 +692,6 @@ Item {
return -Math.max((root.shadowPad || 0) + Theme.effectAnimOffset, 40);
}
// openProgress: 0 = closed (at frozenMotion, scaleCollapsed), 1 = open (at 0, scale 1).
QtObject {
id: morph
property real openProgress: root._motionActive ? 1 : 0
@@ -832,7 +750,6 @@ Item {
width: contentContainer.width
height: contentContainer.height
// Shadow mirrors contentWrapper position/scale/opacity
ElevationShadow {
id: launcherShadowLayer
width: parent.width
@@ -850,7 +767,6 @@ Item {
shadowEnabled: !root.frameOwnsConnectedChrome && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
}
// contentWrapper moves inside static contentContainer DankPopout pattern
Item {
id: contentWrapper
width: parent.width
@@ -84,14 +84,14 @@ Item {
readonly property real alignedX: Theme.snap(modalX, dpr)
readonly property real alignedY: Theme.snap(modalY, dpr)
// Extra headroom above the window for the slide-in animation
// Extra headroom above the content for the slide-in animation
readonly property real _animHeadroom: 16
readonly property real windowX: Math.max(0, Theme.snap(alignedX - shadowPad, dpr))
readonly property real windowY: Math.max(0, Theme.snap(alignedY - shadowPad - _animHeadroom, dpr))
readonly property real contentX: Theme.snap(alignedX - windowX, dpr)
readonly property real contentY: Theme.snap(alignedY - windowY, dpr)
readonly property real windowWidth: alignedWidth + contentX + shadowPad
readonly property real _animatedContentH: Theme.snap(_contentImplicitH, dpr)
readonly property real windowWidth: alignedWidth + contentX + shadowPad
readonly property real windowHeight: _animatedContentH + contentY + shadowPad + _animHeadroom
readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
@@ -114,6 +114,7 @@ Item {
}
}
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackgroundDarken
signal dialogClosed
@@ -164,8 +165,6 @@ Item {
openedFromOverview = false;
keyboardActive = true;
ModalManager.openModal(modalHandle);
if (useHyprlandFocusGrab)
focusGrab.active = true;
_ensureContentLoadedAndInitialize(query || "", mode || "");
}
@@ -201,7 +200,6 @@ Item {
contentVisible = false;
keyboardActive = false;
spotlightOpen = false;
focusGrab.active = false;
ModalManager.closeModal(modalHandle);
closeCleanupTimer.start();
}
@@ -231,7 +229,7 @@ Item {
HyprlandFocusGrab {
id: focusGrab
windows: [launcherWindow]
active: false
active: root.useHyprlandFocusGrab && root.keyboardActive
onCleared: {
if (spotlightOpen)
hide();
@@ -270,8 +268,9 @@ Item {
PanelWindow {
id: clickCatcher
screen: launcherWindow.screen
visible: (spotlightOpen || isClosing) && !root.useBackgroundDarken
visible: (spotlightOpen || isClosing) && !root.useSingleWindow
color: "transparent"
updatesEnabled: false
WlrLayershell.namespace: "dms:spotlight:clickcatcher"
WlrLayershell.layer: root.effectiveLauncherLayer
@@ -337,24 +336,24 @@ Item {
WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: root.effectiveLauncherLayer
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: PopoutManager.screenshotActive ? WlrKeyboardFocus.None : (keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None)
WlrLayershell.keyboardFocus: KeyboardFocus.keyboardFocus(keyboardActive, null)
anchors {
top: true
left: true
right: root.useBackgroundDarken
bottom: root.useBackgroundDarken
right: root.useSingleWindow
bottom: root.useSingleWindow
}
WlrLayershell.margins {
left: root.useBackgroundDarken ? 0 : root.windowX
top: root.useBackgroundDarken ? 0 : root.windowY
left: root.useSingleWindow ? 0 : root.windowX
top: root.useSingleWindow ? 0 : root.windowY
right: 0
bottom: 0
}
implicitWidth: root.useBackgroundDarken ? 0 : root.windowWidth
implicitHeight: root.useBackgroundDarken ? 0 : root.windowHeight
implicitWidth: root.useSingleWindow ? 0 : root.windowWidth
implicitHeight: root.useSingleWindow ? 0 : root.windowHeight
mask: Region {
item: inputMask
@@ -364,15 +363,15 @@ Item {
id: inputMask
visible: false
color: "transparent"
x: root.useBackgroundDarken ? 0 : modalContainer.x
y: root.useBackgroundDarken ? 0 : modalContainer.y + modalContainer.slideOffset
width: root.useBackgroundDarken ? launcherWindow.width : root.alignedWidth
height: root.useBackgroundDarken ? launcherWindow.height : root._contentImplicitH
x: root.useSingleWindow ? 0 : modalContainer.x
y: root.useSingleWindow ? 0 : modalContainer.y + modalContainer.slideOffset
width: root.useSingleWindow ? launcherWindow.width : root.alignedWidth
height: root.useSingleWindow ? launcherWindow.height : root._contentImplicitH
}
MouseArea {
anchors.fill: parent
enabled: root.useBackgroundDarken && spotlightOpen
enabled: root.useSingleWindow && spotlightOpen
z: -2
onClicked: root.hide()
}
@@ -396,13 +395,23 @@ Item {
Item {
id: modalContainer
x: root.useBackgroundDarken ? root.alignedX : root.contentX
y: root.useBackgroundDarken ? root.alignedY : root.contentY
x: root.useSingleWindow ? root.alignedX : root.contentX
y: root.useSingleWindow ? root.alignedY : root.contentY
width: root.alignedWidth
height: root._animatedContentH
visible: _renderActive
z: 0
MouseArea {
anchors.fill: parent
enabled: spotlightOpen
hoverEnabled: false
acceptedButtons: Qt.AllButtons
onPressed: mouse.accepted = true
onClicked: mouse.accepted = true
z: -1
}
property bool _renderActive: contentVisible
property real slideOffset: contentVisible ? 0 : -root._animHeadroom
@@ -80,6 +80,7 @@ Item {
readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
readonly property bool useBackgroundDarken: !SettingsData.frameEnabled && SettingsData.modalDarkenBackground
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackgroundDarken
readonly property bool usesOverlayLayer: useBackgroundDarken || SettingsData.launcherUseOverlayLayer || triggerUsesOverlayLayer
readonly property var effectiveLauncherLayer: LayerShell.fromEnv("DMS_MODAL_LAYER", root.usesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top, {
"allow": ["top", "overlay"],
@@ -172,8 +173,6 @@ Item {
keyboardActive = true;
ModalManager.openModal(modalHandle);
if (useHyprlandFocusGrab)
focusGrab.active = true;
_ensureContentLoadedAndInitialize(query || "", mode || "");
}
@@ -211,7 +210,6 @@ Item {
keyboardActive = false;
spotlightOpen = false;
focusGrab.active = false;
ModalManager.closeModal(modalHandle);
closeCleanupTimer.start();
@@ -262,7 +260,7 @@ Item {
HyprlandFocusGrab {
id: focusGrab
windows: [launcherWindow]
active: false
active: root.useHyprlandFocusGrab && root.keyboardActive
onCleared: {
if (spotlightOpen) {
@@ -306,8 +304,9 @@ Item {
PanelWindow {
id: clickCatcher
screen: launcherWindow.screen
visible: (spotlightOpen || isClosing) && !root.useBackgroundDarken
visible: (spotlightOpen || isClosing) && !root.useSingleWindow
color: "transparent"
updatesEnabled: false
WlrLayershell.namespace: "dms:spotlight:clickcatcher"
WlrLayershell.layer: root.effectiveLauncherLayer
@@ -373,24 +372,24 @@ Item {
WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: root.effectiveLauncherLayer
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: PopoutManager.screenshotActive ? WlrKeyboardFocus.None : (keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None)
WlrLayershell.keyboardFocus: KeyboardFocus.keyboardFocus(keyboardActive, null)
anchors {
top: true
left: true
right: root.useBackgroundDarken
bottom: root.useBackgroundDarken
right: root.useSingleWindow
bottom: root.useSingleWindow
}
WlrLayershell.margins {
left: root.useBackgroundDarken ? 0 : root.windowX
top: root.useBackgroundDarken ? 0 : root.windowY
left: root.useSingleWindow ? 0 : root.windowX
top: root.useSingleWindow ? 0 : root.windowY
right: 0
bottom: 0
}
implicitWidth: root.useBackgroundDarken ? 0 : root.windowWidth
implicitHeight: root.useBackgroundDarken ? 0 : root.windowHeight
implicitWidth: root.useSingleWindow ? 0 : root.windowWidth
implicitHeight: root.useSingleWindow ? 0 : root.windowHeight
mask: Region {
item: launcherInputMask
@@ -400,15 +399,15 @@ Item {
id: launcherInputMask
visible: false
color: "transparent"
x: root.useBackgroundDarken ? 0 : modalContainer.x
y: root.useBackgroundDarken ? 0 : modalContainer.y
width: root.useBackgroundDarken ? launcherWindow.width : modalContainer.width
height: root.useBackgroundDarken ? launcherWindow.height : modalContainer.height
x: root.useSingleWindow ? 0 : modalContainer.x
y: root.useSingleWindow ? 0 : modalContainer.y
width: root.useSingleWindow ? launcherWindow.width : modalContainer.width
height: root.useSingleWindow ? launcherWindow.height : modalContainer.height
}
MouseArea {
anchors.fill: parent
enabled: root.useBackgroundDarken && spotlightOpen
enabled: root.useSingleWindow && spotlightOpen
z: -2
onClicked: root.hide()
}
@@ -432,13 +431,23 @@ Item {
Item {
id: modalContainer
x: root.useBackgroundDarken ? root.alignedX : root.contentX
y: root.useBackgroundDarken ? root.alignedY : root.contentY
x: root.useSingleWindow ? root.alignedX : root.contentX
y: root.useSingleWindow ? root.alignedY : root.contentY
width: root.alignedWidth
height: root.alignedHeight
visible: _renderActive
z: 0
MouseArea {
anchors.fill: parent
enabled: spotlightOpen
hoverEnabled: false
acceptedButtons: Qt.AllButtons
onPressed: mouse.accepted = true
onClicked: mouse.accepted = true
z: -1
}
property bool _renderActive: contentVisible
property real publishedScale: contentVisible ? 1 : 0.96
property real publishedOpacity: contentVisible ? 1 : 0
@@ -15,6 +15,7 @@ DankModal {
shouldBeVisible: false
allowStacking: true
useOverlayLayer: true
modalWidth: 420
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 200
@@ -25,6 +25,7 @@ DankModal {
closeOnEscapeKey: true
closeOnBackgroundClick: true
allowStacking: true
useOverlayLayer: true
keepPopoutsOpen: true
onBackgroundClicked: close()
@@ -320,8 +320,6 @@ Item {
url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings";
else if (CompositorService.isHyprland)
url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings-1";
else if (CompositorService.isDwl)
url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings-2";
else if (CompositorService.isMango)
url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings-2";
Qt.openUrlExternally(url);
@@ -130,7 +130,7 @@ Item {
title: I18n.tr("Multi-Monitor", "greeter feature card title")
description: I18n.tr("Per-screen config", "greeter feature card description")
onClicked: {
const hasDisplayConfig = CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango;
const hasDisplayConfig = CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango;
PopoutService.openSettingsWithTab(hasDisplayConfig ? "display_config" : "display_widgets");
}
}
+2 -8
View File
@@ -1,7 +1,6 @@
import QtQml
import QtQuick
import QtQuick.Layouts
import Quickshell.Hyprland
import qs.Common
import qs.Modals.Common
import qs.Services
@@ -14,8 +13,8 @@ DankModal {
useOverlayLayer: true
property real scrollStep: 60
property var activeFlickable: null
property real _maxW: Math.min(Screen.width * 0.92, 1200)
property real _maxH: Math.min(Screen.height * 0.92, 900)
property real _maxW: Math.min(root.screenWidth * 0.92, 1200)
property real _maxH: Math.min(root.screenHeight * 0.92, 900)
modalWidth: _maxW
modalHeight: _maxH
onBackgroundClicked: close()
@@ -29,11 +28,6 @@ DankModal {
KeybindsService.loadCheatsheet();
}
HyprlandFocusGrab {
windows: [root.contentWindow]
active: root.useHyprlandFocusGrab && root.shouldHaveFocus
}
function scrollDown() {
if (!root.activeFlickable)
return;
-7
View File
@@ -3,7 +3,6 @@ pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Hyprland
import Quickshell.Io
import Quickshell
import qs.Common
@@ -45,12 +44,6 @@ DankModal {
}
}
HyprlandFocusGrab {
id: grab
windows: [muxModal.contentWindow]
active: CompositorService.isHyprland && muxModal.shouldHaveFocus
}
function toggle() {
if (shouldBeVisible) {
hide();
-6
View File
@@ -1,5 +1,4 @@
import QtQuick
import Quickshell.Hyprland
import Quickshell.Io
import qs.Common
import qs.Modals.Common
@@ -11,11 +10,6 @@ DankModal {
layerNamespace: "dms:notification-center-modal"
HyprlandFocusGrab {
windows: [notificationModal.contentWindow]
active: notificationModal.useHyprlandFocusGrab && notificationModal.shouldHaveFocus
}
property bool notificationModalOpen: false
property var notificationListRef: null
property var historyListRef: null
+58 -36
View File
@@ -1,7 +1,6 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Hyprland
import qs.Common
import qs.Modals.Common
import qs.Services
@@ -13,11 +12,6 @@ DankModal {
layerNamespace: "dms:power-menu"
keepPopoutsOpen: true
HyprlandFocusGrab {
windows: [root.contentWindow]
active: root.useHyprlandFocusGrab && root.shouldHaveFocus
}
property int selectedIndex: 0
property int selectedRow: 0
property int selectedCol: 0
@@ -352,9 +346,11 @@ DankModal {
break;
case Qt.Key_P:
if (!(event.modifiers & Qt.ControlModifier)) {
const idx = visibleActions.indexOf("poweroff");
startHold("poweroff", idx);
event.accepted = true;
if (visibleActions.includes("poweroff")) {
const idx = visibleActions.indexOf("poweroff");
startHold("poweroff", idx);
event.accepted = true;
}
} else {
selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length;
event.accepted = true;
@@ -373,28 +369,40 @@ DankModal {
}
break;
case Qt.Key_R:
startHold("reboot", visibleActions.indexOf("reboot"));
event.accepted = true;
if (visibleActions.includes("reboot")) {
startHold("reboot", visibleActions.indexOf("reboot"));
event.accepted = true;
}
break;
case Qt.Key_X:
startHold("logout", visibleActions.indexOf("logout"));
event.accepted = true;
if (visibleActions.includes("logout")) {
startHold("logout", visibleActions.indexOf("logout"));
event.accepted = true;
}
break;
case Qt.Key_L:
startHold("lock", visibleActions.indexOf("lock"));
event.accepted = true;
if (visibleActions.includes("lock")) {
startHold("lock", visibleActions.indexOf("lock"));
event.accepted = true;
}
break;
case Qt.Key_S:
startHold("suspend", visibleActions.indexOf("suspend"));
event.accepted = true;
if (visibleActions.includes("suspend")) {
startHold("suspend", visibleActions.indexOf("suspend"));
event.accepted = true;
}
break;
case Qt.Key_H:
startHold("hibernate", visibleActions.indexOf("hibernate"));
event.accepted = true;
if (visibleActions.includes("hibernate")) {
startHold("hibernate", visibleActions.indexOf("hibernate"));
event.accepted = true;
}
break;
case Qt.Key_D:
startHold("restart", visibleActions.indexOf("restart"));
event.accepted = true;
if (visibleActions.includes("restart")) {
startHold("restart", visibleActions.indexOf("restart"));
event.accepted = true;
}
break;
}
}
@@ -445,9 +453,11 @@ DankModal {
break;
case Qt.Key_P:
if (!(event.modifiers & Qt.ControlModifier)) {
const idx = visibleActions.indexOf("poweroff");
startHold("poweroff", idx);
event.accepted = true;
if (visibleActions.includes("poweroff")) {
const idx = visibleActions.indexOf("poweroff");
startHold("poweroff", idx);
event.accepted = true;
}
} else {
selectedCol = (selectedCol - 1 + gridColumns) % gridColumns;
selectedIndex = selectedRow * gridColumns + selectedCol;
@@ -469,28 +479,40 @@ DankModal {
}
break;
case Qt.Key_R:
startHold("reboot", visibleActions.indexOf("reboot"));
event.accepted = true;
if (visibleActions.includes("reboot")) {
startHold("reboot", visibleActions.indexOf("reboot"));
event.accepted = true;
}
break;
case Qt.Key_X:
startHold("logout", visibleActions.indexOf("logout"));
event.accepted = true;
if (visibleActions.includes("logout")) {
startHold("logout", visibleActions.indexOf("logout"));
event.accepted = true;
}
break;
case Qt.Key_L:
startHold("lock", visibleActions.indexOf("lock"));
event.accepted = true;
if (visibleActions.includes("lock")) {
startHold("lock", visibleActions.indexOf("lock"));
event.accepted = true;
}
break;
case Qt.Key_S:
startHold("suspend", visibleActions.indexOf("suspend"));
event.accepted = true;
if (visibleActions.includes("suspend")) {
startHold("suspend", visibleActions.indexOf("suspend"));
event.accepted = true;
}
break;
case Qt.Key_H:
startHold("hibernate", visibleActions.indexOf("hibernate"));
event.accepted = true;
if (visibleActions.includes("hibernate")) {
startHold("hibernate", visibleActions.indexOf("hibernate"));
event.accepted = true;
}
break;
case Qt.Key_D:
startHold("restart", visibleActions.indexOf("restart"));
event.accepted = true;
if (visibleActions.includes("restart")) {
startHold("restart", visibleActions.indexOf("restart"));
event.accepted = true;
}
break;
}
}
+76 -23
View File
@@ -1,6 +1,7 @@
import QtQuick
import qs.Common
import qs.Modules.Settings
import qs.Widgets
FocusScope {
id: root
@@ -105,6 +106,61 @@ FocusScope {
}
}
Loader {
id: compositorLayoutLoader
anchors.fill: parent
active: root.currentIndex === 37
visible: active
focus: active
sourceComponent: CompositorLayoutTab {}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
Loader {
id: windowRulesLoader
property bool loadedOnce: false
anchors.fill: parent
active: root.currentIndex === 38 || loadedOnce
visible: root.currentIndex === 38 && status === Loader.Ready
focus: visible
asynchronous: true
sourceComponent: WindowRulesTab {
pageActive: root.currentIndex === 38
}
onLoaded: loadedOnce = true
}
DankSpinner {
anchors.centerIn: parent
visible: root.currentIndex === 38 && windowRulesLoader.status === Loader.Loading
}
Loader {
id: dankBarAppearanceLoader
anchors.fill: parent
active: root.currentIndex === 6
visible: active
focus: active
sourceComponent: DankBarAppearanceTab {
parentModal: root.parentModal
}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
Loader {
id: dockLoader
anchors.fill: parent
@@ -370,7 +426,7 @@ FocusScope {
}
}
Loader {
Loader {
id: defaultAppsLoader
anchors.fill: parent
active: root.currentIndex === 34
@@ -432,19 +488,33 @@ FocusScope {
Loader {
id: widgetsLoader
property bool loadedOnce: false
anchors.fill: parent
active: root.currentIndex === 22
visible: active
focus: active
active: root.currentIndex === 22 || loadedOnce
visible: root.currentIndex === 22 && status === Loader.Ready
focus: visible
asynchronous: true
sourceComponent: WidgetsTab {
parentModal: root.parentModal
}
onActiveChanged: {
if (active && item)
onLoaded: {
loadedOnce = true;
if (visible && item)
Qt.callLater(() => item.forceActiveFocus());
}
onVisibleChanged: {
if (visible && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
DankSpinner {
anchors.centerIn: parent
visible: root.currentIndex === 22 && widgetsLoader.status === Loader.Loading
}
Loader {
@@ -479,23 +549,6 @@ FocusScope {
}
}
Loader {
id: windowRulesLoader
anchors.fill: parent
active: root.currentIndex === 28
visible: active
focus: active
sourceComponent: WindowRulesTab {
parentModal: root.parentModal
}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
Loader {
id: audioLoader
anchors.fill: parent
+45 -21
View File
@@ -23,6 +23,7 @@ Rectangle {
property bool searchActive: searchField.text.length > 0
property int searchSelectedIndex: 0
property int keyboardHighlightIndex: -1
readonly property int navigationStateDuration: Theme.currentAnimationSpeed === SettingsData.AnimationSpeed.None ? 0 : Anims.settingsNavigationStateDuration
function focusSearch() {
searchField.forceActiveFocus();
@@ -101,6 +102,13 @@ Rectangle {
"icon": "volume_up",
"tabIndex": 15,
"soundsOnly": true
},
{
"id": "compositor_layout",
"text": CompositorService.isNiri ? "Niri" : (CompositorService.isHyprland ? "Hyprland" : "MangoWC"),
"icon": "layers",
"tabIndex": 37,
"layoutCapable": true
}
]
},
@@ -109,6 +117,12 @@ Rectangle {
"text": I18n.tr("Dank Bar"),
"icon": "toolbar",
"children": [
{
"id": "dankbar_appearance",
"text": I18n.tr("Appearance"),
"icon": "palette",
"tabIndex": 6
},
{
"id": "dankbar_settings",
"text": I18n.tr("Settings"),
@@ -121,6 +135,12 @@ Rectangle {
"icon": "widgets",
"tabIndex": 22
},
{
"id": "workspaces",
"text": I18n.tr("Workspaces"),
"icon": "view_module",
"tabIndex": 4
},
{
"id": "frame",
"text": I18n.tr("Frame"),
@@ -131,16 +151,10 @@ Rectangle {
},
{
"id": "workspaces_widgets",
"text": I18n.tr("Workspaces & Widgets"),
"text": I18n.tr("Widgets & Notifications"),
"icon": "dashboard",
"collapsedByDefault": true,
"children": [
{
"id": "workspaces",
"text": I18n.tr("Workspaces"),
"icon": "view_module",
"tabIndex": 4
},
{
"id": "media_player",
"text": I18n.tr("Media Player"),
@@ -252,6 +266,13 @@ Rectangle {
"icon": "line_start",
"tabIndex": 36,
"autostartOnly": true
},
{
"id": "window_rules",
"text": I18n.tr("Window Rules"),
"icon": "select_window",
"tabIndex": 38,
"windowRulesCapable": true
}
]
},
@@ -305,13 +326,6 @@ Rectangle {
"text": I18n.tr("Users"),
"icon": "manage_accounts",
"tabIndex": 35
},
{
"id": "window_rules",
"text": I18n.tr("Window Rules"),
"icon": "select_window",
"tabIndex": 28,
"windowRulesCapable": true
}
]
},
@@ -372,6 +386,8 @@ Rectangle {
return false;
if (item.windowRulesCapable && !CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango)
return false;
if (item.layoutCapable && !CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango)
return false;
if (item.niriOnly && !CompositorService.isNiri)
return false;
if (item.clipboardOnly && (!DMSService.isConnected || DMSService.apiVersion < 23))
@@ -544,6 +560,8 @@ Rectangle {
return -1;
var normalized = name.toLowerCase().replace(/[_\-\s]/g, "");
if (normalized === "compositor")
normalized = "workspaces";
for (var i = 0; i < categoryStructure.length; i++) {
var cat = categoryStructure[i];
@@ -588,7 +606,7 @@ Rectangle {
id: __m1
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
text: I18n.tr("Workspaces & Widgets")
text: I18n.tr("Widgets & Notifications")
}
StyledTextMetrics {
id: __m2
@@ -782,6 +800,7 @@ Rectangle {
id: resultRipple
rippleColor: root.searchSelectedIndex === resultDelegate.index ? Theme.buttonText : Theme.surfaceText
cornerRadius: resultDelegate.radius
animationDuration: Anims.settingsNavigationRippleDuration
}
Row {
@@ -837,8 +856,9 @@ Rectangle {
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
duration: root.navigationStateDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.expressiveEffects
}
}
}
@@ -912,6 +932,7 @@ Rectangle {
id: categoryRipple
rippleColor: categoryRow.isActive ? Theme.buttonText : Theme.surfaceText
cornerRadius: categoryRow.radius
animationDuration: Anims.settingsNavigationRippleDuration
}
Row {
@@ -967,8 +988,9 @@ Rectangle {
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
duration: root.navigationStateDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.expressiveEffects
}
}
}
@@ -1009,6 +1031,7 @@ Rectangle {
id: childRipple
rippleColor: childDelegate.isActive ? Theme.buttonText : Theme.surfaceText
cornerRadius: childDelegate.radius
animationDuration: Anims.settingsNavigationRippleDuration
}
Row {
@@ -1049,8 +1072,9 @@ Rectangle {
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
duration: root.navigationStateDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.expressiveEffects
}
}
}
@@ -7,6 +7,7 @@ import qs.Widgets
import qs.Services
Variants {
readonly property var log: Log.scoped("BlurredWallpaperBackground")
model: {
if (SessionData.isGreeterMode) {
return Quickshell.screens;
@@ -32,6 +33,8 @@ Variants {
color: "transparent"
updatesEnabled: root.renderActive || root._settleFrames > 0
mask: Region {
item: Item {}
}
@@ -85,7 +88,6 @@ Variants {
}
Component.onCompleted: {
blurWallpaperWindow.updatesEnabled = Qt.binding(() => !root.source || root.effectActive || root._renderSettling || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading);
isInitialized = true;
}
@@ -93,51 +95,67 @@ Variants {
property real transitionProgress: 0
readonly property bool transitioning: transitionAnimation.running
property bool effectActive: false
property bool _renderSettling: true
property bool useNextForEffect: false
readonly property var backingWindow: Window.window
readonly property bool renderActive: !source || effectActive || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading
property int _settleFrames: 3
Connections {
target: currentWallpaper
function onStatusChanged() {
if (currentWallpaper.status !== Image.Ready && currentWallpaper.status !== Image.Error)
return;
root._renderSettling = true;
renderSettleTimer.restart();
}
function invalidate() {
_settleFrames = 3;
backingWindow?.update();
}
onRenderActiveChanged: invalidate()
onBackingWindowChanged: invalidate()
Connections {
target: blurWallpaperWindow
target: root.backingWindow
function onFrameSwapped() {
if (root._settleFrames > 0)
root._settleFrames--;
}
function onVisibleChanged() {
root.invalidate();
}
function onWidthChanged() {
root._renderSettling = true;
renderSettleTimer.restart();
root.invalidate();
}
function onHeightChanged() {
root._renderSettling = true;
renderSettleTimer.restart();
root.invalidate();
}
}
Connections {
target: Quickshell
function onScreensChanged() {
root._renderSettling = true;
renderSettleTimer.restart();
root.invalidate();
}
}
Connections {
target: SettingsData
function onWallpaperFillModeChanged() {
root._renderSettling = true;
renderSettleTimer.restart();
root.invalidate();
}
}
Timer {
id: renderSettleTimer
interval: 1000
onTriggered: root._renderSettling = false
Connections {
target: IdleService
function onIsShellLockedChanged() {
if (IdleService.isShellLocked)
return;
root.invalidate();
}
}
function handleTransitionLoadError(failedSource) {
log.warn("failed to load candidate wallpaper for", modelData.name + ":", failedSource);
transitionDelayTimer.stop();
transitionAnimation.stop();
root.useNextForEffect = false;
root.effectActive = false;
root.transitionProgress = 0.0;
nextWallpaper.source = "";
}
onSourceChanged: {
@@ -164,8 +182,6 @@ Variants {
transitionAnimation.stop();
root.transitionProgress = 0.0;
root.effectActive = false;
root._renderSettling = true;
renderSettleTimer.restart();
currentWallpaper.source = newSource;
nextWallpaper.source = "";
}
@@ -194,8 +210,6 @@ Variants {
transitionAnimation.stop();
root.transitionProgress = 0;
root.effectActive = false;
root._renderSettling = true;
renderSettleTimer.restart();
currentWallpaper.source = nextWallpaper.source;
nextWallpaper.source = "";
}
@@ -204,9 +218,6 @@ Variants {
return;
}
root._renderSettling = true;
renderSettleTimer.restart();
nextWallpaper.source = newPath;
if (nextWallpaper.status === Image.Ready)
@@ -215,7 +226,7 @@ Variants {
Loader {
anchors.fill: parent
active: !root.source || root.isColorSource
active: !root.source || root.isColorSource || currentWallpaper.status === Image.Error
asynchronous: true
sourceComponent: DankBackdrop {
@@ -238,6 +249,12 @@ Variants {
cache: true
sourceSize: Qt.size(root.textureWidth, root.textureHeight)
fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SessionData.getMonitorWallpaperFillMode(modelData.name))
onStatusChanged: {
if (status === Image.Error) {
log.warn("failed to load active wallpaper for", modelData.name + ":", source);
}
}
}
Image {
@@ -253,6 +270,10 @@ Variants {
fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SessionData.getMonitorWallpaperFillMode(modelData.name))
onStatusChanged: {
if (status === Image.Error) {
root.handleTransitionLoadError(source);
return;
}
if (status !== Image.Ready)
return;
if (!root.transitioning) {
@@ -329,8 +350,6 @@ Variants {
root.useNextForEffect = false;
nextWallpaper.source = "";
root.transitionProgress = 0.0;
root._renderSettling = true;
renderSettleTimer.restart();
root.effectActive = false;
}
}
@@ -12,7 +12,7 @@ PluginComponent {
service: CupsService
}
ccWidgetIcon: CupsService.cupsAvailable && CupsService.getPrintersNum() > 0 ? "print" : "print_disabled"
ccWidgetIcon: "print"
ccWidgetPrimaryText: I18n.tr("Printers")
ccWidgetSecondaryText: {
if (CupsService.cupsAvailable && CupsService.getPrintersNum() > 0) {
@@ -11,7 +11,7 @@ PluginComponent {
service: DMSNetworkService
}
ccWidgetIcon: DMSNetworkService.isBusy ? "sync" : (DMSNetworkService.connected ? "vpn_lock" : "vpn_key_off")
ccWidgetIcon: "vpn_key"
ccWidgetPrimaryText: I18n.tr("VPN")
ccWidgetSecondaryText: {
if (!DMSNetworkService.connected)
@@ -102,6 +102,120 @@ Column {
item.z = 1000;
}
function getCompoundPillIconBlinking(id) {
if (id === "wifi") return NetworkService.isWifiConnecting;
if (id === "bluetooth") return BluetoothService.connecting;
return false;
}
function getCompoundPillIconName(id, widgetDef) {
switch (id) {
case "wifi": {
if (NetworkService.wifiToggling) return "sync";
if (NetworkService.isConnecting && !NetworkService.ethernetConnected) return NetworkService.wifiSignalIcon;
const status = NetworkService.networkStatus;
if (status === "ethernet") return "settings_ethernet";
if (status === "vpn") return NetworkService.ethernetConnected ? "settings_ethernet" : NetworkService.wifiSignalIcon;
if (status === "wifi") return NetworkService.wifiSignalIcon;
return "wifi";
}
case "bluetooth": {
return "bluetooth";
}
case "audioOutput": {
if (!AudioService.sink?.audio) return "volume_off";
let volume = AudioService.sink.audio.volume;
let muted = AudioService.sink.audio.muted;
if (muted) return "volume_off";
if (volume === 0.0) return "volume_mute";
if (volume <= 0.33) return "volume_down";
if (volume <= 0.66) return "volume_up";
return "volume_up";
}
case "audioInput": {
if (!AudioService.source?.audio) return "mic_off";
return AudioService.source.audio.muted ? "mic_off" : "mic";
}
default:
return widgetDef?.icon || "help";
}
}
function getCompoundPillIsActive(id) {
switch (id) {
case "wifi": {
if (NetworkService.wifiToggling) return false;
const status = NetworkService.networkStatus;
if (status === "ethernet") return true;
if (status === "vpn") return NetworkService.ethernetConnected || NetworkService.wifiConnected;
if (status === "wifi") return true;
return NetworkService.wifiEnabled;
}
case "bluetooth":
return !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled);
case "audioOutput":
return !!(AudioService.sink?.audio && !AudioService.sink.audio.muted);
case "audioInput":
return !!(AudioService.source?.audio && !AudioService.source.audio.muted);
default:
return false;
}
}
function handleCompoundPillToggled(id) {
switch (id) {
case "wifi": {
if (NetworkService.networkStatus !== "ethernet" && !NetworkService.wifiToggling) {
NetworkService.toggleWifiRadio();
}
break;
}
case "bluetooth": {
if (BluetoothService.available && BluetoothService.adapter) {
BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled;
}
break;
}
case "audioOutput": {
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.muted = !AudioService.sink.audio.muted;
}
break;
}
case "audioInput": {
if (AudioService.source && AudioService.source.audio) {
AudioService.source.audio.muted = !AudioService.source.audio.muted;
}
break;
}
}
}
function handleCompoundPillWheelEvent(id, wheelEvent) {
if (id === "audioOutput") {
if (!AudioService.sink || !AudioService.sink.audio) return;
let delta = wheelEvent.angleDelta.y;
let maxVol = AudioService.sinkMaxVolume;
let currentVolume = AudioService.sink.audio.volume * 100;
let newVolume;
if (delta > 0) newVolume = Math.min(maxVol, currentVolume + 5);
else newVolume = Math.max(0, currentVolume - 5);
AudioService.sink.audio.muted = false;
AudioService.sink.audio.volume = newVolume / 100;
wheelEvent.accepted = true;
} else if (id === "audioInput") {
if (!AudioService.source || !AudioService.source.audio) return;
let delta = wheelEvent.angleDelta.y;
let currentVolume = AudioService.source.audio.volume * 100;
let newVolume;
if (delta > 0) newVolume = Math.min(100, currentVolume + 5);
else newVolume = Math.max(0, currentVolume - 5);
AudioService.source.audio.muted = false;
AudioService.source.audio.volume = newVolume / 100;
wheelEvent.accepted = true;
}
}
function componentForWidget(widgetData) {
const id = widgetData.id || "";
const widgetWidth = widgetData.width || 50;
@@ -114,7 +228,7 @@ Column {
case "bluetooth":
case "audioOutput":
case "audioInput":
return compoundPillComponent;
return widgetWidth <= 25 ? smallCompoundComponent : compoundPillComponent;
case "volumeSlider":
return audioSliderComponent;
case "brightnessSlider":
@@ -126,7 +240,7 @@ Column {
case "diskUsage":
return widgetWidth <= 25 ? smallDiskUsageComponent : diskUsagePillComponent;
case "colorPicker":
return colorPickerPillComponent;
return widgetWidth <= 25 ? smallColorPickerComponent : colorPickerPillComponent;
case "doNotDisturb":
return widgetWidth <= 25 ? smallToggleComponent : dndPillComponent;
default:
@@ -329,69 +443,8 @@ Column {
property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
width: parent.width
height: 60
iconBlinking: {
const id = widgetData.id || "";
if (id === "wifi")
return NetworkService.isWifiConnecting;
if (id === "bluetooth")
return BluetoothService.connecting;
return false;
}
iconName: {
switch (widgetData.id || "") {
case "wifi":
{
if (NetworkService.wifiToggling)
return "sync";
if (NetworkService.isConnecting && !NetworkService.ethernetConnected)
return NetworkService.wifiSignalIcon;
const status = NetworkService.networkStatus;
if (status === "ethernet")
return "settings_ethernet";
if (status === "vpn")
return NetworkService.ethernetConnected ? "settings_ethernet" : NetworkService.wifiSignalIcon;
if (status === "wifi")
return NetworkService.wifiSignalIcon;
if (NetworkService.wifiEnabled)
return "wifi_off";
return "wifi_off";
}
case "bluetooth":
{
if (!BluetoothService.available)
return "bluetooth_disabled";
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled)
return "bluetooth_disabled";
return "bluetooth";
}
case "audioOutput":
{
if (!AudioService.sink?.audio)
return "volume_off";
let volume = AudioService.sink.audio.volume;
let muted = AudioService.sink.audio.muted;
if (muted)
return "volume_off";
if (volume === 0.0)
return "volume_mute";
if (volume <= 0.33)
return "volume_down";
if (volume <= 0.66)
return "volume_up";
return "volume_up";
}
case "audioInput":
{
if (!AudioService.source?.audio)
return "mic_off";
let muted = AudioService.source.audio.muted;
return muted ? "mic_off" : "mic";
}
default:
return widgetDef?.icon || "help";
}
}
iconBlinking: root.getCompoundPillIconBlinking(widgetData.id || "")
iconName: root.getCompoundPillIconName(widgetData.id || "", widgetDef)
primaryText: {
switch (widgetData.id || "") {
case "wifi":
@@ -506,66 +559,12 @@ Column {
return widgetDef?.description || "";
}
}
isActive: {
switch (widgetData.id || "") {
case "wifi":
{
if (NetworkService.wifiToggling)
return false;
const status = NetworkService.networkStatus;
if (status === "ethernet")
return true;
if (status === "vpn")
return NetworkService.ethernetConnected || NetworkService.wifiConnected;
if (status === "wifi")
return true;
return NetworkService.wifiEnabled;
}
case "bluetooth":
return !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled);
case "audioOutput":
return !!(AudioService.sink?.audio && !AudioService.sink.audio.muted);
case "audioInput":
return !!(AudioService.source?.audio && !AudioService.source.audio.muted);
default:
return false;
}
}
isActive: root.getCompoundPillIsActive(widgetData.id || "")
enabled: widgetDef?.enabled ?? true
onToggled: {
if (root.editMode)
return;
switch (widgetData.id || "") {
case "wifi":
{
if (NetworkService.networkStatus !== "ethernet" && !NetworkService.wifiToggling) {
NetworkService.toggleWifiRadio();
}
break;
}
case "bluetooth":
{
if (BluetoothService.available && BluetoothService.adapter) {
BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled;
}
break;
}
case "audioOutput":
{
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.muted = !AudioService.sink.audio.muted;
}
break;
}
case "audioInput":
{
if (AudioService.source && AudioService.source.audio) {
AudioService.source.audio.muted = !AudioService.source.audio.muted;
}
break;
}
}
root.handleCompoundPillToggled(widgetData.id || "");
}
onExpandClicked: {
if (root.editMode)
@@ -575,35 +574,7 @@ Column {
onWheelEvent: function (wheelEvent) {
if (root.editMode)
return;
const id = widgetData.id || "";
if (id === "audioOutput") {
if (!AudioService.sink || !AudioService.sink.audio)
return;
let delta = wheelEvent.angleDelta.y;
let maxVol = AudioService.sinkMaxVolume;
let currentVolume = AudioService.sink.audio.volume * 100;
let newVolume;
if (delta > 0)
newVolume = Math.min(maxVol, currentVolume + 5);
else
newVolume = Math.max(0, currentVolume - 5);
AudioService.sink.audio.muted = false;
AudioService.sink.audio.volume = newVolume / 100;
wheelEvent.accepted = true;
} else if (id === "audioInput") {
if (!AudioService.source || !AudioService.source.audio)
return;
let delta = wheelEvent.angleDelta.y;
let currentVolume = AudioService.source.audio.volume * 100;
let newVolume;
if (delta > 0)
newVolume = Math.min(100, currentVolume + 5);
else
newVolume = Math.max(0, currentVolume - 5);
AudioService.source.audio.muted = false;
AudioService.source.audio.volume = newVolume / 100;
wheelEvent.accepted = true;
}
root.handleCompoundPillWheelEvent(widgetData.id || "", wheelEvent);
}
}
}
@@ -736,7 +707,7 @@ Column {
case "darkMode":
return "contrast";
case "idleInhibitor":
return SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle";
return "motion_sensor_active";
default:
return "help";
}
@@ -821,9 +792,9 @@ Column {
case "darkMode":
return "contrast";
case "doNotDisturb":
return SessionData.doNotDisturb ? "do_not_disturb_on" : "do_not_disturb_off";
return "do_not_disturb_on";
case "idleInhibitor":
return SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle";
return "motion_sensor_active";
default:
return "help";
}
@@ -1223,4 +1194,47 @@ Column {
}
}
}
Component {
id: smallCompoundComponent
SmallCompoundButton {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
width: parent.width
height: 48
iconBlinking: root.getCompoundPillIconBlinking(widgetData.id || "")
iconName: root.getCompoundPillIconName(widgetData.id || "", widgetDef)
isActive: root.getCompoundPillIsActive(widgetData.id || "")
enabled: (widgetDef?.enabled ?? true) && !root.editMode
onToggled: {
if (root.editMode) return;
root.handleCompoundPillToggled(widgetData.id || "");
}
onExpandClicked: {
if (root.editMode) return;
root.expandClicked(widgetData, widgetIndex);
}
onWheelEvent: function(wheelEvent) {
if (root.editMode) return;
root.handleCompoundPillWheelEvent(widgetData.id || "", wheelEvent);
}
}
}
Component {
id: smallColorPickerComponent
SmallColorPickerButton {
property var widgetData: parent.widgetData || {}
property int widgetIndex: parent.widgetIndex || 0
width: parent.width
height: 48
colorPickerModal: root.colorPickerModal
onClicked: {
if (root.editMode) return;
if (root.colorPickerModal)
root.colorPickerModal.show();
}
}
}
}
@@ -60,7 +60,7 @@ Rectangle {
}
Typography {
text: DgopService.uptime ? I18n.tr("up") + " " + DgopService.uptime.slice(3) : I18n.tr("Unknown")
text: DgopService.uptime ? I18n.tr("up", "uptime prefix, e.g. 'up 4h 2m'") + " " + DgopService.uptime.slice(3) : I18n.tr("Unknown")
style: Typography.Style.Caption
color: Theme.surfaceVariantText
}
@@ -109,15 +109,7 @@ DankPopout {
close();
}
customKeyboardFocus: {
if (!shouldBeVisible)
return WlrKeyboardFocus.None;
if (anyModalOpen)
return WlrKeyboardFocus.None;
if (CompositorService.useHyprlandFocusGrab)
return WlrKeyboardFocus.OnDemand;
return WlrKeyboardFocus.Exclusive;
}
customKeyboardFocus: anyModalOpen ? WlrKeyboardFocus.None : null
onBackgroundClicked: close()
@@ -5,7 +5,7 @@ import qs.Modules.ControlCenter.Widgets
CompoundPill {
id: root
iconName: SessionData.doNotDisturb ? "do_not_disturb_on" : "do_not_disturb_off"
iconName: "do_not_disturb_on"
iconColor: SessionData.doNotDisturb ? Theme.primary : Theme.surfaceText
primaryText: I18n.tr("Do Not Disturb")
isActive: SessionData.doNotDisturb
@@ -0,0 +1,64 @@
import QtQuick
import qs.Common
import qs.Widgets
Rectangle {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property var colorPickerModal: null
signal clicked
width: parent ? ((parent.width - parent.spacing * 3) / 4) : 48
height: 48
radius: Theme.cornerRadius === 0 ? 0 : Theme.cornerRadius
function hoverTint(base) {
const factor = 1.2;
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
}
color: Theme.primary
border.color: Theme.ccTileRing
border.width: 1
antialiasing: true
Rectangle {
anchors.fill: parent
radius: parent.radius
color: hoverTint(root.color)
opacity: mouseArea.pressed ? 0.3 : (mouseArea.containsMouse ? 0.2 : 0.0)
visible: opacity > 0
antialiasing: true
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
}
}
}
DankIcon {
anchors.centerIn: parent
name: "palette"
size: Theme.iconSize
color: Theme.primaryText
}
DankRipple {
id: ripple
cornerRadius: root.radius
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: root.enabled
onPressed: mouse => ripple.trigger(mouse.x, mouse.y)
onClicked: root.clicked()
}
}
@@ -0,0 +1,107 @@
import QtQuick
import qs.Common
import qs.Widgets
Rectangle {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property string iconName: ""
property bool isActive: false
property bool iconBlinking: false
// Left click expands the widget (primary detail action), right click toggles on/off.
signal toggled
signal expandClicked
signal wheelEvent(var wheelEvent)
width: parent ? ((parent.width - parent.spacing * 3) / 4) : 48
height: 48
radius: {
if (Theme.cornerRadius === 0)
return 0;
return isActive ? Theme.cornerRadius : Theme.cornerRadius + 4;
}
function hoverTint(base) {
const factor = 1.2;
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor);
}
readonly property color _tileBgActive: Theme.ccTileActiveBg
readonly property color _tileBgInactive: Theme.ccPillInactiveBg
readonly property color _tileRingActive: Theme.ccTileRing
readonly property color _tileIconActive: Theme.ccTileActiveText
readonly property color _tileIconInactive: Theme.ccTileInactiveIcon
color: {
if (isActive)
return _tileBgActive;
const baseColor = mouseArea.containsMouse ? Theme.ccPillInactiveHoverBg : _tileBgInactive;
return baseColor;
}
border.color: isActive ? _tileRingActive : Theme.outlineMedium
border.width: isActive ? 1 : Theme.layerOutlineWidth
antialiasing: true
opacity: enabled ? 1.0 : 0.6
Rectangle {
anchors.fill: parent
radius: parent.radius
color: hoverTint(root.color)
opacity: mouseArea.pressed ? 0.3 : (mouseArea.containsMouse ? 0.2 : 0.0)
visible: opacity > 0
antialiasing: true
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
}
}
}
DankIcon {
id: tileIcon
anchors.centerIn: parent
name: iconName
size: Theme.iconSize
color: isActive ? _tileIconActive : _tileIconInactive
DankBlink {
target: tileIcon
running: root.iconBlinking
}
}
DankRipple {
id: ripple
cornerRadius: root.radius
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
enabled: root.enabled
onPressed: mouse => ripple.trigger(mouse.x, mouse.y)
onClicked: mouse => {
if (mouse.button === Qt.RightButton)
root.toggled();
else
root.expandClicked();
}
onWheel: function (ev) {
root.wheelEvent(ev);
}
}
Behavior on radius {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
+1 -2
View File
@@ -61,7 +61,7 @@ Item {
// M3 elevation shadow Level 2 baseline (navigation bar), with per-bar override support
readonly property bool hasPerBarOverride: (barConfig?.shadowIntensity ?? 0) > 0
readonly property var elevLevel: Theme.elevationLevel2
readonly property bool shadowEnabled: !BlurService.enabled && ((Theme.elevationEnabled && (typeof SettingsData !== "undefined" ? (SettingsData.barElevationEnabled ?? true) : false)) || hasPerBarOverride)
readonly property bool shadowEnabled: (Theme.elevationEnabled && (typeof SettingsData !== "undefined" ? (SettingsData.barElevationEnabled ?? true) : false)) || hasPerBarOverride
readonly property string autoBarShadowDirection: isTop ? "top" : (isBottom ? "bottom" : (isLeft ? "left" : (isRight ? "right" : "top")))
readonly property string globalShadowDirection: Theme.elevationLightDirection === "autoBar" ? autoBarShadowDirection : Theme.elevationLightDirection
readonly property string perBarShadowDirectionMode: barConfig?.shadowDirectionMode ?? "inherit"
@@ -207,7 +207,6 @@ Item {
shadowOffsetX: root.shadowOffsetX
shadowOffsetY: root.shadowOffsetY
shadowColor: root.shadowColor
blurMax: Theme.elevationBlurMax
}
Loader {
-4
View File
@@ -108,8 +108,6 @@ Item {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
focusedScreenName = focusedWs?.monitor?.name || "";
} else if (CompositorService.isDwl && DwlService.activeOutput) {
focusedScreenName = DwlService.activeOutput;
} else if (CompositorService.isMango && MangoService.activeOutput) {
focusedScreenName = MangoService.activeOutput;
}
@@ -139,8 +137,6 @@ Item {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
focusedScreenName = focusedWs?.monitor?.name || "";
} else if (CompositorService.isDwl && DwlService.activeOutput) {
focusedScreenName = DwlService.activeOutput;
} else if (CompositorService.isMango && MangoService.activeOutput) {
focusedScreenName = MangoService.activeOutput;
}
File diff suppressed because it is too large Load Diff
+68 -16
View File
@@ -9,6 +9,8 @@ PanelWindow {
id: barWindow
readonly property var log: Log.scoped("DankBarWindow")
Component.onDestruction: KeyboardFocus.unregisterBarWindow(barWindow)
required property var rootWindow
required property var barConfig
property var modelData: item
@@ -18,6 +20,8 @@ PanelWindow {
property var centerWidgetsModel
property var rightWidgetsModel
readonly property bool barRevealed: inputMask.showing
property var controlCenterButtonRef: null
property var clockButtonRef: null
property var systemUpdateButtonRef: null
@@ -282,9 +286,6 @@ PanelWindow {
readonly property bool isVertical: axis.isVertical
property bool gothCornersEnabled: barConfig?.gothCornersEnabled ?? false
property real wingtipsRadius: barConfig?.gothCornerRadiusOverride ? (barConfig?.gothCornerRadiusValue ?? 12) : Theme.cornerRadius
readonly property real _wingR: Math.max(0, wingtipsRadius)
readonly property color _surfaceContainer: Theme.surfaceContainer
readonly property string _barId: barConfig?.id ?? "default"
property real _backgroundAlpha: barConfig?.transparency ?? 1.0
@@ -296,25 +297,30 @@ PanelWindow {
}
readonly property real _dpr: CompositorService.getScreenScale(barWindow.screen)
property string screenName: modelData.name
readonly property bool usesConnectedFrameChrome: CompositorService.usesConnectedFrameChromeForScreen(screenName)
readonly property bool usesFrameBarChrome: CompositorService.frameWindowVisibleForScreen(screenName)
readonly property var renderBarConfig: SettingsData.effectiveBarConfigForRender(barConfig, usesFrameBarChrome)
property bool gothCornersEnabled: renderBarConfig?.gothCornersEnabled ?? false
property real wingtipsRadius: renderBarConfig?.gothCornerRadiusOverride ? (renderBarConfig?.gothCornerRadiusValue ?? 12) : Theme.cornerRadius
readonly property real _wingR: Math.max(0, wingtipsRadius)
// Shadow buffer: extra window space for shadow to render beyond bar bounds
readonly property bool _shadowActive: (Theme.elevationEnabled && (typeof SettingsData !== "undefined" ? (SettingsData.barElevationEnabled ?? true) : false)) || (barConfig?.shadowIntensity ?? 0) > 0
readonly property bool _shadowActive: (Theme.elevationEnabled && (typeof SettingsData !== "undefined" ? (SettingsData.barElevationEnabled ?? true) : false)) || (renderBarConfig?.shadowIntensity ?? 0) > 0
readonly property real _shadowBuffer: {
if (!_shadowActive)
return 0;
const hasOverride = (barConfig?.shadowIntensity ?? 0) > 0;
const hasOverride = (renderBarConfig?.shadowIntensity ?? 0) > 0;
if (hasOverride) {
const blur = (barConfig.shadowIntensity ?? 0) * 0.2;
const blur = (renderBarConfig.shadowIntensity ?? 0) * 0.2;
const offset = blur * 0.5;
return Theme.snap(Math.max(16, blur + offset + 8), _dpr);
}
return Theme.snap(Theme.elevationRenderPadding(Theme.elevationLevel2, "top", 4, 8, 16), _dpr);
}
property string screenName: modelData.name
readonly property bool usesConnectedFrameChrome: CompositorService.usesConnectedFrameChromeForScreen(screenName)
readonly property bool usesFrameBarChrome: CompositorService.frameWindowVisibleForScreen(screenName)
// Flatten/spacing collapse for maximized windows is only for frame-integrated layout.
// When the bar draws its own pill, keep rounded corners and spacing like the dock.
readonly property bool flattenForMaximizedWindow: !SettingsData.frameEnabled || usesFrameBarChrome
@@ -550,11 +556,12 @@ PanelWindow {
}
screen: modelData
implicitHeight: !isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((barConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0
implicitWidth: isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((barConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0
implicitHeight: !isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((renderBarConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0
implicitWidth: isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((renderBarConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0
color: "transparent"
Component.onCompleted: {
KeyboardFocus.registerBarWindow(barWindow);
updateGpuTempConfig();
_updateBackgroundAlpha();
_updateHasMaximizedToplevel();
@@ -702,6 +709,14 @@ PanelWindow {
readonly property var _rightSection: topBarContent ? (barWindow.isVertical ? topBarContent.vRightSection : topBarContent.hRightSection) : null
readonly property real _revealProgress: topBarSlide.x + topBarSlide.y
function containsGlobalPoint(gx, gy, padding) {
const pad = padding !== undefined ? padding : 16;
if (!inputMask.showing)
return false;
const topLeft = inputMask.mapToItem(null, 0, 0);
return gx >= topLeft.x - pad && gx < topLeft.x + inputMask.width + pad && gy >= topLeft.y - pad && gy < topLeft.y + inputMask.height + pad;
}
function sectionRect(section, isCenter, _dep) {
if (!section)
return {
@@ -947,7 +962,7 @@ PanelWindow {
id: barBackground
barWindow: barWindow
axis: axis
barConfig: barWindow.barConfig
barConfig: barWindow.renderBarConfig
}
MouseArea {
@@ -956,8 +971,13 @@ PanelWindow {
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: {
const screenName = barWindow.screen?.name;
if (screenName && PopoutManager.currentPopoutsByScreen[screenName])
if (!screenName)
return;
if (PopoutManager.currentPopoutsByScreen[screenName])
PopoutManager.closeAllPopouts();
if (ModalManager.currentModalsByScreen[screenName])
ModalManager.closeAllModalsExcept(null);
TrayMenuManager.closeAllMenus();
}
}
@@ -998,7 +1018,7 @@ PanelWindow {
}
}
onWheel: wheel => {
function processWheel(wheel) {
if (!(barConfig?.scrollEnabled ?? true) || actionInProgress) {
wheel.accepted = false;
return;
@@ -1067,6 +1087,8 @@ PanelWindow {
wheel.accepted = false;
}
onWheel: wheel => processWheel(wheel)
}
DankBarContent {
@@ -1078,6 +1100,36 @@ PanelWindow {
centerWidgetsModel: barWindow.centerWidgetsModel
rightWidgetsModel: barWindow.rightWidgetsModel
}
MouseArea {
id: hoverPopoutArea
anchors.fill: parent
z: 1
hoverEnabled: barConfig?.hoverPopouts ?? false
enabled: hoverPopoutArea.hoverEnabled && !barWindow.clickThroughEnabled
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
property real lastGlobalX: 0
property real lastGlobalY: 0
onPositionChanged: mouse => {
const gp = mapToItem(null, mouse.x, mouse.y);
lastGlobalX = gp.x;
lastGlobalY = gp.y;
topBarContent.checkHoverPopout(gp.x, gp.y);
}
onWheel: wheel => scrollArea.processWheel(wheel)
onContainsMouseChanged: {
if (containsMouse)
return;
if (topBarContent.cursorOverHoverChain(lastGlobalX, lastGlobalY))
return;
topBarContent.closeHoverSurfaces();
}
}
}
}
}
@@ -10,9 +10,7 @@ DankPopout {
property var triggerScreen: null
// mango shares dwl's layout model; route to the right service.
readonly property bool isDwlLike: CompositorService.isDwl || CompositorService.isMango
readonly property var dwlSvc: CompositorService.isMango ? MangoService : DwlService
readonly property bool isMango: CompositorService.isMango
function setTriggerPosition(x, y, width, section, screen, barPosition, barThickness, barSpacing, barConfig) {
triggerX = x;
@@ -37,8 +35,8 @@ DankPopout {
onScreenChanged: updateOutputState()
function updateOutputState() {
if (screen && root.dwlSvc.available) {
outputState = root.dwlSvc.getOutputState(screen.name);
if (screen && MangoService.available) {
outputState = MangoService.getOutputState(screen.name);
} else {
outputState = null;
}
@@ -84,7 +82,7 @@ DankPopout {
}
Connections {
target: DwlService
target: MangoService
function onStateChanged() {
updateOutputState();
}
@@ -219,7 +217,7 @@ DankPopout {
spacing: Theme.spacingS
Repeater {
model: root.dwlSvc.layouts
model: MangoService.layouts
delegate: Rectangle {
required property string modelData
@@ -273,11 +271,11 @@ DankPopout {
if (!root.triggerScreen) {
return;
}
if (!root.dwlSvc.available) {
if (!MangoService.available) {
return;
}
root.dwlSvc.setLayout(root.triggerScreen.name, index);
MangoService.setLayout(root.triggerScreen.name, index);
root.close();
}
}
@@ -38,15 +38,7 @@ DankPopout {
backgroundInteractive: !anyModalOpen
customKeyboardFocus: {
if (!shouldBeVisible)
return WlrKeyboardFocus.None;
if (anyModalOpen)
return WlrKeyboardFocus.None;
if (CompositorService.useHyprlandFocusGrab)
return WlrKeyboardFocus.OnDemand;
return WlrKeyboardFocus.Exclusive;
}
customKeyboardFocus: anyModalOpen ? WlrKeyboardFocus.None : null
Connections {
target: SystemUpdateService
+1 -1
View File
@@ -282,7 +282,7 @@ Loader {
"cpuTemp": dgopAvailable,
"gpuTemp": dgopAvailable,
"network_speed_monitor": dgopAvailable,
"layout": (CompositorService.isDwl && DwlService.dwlAvailable) || (CompositorService.isMango && MangoService.available)
"layout": CompositorService.isMango && MangoService.available
};
return widgetVisibility[widgetId] ?? true;
@@ -13,12 +13,11 @@ BasePill {
signal toggleLayoutPopup
// mango shares dwl's tag/layout model; route to the right service.
readonly property bool isDwlLike: CompositorService.isDwl || CompositorService.isMango
readonly property var dwlSvc: CompositorService.isMango ? MangoService : DwlService
readonly property bool isMango: CompositorService.isMango
visible: layout.isDwlLike && layout.dwlSvc.available
visible: layout.isMango && MangoService.available
property var outputState: parentScreen ? layout.dwlSvc.getOutputState(parentScreen.name) : null
property var outputState: parentScreen ? MangoService.getOutputState(parentScreen.name) : null
property string currentLayoutSymbol: outputState?.layoutSymbol || ""
property int currentLayoutIndex: outputState?.layout || 0
@@ -41,9 +40,9 @@ BasePill {
}
Connections {
target: layout.dwlSvc
target: MangoService
function onStateChanged() {
outputState = parentScreen ? layout.dwlSvc.getOutputState(parentScreen.name) : null;
outputState = parentScreen ? MangoService.getOutputState(parentScreen.name) : null;
}
}
@@ -101,13 +100,13 @@ BasePill {
}
onRightClicked: {
if (!parentScreen || !layout.dwlSvc.available || layout.dwlSvc.layouts.length === 0) {
if (!parentScreen || !MangoService.available || MangoService.layouts.length === 0) {
return;
}
const currentIndex = layout.currentLayoutIndex;
const nextIndex = (currentIndex + 1) % layout.dwlSvc.layouts.length;
const nextIndex = (currentIndex + 1) % MangoService.layouts.length;
layout.dwlSvc.setLayout(parentScreen.name, nextIndex);
MangoService.setLayout(parentScreen.name, nextIndex);
}
}
@@ -112,8 +112,6 @@ BasePill {
property string currentLayout: {
if (CompositorService.isNiri) {
return NiriService.getCurrentKeyboardLayoutName();
} else if (CompositorService.isDwl) {
return DwlService.currentKeyboardLayout;
} else if (CompositorService.isMango) {
return MangoService.currentKeyboardLayout;
}
@@ -209,8 +207,6 @@ BasePill {
NiriService.cycleKeyboardLayout();
} else if (CompositorService.isHyprland) {
Quickshell.execDetached(["hyprctl", "switchxkblayout", root.hyprlandKeyboard, "next"]);
} else if (CompositorService.isDwl) {
Quickshell.execDetached(["mmsg", "dispatch", "switch_keyboard_layout"]);
} else if (CompositorService.isMango) {
MangoService.cycleKeyboardLayout();
}
@@ -55,7 +55,7 @@ BasePill {
}
IconImage {
visible: SettingsData.launcherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isLabwc)
visible: SettingsData.launcherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isLabwc)
anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
@@ -66,8 +66,6 @@ BasePill {
return "file://" + Theme.shellDir + "/assets/niri.svg";
} else if (CompositorService.isHyprland) {
return "file://" + Theme.shellDir + "/assets/hyprland.svg";
} else if (CompositorService.isDwl) {
return "file://" + Theme.shellDir + "/assets/mango.png";
} else if (CompositorService.isMango) {
return "file://" + Theme.shellDir + "/assets/mango.png";
} else if (CompositorService.isSway) {
@@ -980,21 +980,13 @@ BasePill {
screen: root.parentScreen
WlrLayershell.layer: root.barUsesOverlayLayer ? WlrLayershell.Overlay : WlrLayershell.Top
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: {
if (PopoutManager.screenshotActive)
return WlrKeyboardFocus.None;
if (!root.menuOpen)
return WlrKeyboardFocus.None;
if (CompositorService.useHyprlandFocusGrab)
return WlrKeyboardFocus.OnDemand;
return WlrKeyboardFocus.Exclusive;
}
WlrLayershell.keyboardFocus: KeyboardFocus.keyboardFocus(root.menuOpen, null)
WlrLayershell.namespace: "dms:tray-overflow-menu"
color: "transparent"
HyprlandFocusGrab {
windows: [overflowMenu]
active: CompositorService.useHyprlandFocusGrab && root.useOverflowPopup && root.menuOpen
windows: [overflowMenu].concat(KeyboardFocus.barWindows)
active: root.useOverflowPopup && KeyboardFocus.wantsGrab(root.menuOpen, null)
}
Connections {
@@ -1051,32 +1043,21 @@ BasePill {
"leftBar": 0,
"rightBar": 0
})
readonly property real effectiveBarSize: root.barThickness + root.barSpacing
readonly property real maskX: _overflowDismissZone.x
readonly property real maskY: _overflowDismissZone.y
readonly property real maskWidth: _overflowDismissZone.width
readonly property real maskHeight: _overflowDismissZone.height
readonly property real maskX: {
const triggeringBarX = (barPosition === 2) ? effectiveBarSize : 0;
const adjacentLeftBar = adjacentBarInfo?.leftBar ?? 0;
return Math.max(triggeringBarX, adjacentLeftBar);
}
readonly property real maskY: {
const triggeringBarY = (barPosition === 0) ? effectiveBarSize : 0;
const adjacentTopBar = adjacentBarInfo?.topBar ?? 0;
return Math.max(triggeringBarY, adjacentTopBar);
}
readonly property real maskWidth: {
const triggeringBarRight = (barPosition === 3) ? effectiveBarSize : 0;
const adjacentRightBar = adjacentBarInfo?.rightBar ?? 0;
const rightExclusion = Math.max(triggeringBarRight, adjacentRightBar);
return Math.max(100, width - maskX - rightExclusion);
}
readonly property real maskHeight: {
const triggeringBarBottom = (barPosition === 1) ? effectiveBarSize : 0;
const adjacentBottomBar = adjacentBarInfo?.bottomBar ?? 0;
const bottomExclusion = Math.max(triggeringBarBottom, adjacentBottomBar);
return Math.max(100, height - maskY - bottomExclusion);
DismissZone {
id: _overflowDismissZone
barPosition: overflowMenu.barPosition
barX: overflowMenu.barX
barY: overflowMenu.barY
barWidth: overflowMenu.barWidth
barHeight: overflowMenu.barHeight
screenWidth: overflowMenu.width
screenHeight: overflowMenu.height
adjacentBarInfo: overflowMenu.adjacentBarInfo
}
mask: Region {
@@ -1237,13 +1218,7 @@ BasePill {
fallbackOffset: 6
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
targetRadius: Theme.cornerRadius
sourceRect.antialiasing: true
sourceRect.smooth: true
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && !BlurService.enabled
layer.smooth: true
layer.textureSize: Qt.size(Math.round(width * overflowMenu.dpr * 2), Math.round(height * overflowMenu.dpr * 2))
layer.textureMirroring: ShaderEffectSource.MirrorVertically
layer.samples: 4
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled
}
Rectangle {
@@ -1450,20 +1425,12 @@ BasePill {
screen: menuRoot.parentScreen
WlrLayershell.layer: root.barUsesOverlayLayer ? WlrLayershell.Overlay : WlrLayershell.Top
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: {
if (PopoutManager.screenshotActive)
return WlrKeyboardFocus.None;
if (!menuRoot.showMenu)
return WlrKeyboardFocus.None;
if (CompositorService.useHyprlandFocusGrab)
return WlrKeyboardFocus.OnDemand;
return WlrKeyboardFocus.Exclusive;
}
WlrLayershell.keyboardFocus: KeyboardFocus.keyboardFocus(menuRoot.showMenu, null)
color: "transparent"
HyprlandFocusGrab {
windows: [menuWindow]
active: CompositorService.useHyprlandFocusGrab && menuRoot.showMenu
windows: [menuWindow].concat(KeyboardFocus.barWindows)
active: KeyboardFocus.wantsGrab(menuRoot.showMenu, null)
}
anchors {
@@ -1502,32 +1469,21 @@ BasePill {
"leftBar": 0,
"rightBar": 0
})
readonly property real effectiveBarSize: root.barThickness + root.barSpacing
readonly property real maskX: _menuDismissZone.x
readonly property real maskY: _menuDismissZone.y
readonly property real maskWidth: _menuDismissZone.width
readonly property real maskHeight: _menuDismissZone.height
readonly property real maskX: {
const triggeringBarX = (barPosition === 2) ? effectiveBarSize : 0;
const adjacentLeftBar = adjacentBarInfo?.leftBar ?? 0;
return Math.max(triggeringBarX, adjacentLeftBar);
}
readonly property real maskY: {
const triggeringBarY = (barPosition === 0) ? effectiveBarSize : 0;
const adjacentTopBar = adjacentBarInfo?.topBar ?? 0;
return Math.max(triggeringBarY, adjacentTopBar);
}
readonly property real maskWidth: {
const triggeringBarRight = (barPosition === 3) ? effectiveBarSize : 0;
const adjacentRightBar = adjacentBarInfo?.rightBar ?? 0;
const rightExclusion = Math.max(triggeringBarRight, adjacentRightBar);
return Math.max(100, width - maskX - rightExclusion);
}
readonly property real maskHeight: {
const triggeringBarBottom = (barPosition === 1) ? effectiveBarSize : 0;
const adjacentBottomBar = adjacentBarInfo?.bottomBar ?? 0;
const bottomExclusion = Math.max(triggeringBarBottom, adjacentBottomBar);
return Math.max(100, height - maskY - bottomExclusion);
DismissZone {
id: _menuDismissZone
barPosition: menuWindow.barPosition
barX: menuWindow.barX
barY: menuWindow.barY
barWidth: menuWindow.barWidth
barHeight: menuWindow.barHeight
screenWidth: menuWindow.width
screenHeight: menuWindow.height
adjacentBarInfo: menuWindow.adjacentBarInfo
}
mask: Region {
@@ -1689,11 +1645,7 @@ BasePill {
fallbackOffset: 6
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
targetRadius: Theme.cornerRadius
sourceRect.antialiasing: true
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && !BlurService.enabled
layer.smooth: true
layer.textureSize: Qt.size(Math.round(width * menuWindow.dpr), Math.round(height * menuWindow.dpr))
layer.textureMirroring: ShaderEffectSource.MirrorVertically
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled
}
Rectangle {
@@ -1970,4 +1922,53 @@ BasePill {
return;
currentTrayMenu.showForTrayItem(item, anchor, screen, atBottom, vertical ?? false, axisObj);
}
function _trayLayoutRoot() {
const contentChildren = root.visualContent?.children;
if (!contentChildren || contentChildren.length === 0)
return null;
const contentRoot = contentChildren[0];
return contentRoot?.layoutLoader?.item || null;
}
function _trayHitAtGlobalPoint(gx, gy) {
if (!root.visible || root.width <= 0 || root.height <= 0)
return null;
const local = root.mapFromItem(null, gx, gy);
if (local.x < 0 || local.y < 0 || local.x > root.width || local.y > root.height)
return null;
const layout = _trayLayoutRoot();
if (!layout)
return null;
const layoutLocal = layout.mapFromItem(null, gx, gy);
const children = layout.children || [];
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (!child.visible || child.width <= 0 || child.height <= 0)
continue;
if (layoutLocal.x < child.x || layoutLocal.x >= child.x + child.width)
continue;
if (layoutLocal.y < child.y || layoutLocal.y >= child.y + child.height)
continue;
if (child.trayItem)
return child;
}
return null;
}
function hoverTriggerAtGlobalPoint(gx, gy) {
const hit = _trayHitAtGlobalPoint(gx, gy);
if (!hit?.trayItem?.hasMenu)
return "";
return "tray-" + (hit.trayItem.id || hit.itemKey || "");
}
function openHoverAtGlobalPoint(gx, gy) {
const hit = _trayHitAtGlobalPoint(gx, gy);
if (!hit?.trayItem?.hasMenu)
return false;
const anchor = hit.children?.length > 0 ? hit.children[0] : hit;
showForTrayItem(hit.trayItem, anchor, parentScreen, isAtBottom, isVerticalOrientation, axis);
return true;
}
}
@@ -22,10 +22,7 @@ Item {
property var hyprlandOverviewLoader: null
property var parentScreen: null
// mango shares dwl's tag model; route to the right service so one set of
// branches serves both.
readonly property bool isDwlLike: CompositorService.isDwl || CompositorService.isMango
readonly property var dwlSvc: CompositorService.isMango ? MangoService : DwlService
readonly property bool isMango: CompositorService.isMango
readonly property real _leftMargin: {
if (isVertical)
@@ -80,9 +77,8 @@ Item {
return NiriService.currentOutput || root.screenName;
case "hyprland":
return Hyprland.focusedWorkspace?.monitor?.name || root.screenName;
case "dwl":
case "mango":
return root.dwlSvc.activeOutput || root.screenName;
return MangoService.activeOutput || root.screenName;
case "sway":
case "scroll":
case "miracle":
@@ -92,6 +88,7 @@ Item {
return root.screenName;
}
}
readonly property bool mangoOverviewActive: CompositorService.isMango && MangoService.isOutputInOverview(effectiveScreenName)
readonly property var extProjection: (useExtWorkspace && parentScreen) ? WindowManager.screenProjection(parentScreen) : null
readonly property bool useExtWorkspace: {
@@ -100,7 +97,6 @@ Item {
switch (CompositorService.compositor) {
case "niri":
case "hyprland":
case "dwl":
case "mango":
case "sway":
case "scroll":
@@ -127,7 +123,6 @@ Item {
return getNiriActiveWorkspace();
case "hyprland":
return getHyprlandActiveWorkspace();
case "dwl":
case "mango":
const activeTags = getDwlActiveTags();
return activeTags.length > 0 ? activeTags[0] : -1;
@@ -140,7 +135,7 @@ Item {
}
}
property var dwlActiveTags: {
if (root.isDwlLike) {
if (root.isMango) {
return getDwlActiveTags();
}
return [];
@@ -159,8 +154,9 @@ Item {
case "hyprland":
baseList = getHyprlandWorkspaces();
break;
case "dwl":
case "mango":
if (root.mangoOverviewActive)
return [];
baseList = getDwlTags();
break;
case "sway":
@@ -297,7 +293,7 @@ Item {
}
} else if (CompositorService.isHyprland) {
targetWorkspaceId = ws.id !== undefined ? ws.id : ws;
} else if (root.isDwlLike) {
} else if (root.isMango) {
if (typeof ws !== "object" || ws.tag === undefined) {
return [];
}
@@ -317,8 +313,8 @@ Item {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
isActiveWs = focusedWs ? (focusedWs.num === targetWorkspaceId) : false;
} else if (root.isDwlLike) {
const output = root.dwlSvc.getOutputState(root.effectiveScreenName);
} else if (root.isMango) {
const output = MangoService.getOutputState(root.effectiveScreenName);
if (output && output.tags) {
const tag = output.tags.find(t => t.tag === targetWorkspaceId);
isActiveWs = tag ? (tag.state === 1) : false;
@@ -406,7 +402,7 @@ Item {
"id": -1,
"name": ""
};
} else if (root.isDwlLike) {
} else if (root.isMango) {
placeholder = {
"tag": -1
};
@@ -488,11 +484,11 @@ Item {
}
function getDwlTags() {
if (!root.dwlSvc.available)
if (!MangoService.available)
return [];
const targetScreen = root.effectiveScreenName;
const output = root.dwlSvc.getOutputState(targetScreen);
const output = MangoService.getOutputState(targetScreen);
if (!output || !output.tags || output.tags.length === 0)
return [];
@@ -505,7 +501,7 @@ Item {
}));
}
const visibleTagIndices = root.dwlSvc.getVisibleTags(targetScreen);
const visibleTagIndices = MangoService.getVisibleTags(targetScreen);
return visibleTagIndices.map(tagIndex => {
const tagData = output.tags.find(t => t.tag === tagIndex);
return {
@@ -518,10 +514,10 @@ Item {
}
function getDwlActiveTags() {
if (!root.dwlSvc.available)
if (!MangoService.available)
return [];
return root.dwlSvc.getActiveTags(root.effectiveScreenName);
return MangoService.getActiveTags(root.effectiveScreenName);
}
function getExtWorkspaceWorkspaces() {
@@ -572,7 +568,7 @@ Item {
return ws && ws.idx !== -1;
if (CompositorService.isHyprland)
return ws && ws.id !== -1;
if (root.isDwlLike)
if (root.isMango)
return ws && ws.tag !== -1;
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return ws && ws.num !== -1;
@@ -600,10 +596,9 @@ Item {
HyprlandService.focusWorkspace(data.id);
}
break;
case "dwl":
case "mango":
if (data.tag !== undefined)
root.dwlSvc.switchToTag(root.screenName, data.tag);
MangoService.switchToTag(root.screenName, data.tag);
break;
case "sway":
case "scroll":
@@ -689,7 +684,7 @@ Item {
}
HyprlandService.focusWorkspace(realWorkspaces[nextIndex].id);
} else if (root.isDwlLike) {
} else if (root.isMango) {
const realWorkspaces = getRealWorkspaces();
if (realWorkspaces.length < 2) {
return;
@@ -703,7 +698,7 @@ Item {
return;
}
root.dwlSvc.switchToTag(root.screenName, realWorkspaces[nextIndex].tag);
MangoService.switchToTag(root.screenName, realWorkspaces[nextIndex].tag);
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const realWorkspaces = getRealWorkspaces();
if (realWorkspaces.length < 2) {
@@ -731,7 +726,7 @@ Item {
return (modelData?.idx !== undefined && modelData?.idx !== -1) ? modelData.idx : "";
if (CompositorService.isHyprland)
return modelData?.id || "";
if (root.isDwlLike)
if (root.isMango)
return (modelData?.tag !== undefined) ? (modelData.tag + 1) : "";
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return modelData?.num || "";
@@ -746,7 +741,7 @@ Item {
isPlaceholder = modelData?.idx === -1;
} else if (CompositorService.isHyprland) {
isPlaceholder = modelData?.id === -1;
} else if (root.isDwlLike) {
} else if (root.isMango) {
isPlaceholder = modelData?.tag === -1;
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
isPlaceholder = modelData?.num === -1;
@@ -781,7 +776,7 @@ Item {
return getWorkspaceIndexFallback(modelData, index);
}
readonly property bool hasNativeWorkspaceSupport: CompositorService.isNiri || CompositorService.isHyprland || root.isDwlLike || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
readonly property bool hasNativeWorkspaceSupport: CompositorService.isNiri || CompositorService.isHyprland || root.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
readonly property bool hasWorkspaces: getRealWorkspaces().length > 0
readonly property bool shouldShow: hasNativeWorkspaceSupport || (useExtWorkspace && hasWorkspaces)
@@ -977,7 +972,7 @@ Item {
StyledText {
anchors.verticalCenter: parent.verticalCenter
visible: !root.isVertical
text: I18n.tr("OVERVIEW")
text: I18n.tr("Overview")
color: Theme.primary
font.pixelSize: overviewPill.labelSize
font.weight: Font.DemiBold
@@ -1046,7 +1041,7 @@ Item {
return !!(modelData && modelData.idx === root.currentWorkspace);
if (CompositorService.isHyprland)
return !!(modelData && modelData.id === root.currentWorkspace);
if (root.isDwlLike)
if (root.isMango)
return !!(modelData && root.dwlActiveTags.includes(modelData.tag));
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return !!(modelData && modelData.num === root.currentWorkspace);
@@ -1055,7 +1050,7 @@ Item {
property bool isOccupied: {
if (CompositorService.isHyprland)
return Array.from(Hyprland.toplevels?.values || []).some(tl => tl.workspace?.id === modelData?.id);
if (root.isDwlLike)
if (root.isMango)
return modelData.clients > 0;
if (CompositorService.isNiri) {
const workspace = NiriService.allWorkspaces.find(ws => ws.idx + 1 === modelData && ws.output === root.effectiveScreenName);
@@ -1070,7 +1065,7 @@ Item {
return !!(modelData && modelData.idx === -1);
if (CompositorService.isHyprland)
return !!(modelData && modelData.id === -1);
if (root.isDwlLike)
if (root.isMango)
return !!(modelData && modelData.tag === -1);
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return !!(modelData && modelData.num === -1);
@@ -1087,7 +1082,7 @@ Item {
return modelData?.urgent ?? false;
if (CompositorService.isNiri)
return loadedIsUrgent;
if (root.isDwlLike)
if (root.isMango)
return modelData?.state === 2;
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return loadedIsUrgent;
@@ -1115,7 +1110,7 @@ Item {
targetWorkspaceId = modelData?.id;
} else if (CompositorService.isHyprland) {
targetWorkspaceId = modelData?.id;
} else if (CompositorService.isDwl) {
} else if (root.isMango) {
targetWorkspaceId = modelData?.tag;
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
targetWorkspaceId = modelData?.num;
@@ -1378,8 +1373,8 @@ Item {
}
} else if (CompositorService.isHyprland && modelData?.id) {
HyprlandService.focusWorkspace(modelData.id);
} else if (root.isDwlLike && modelData?.tag !== undefined) {
root.dwlSvc.switchToTag(root.screenName, modelData.tag);
} else if (root.isMango && modelData?.tag !== undefined) {
MangoService.switchToTag(root.screenName, modelData.tag);
} else if ((CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) && modelData?.num) {
try {
I3.dispatch(`workspace number ${modelData.num}`);
@@ -1390,8 +1385,8 @@ Item {
NiriService.toggleOverview();
} else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen;
} else if (root.isDwlLike && modelData?.tag !== undefined) {
root.dwlSvc.toggleTag(root.screenName, modelData.tag);
} else if (root.isMango && modelData?.tag !== undefined) {
MangoService.toggleTag(root.screenName, modelData.tag);
}
}
}
@@ -1415,7 +1410,7 @@ Item {
wsData = modelData || null;
} else if (CompositorService.isHyprland) {
wsData = modelData;
} else if (root.isDwlLike) {
} else if (root.isMango) {
wsData = modelData;
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
wsData = modelData;
@@ -1429,7 +1424,7 @@ Item {
}
if (SettingsData.showWorkspaceApps) {
if (root.isDwlLike || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
if (root.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
delegateRoot.loadedIcons = root.getWorkspaceIcons(modelData);
} else if (CompositorService.isNiri) {
delegateRoot.loadedIcons = root.getWorkspaceIcons(isPlaceholder ? null : modelData);
@@ -1989,8 +1984,8 @@ Item {
}
}
Connections {
target: root.dwlSvc
enabled: root.isDwlLike
target: MangoService
enabled: root.isMango
function onStateChanged() {
delegateRoot.updateAllData();
}
@@ -130,7 +130,7 @@ Item {
borderColor: volumePanel.border.color
borderWidth: volumePanel.border.width
shadowOpacity: Theme.elevationLevel2 && Theme.elevationLevel2.alpha !== undefined ? Theme.elevationLevel2.alpha : 0.25
shadowEnabled: Theme.elevationEnabled && !BlurService.enabled
shadowEnabled: Theme.elevationEnabled
}
MouseArea {
@@ -272,7 +272,7 @@ Item {
borderColor: audioDevicesPanel.border.color
borderWidth: audioDevicesPanel.border.width
shadowOpacity: Theme.elevationLevel2 && Theme.elevationLevel2.alpha !== undefined ? Theme.elevationLevel2.alpha : 0.25
shadowEnabled: Theme.elevationEnabled && !BlurService.enabled
shadowEnabled: Theme.elevationEnabled
}
MouseArea {
@@ -444,7 +444,7 @@ Item {
borderColor: playersPanel.border.color
borderWidth: playersPanel.border.width
shadowOpacity: Theme.elevationLevel2 && Theme.elevationLevel2.alpha !== undefined ? Theme.elevationLevel2.alpha : 0.25
shadowEnabled: Theme.elevationEnabled && !BlurService.enabled
shadowEnabled: Theme.elevationEnabled
}
MouseArea {
@@ -105,6 +105,13 @@ Rectangle {
}
onSelectedDateChanged: updateSelectedDateEvents()
onShowEventDetailsChanged: {
if (showEventDetails) {
taskInput.forceActiveFocus();
}
}
Component.onCompleted: {
loadEventsForMonth();
updateSelectedDateEvents();
@@ -176,7 +183,7 @@ Rectangle {
text: {
const dateStr = Qt.formatDate(selectedDate, "MMM d");
if (selectedDateEvents && selectedDateEvents.length > 0) {
const eventCount = selectedDateEvents.length === 1 ? I18n.tr("1 event") : selectedDateEvents.length + " " + I18n.tr("events");
const eventCount = selectedDateEvents.length === 1 ? I18n.tr("1 task", "task count next to a date") : I18n.tr("%1 tasks", "task count next to a date, %1 is the number of tasks").arg(selectedDateEvents.length);
return dateStr + " • " + eventCount;
}
return dateStr;
@@ -416,9 +423,503 @@ Rectangle {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)) {
root.selectedDate = dayDate;
root.showEventDetails = true;
root.selectedDate = dayDate;
root.showEventDetails = true;
}
}
}
}
}
}
}
Flickable {
id: flickableArea
width: parent.width - Theme.spacingS * 2
height: parent.height - (showEventDetails ? 40 + 42 : 28 + 18) - Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
visible: showEventDetails
clip: true
contentWidth: width
contentHeight: listViewContainer.height
interactive: listViewContainer.draggedItem === null
Item {
id: listViewContainer
width: parent.width
height: 100
property var draggedItem: null
property bool orderChanged: false
function resetAndLayout() {
for (let i = 0; i < repeater.count; i++) {
let item = repeater.itemAt(i);
if (item) {
item.visualIndex = i;
item.isDragging = false;
item.isEditing = false;
}
}
updateLayout();
}
function updateLayout() {
let items = [];
for (let i = 0; i < repeater.count; i++) {
let item = repeater.itemAt(i);
if (item) {
items.push(item);
}
}
items.sort((a, b) => a.visualIndex - b.visualIndex);
let currentY = 0;
for (let i = 0; i < items.length; i++) {
let item = items[i];
if (item && !item.isDragging) {
item.y = currentY;
}
if (item) {
currentY += item.height + Theme.spacingXS;
}
}
listViewContainer.height = Math.max(0, currentY - Theme.spacingXS);
}
function checkAndReorder(dragged) {
let items = [];
for (let i = 0; i < repeater.count; i++) {
let item = repeater.itemAt(i);
if (item) {
items.push(item);
}
}
items.sort((a, b) => a.visualIndex - b.visualIndex);
let swapped = false;
// Helper to get target Y position without animation offsets
function getTargetY(index) {
let y = 0;
for (let i = 0; i < index; i++) {
y += items[i].height + Theme.spacingXS;
}
return y;
}
while (true) {
let draggedIdx = items.indexOf(dragged);
if (draggedIdx === -1)
break;
let didSwap = false;
// Check item above
if (draggedIdx > 0) {
let above = items[draggedIdx - 1];
let targetYAbove = getTargetY(draggedIdx - 1);
if (above && dragged.y < (targetYAbove + above.height / 2)) {
// Swap visualIndex
let temp = dragged.visualIndex;
dragged.visualIndex = above.visualIndex;
above.visualIndex = temp;
// Swap in local array
items[draggedIdx] = above;
items[draggedIdx - 1] = dragged;
listViewContainer.orderChanged = true;
swapped = true;
didSwap = true;
}
}
// Check item below
if (!didSwap && draggedIdx < items.length - 1) {
let below = items[draggedIdx + 1];
let targetYBelow = getTargetY(draggedIdx + 1);
if (below && (dragged.y + dragged.height) > (targetYBelow + below.height / 2)) {
// Swap visualIndex
let temp = dragged.visualIndex;
dragged.visualIndex = below.visualIndex;
below.visualIndex = temp;
// Swap in local array
items[draggedIdx] = below;
items[draggedIdx + 1] = dragged;
listViewContainer.orderChanged = true;
swapped = true;
didSwap = true;
}
}
if (!didSwap) {
break;
}
}
if (swapped) {
updateLayout();
}
}
function saveNewOrder() {
if (!orderChanged)
return;
let items = [];
for (let i = 0; i < repeater.count; i++) {
let item = repeater.itemAt(i);
if (item) {
items.push(item);
}
}
items.sort((a, b) => a.visualIndex - b.visualIndex);
let orderedIds = [];
for (let i = 0; i < items.length; i++) {
let tid = items[i].taskId;
if (tid && tid.startsWith("task_")) {
orderedIds.push(tid.replace("task_", ""));
}
}
if (orderedIds.length > 0) {
CalendarService.reorderTasksForDate(root.selectedDate, orderedIds);
}
orderChanged = false;
}
Repeater {
id: repeater
model: selectedDateEvents
onModelChanged: {
Qt.callLater(listViewContainer.resetAndLayout);
}
delegate: Rectangle {
id: taskItem
width: parent ? parent.width : 0
height: isEditing ? 34 : (eventContent.implicitHeight + Theme.spacingS)
radius: Theme.cornerRadius
property int modelIndex: index
property int visualIndex: index
property string taskId: (modelData && modelData.id) ? modelData.id : ""
property bool isDragging: false
property bool isEditing: false
property real dragMouseOffsetY: 0
onModelIndexChanged: {
visualIndex = modelIndex;
}
onYChanged: {
if (isDragging) {
listViewContainer.checkAndReorder(taskItem);
}
}
color: isDragging ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : (eventMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : Theme.nestedSurface)
border.color: isDragging ? Theme.primary : (eventMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : Theme.outlineMedium)
border.width: (isDragging || eventMouseArea.containsMouse) ? 1 : Theme.layerOutlineWidth
scale: isDragging ? 1.02 : 1.0
z: isDragging ? 100 : visualIndex
Behavior on scale {
NumberAnimation {
duration: 100
}
}
Behavior on y {
id: yBehavior
enabled: !taskItem.isDragging
NumberAnimation {
duration: 150
easing.type: Easing.OutQuad
}
}
Component.onCompleted: {
visualIndex = index;
listViewContainer.updateLayout();
}
onHeightChanged: {
listViewContainer.updateLayout();
}
onIsEditingChanged: {
if (isEditing) {
editInput.forceActiveFocus();
editInput.selectAll();
}
}
Rectangle {
width: 3
height: parent.height - 6
anchors.left: parent.left
anchors.leftMargin: 3
anchors.verticalCenter: parent.verticalCenter
radius: Theme.cornerRadius
color: (modelData && modelData.id && modelData.id.startsWith("task_")) ? (modelData.completed ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Theme.primary) : Theme.primary
opacity: 0.8
}
// Drag Handle
Rectangle {
id: dragHandle
width: 24
height: 24
anchors.left: parent.left
anchors.leftMargin: 8
anchors.verticalCenter: parent.verticalCenter
radius: Theme.cornerRadius
color: "transparent"
visible: modelData && modelData.id && modelData.id.startsWith("task_") && !taskItem.isEditing
DankIcon {
anchors.centerIn: parent
name: "drag_indicator"
size: 14
color: dragMouseArea.containsMouse ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
}
MouseArea {
id: dragMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.SizeAllCursor
preventStealing: true
drag.target: taskItem
drag.axis: Drag.YAxis
drag.minimumY: 0
drag.maximumY: listViewContainer.height - taskItem.height
onPressed: {
taskItem.isDragging = true;
listViewContainer.orderChanged = false;
listViewContainer.draggedItem = taskItem;
}
onPositionChanged: {
// Handled natively by MouseArea.drag
}
onReleased: {
taskItem.isDragging = false;
listViewContainer.draggedItem = null;
if (listViewContainer.orderChanged) {
listViewContainer.saveNewOrder();
} else {
listViewContainer.updateLayout();
}
}
onCanceled: {
taskItem.isDragging = false;
listViewContainer.draggedItem = null;
listViewContainer.resetAndLayout();
}
}
}
// Checkbox status icon
Rectangle {
id: checkboxContainer
width: 24
height: 24
anchors.left: parent.left
anchors.leftMargin: (modelData && modelData.id && modelData.id.startsWith("task_")) ? (taskItem.isEditing ? 8 : 32) : 8
anchors.verticalCenter: parent.verticalCenter
radius: Theme.cornerRadius
color: "transparent"
visible: modelData && modelData.id && modelData.id.startsWith("task_")
DankIcon {
anchors.centerIn: parent
name: (modelData && modelData.completed) ? "check_box" : "check_box_outline_blank"
size: 16
color: (modelData && modelData.completed) ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
}
}
Column {
id: eventContent
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: (modelData && modelData.id && modelData.id.startsWith("task_")) ? 60 : (Theme.spacingS + 6)
anchors.rightMargin: (modelData && modelData.id && modelData.id.startsWith("task_")) ? 64 : Theme.spacingXS
spacing: 2
visible: !taskItem.isEditing
StyledText {
width: parent.width
text: modelData ? modelData.title : ""
font.pixelSize: Theme.fontSizeSmall
color: (modelData && modelData.id && modelData.id.startsWith("task_") && modelData.completed) ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) : Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
}
StyledText {
width: parent.width
text: {
if (!modelData || modelData.allDay) {
return I18n.tr("All day", "calendar task with no specific time");
} else if (modelData.start && modelData.end) {
const timeFormat = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP";
const startTime = Qt.formatTime(modelData.start, timeFormat);
if (modelData.start.toDateString() !== modelData.end.toDateString() || modelData.start.getTime() !== modelData.end.getTime()) {
return startTime + " " + Qt.formatTime(modelData.end, timeFormat);
}
return startTime;
}
return "";
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
font.weight: Font.Normal
visible: text !== "" && modelData && modelData.id && !modelData.id.startsWith("task_")
}
}
// Inline Edit Input Box
Rectangle {
id: editInputContainer
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 36
anchors.rightMargin: 64
anchors.verticalCenter: parent.verticalCenter
height: 28
visible: taskItem.isEditing
color: "transparent"
TextInput {
id: editInput
anchors.fill: parent
verticalAlignment: TextInput.AlignVCenter
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeSmall
selectByMouse: true
clip: true
text: modelData ? modelData.title : ""
onAccepted: {
let txt = text.trim();
if (txt !== "" && modelData && modelData.id) {
CalendarService.editTask(modelData.id, txt);
}
taskItem.isEditing = false;
}
Keys.onEscapePressed: {
taskItem.isEditing = false;
}
}
}
// Main body MouseArea (declared before the delete/edit buttons so they sit on top)
MouseArea {
id: eventMouseArea
anchors.fill: parent
anchors.leftMargin: (modelData && modelData.id && modelData.id.startsWith("task_")) ? 32 : 6
anchors.rightMargin: (modelData && modelData.id && modelData.id.startsWith("task_")) ? 64 : 0
hoverEnabled: true
cursorShape: (modelData && (modelData.url || (modelData.id && modelData.id.startsWith("task_")))) ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: modelData && (modelData.url !== "" || (modelData.id && modelData.id.startsWith("task_"))) && !taskItem.isEditing
onClicked: {
if (modelData && modelData.id && modelData.id.startsWith("task_")) {
CalendarService.toggleTask(modelData.id);
} else if (modelData && modelData.url && modelData.url !== "") {
if (Qt.openUrlExternally(modelData.url) === false) {
log.warn("Failed to open URL: " + modelData.url);
} else {
root.closeDash();
}
}
}
}
// Delete / Cancel Button
Rectangle {
id: deleteButton
width: 24
height: 24
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
radius: Theme.cornerRadius
color: deleteMouseArea.containsMouse ? (taskItem.isEditing ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(0.9, 0.2, 0.2, 0.15)) : "transparent"
visible: modelData && modelData.id && modelData.id.startsWith("task_")
DankIcon {
anchors.centerIn: parent
name: taskItem.isEditing ? "close" : "delete"
size: 14
color: deleteMouseArea.containsMouse ? (taskItem.isEditing ? Theme.primary : Qt.rgba(0.9, 0.2, 0.2, 1.0)) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
}
MouseArea {
id: deleteMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (taskItem.isEditing) {
taskItem.isEditing = false;
} else if (modelData && modelData.id) {
CalendarService.removeTask(modelData.id);
}
}
}
}
// Edit / Save Button
Rectangle {
id: editButton
width: 24
height: 24
anchors.right: deleteButton.left
anchors.rightMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
radius: Theme.cornerRadius
color: editMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
visible: modelData && modelData.id && modelData.id.startsWith("task_")
DankIcon {
anchors.centerIn: parent
name: taskItem.isEditing ? "check" : "edit"
size: 14
color: editMouseArea.containsMouse ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
}
MouseArea {
id: editMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (taskItem.isEditing) {
let txt = editInput.text.trim();
if (txt !== "" && modelData && modelData.id) {
CalendarService.editTask(modelData.id, txt);
}
taskItem.isEditing = false;
} else {
taskItem.isEditing = true;
}
}
}
@@ -428,105 +929,40 @@ Rectangle {
}
}
DankListView {
Rectangle {
width: parent.width - Theme.spacingS * 2
height: parent.height - (showEventDetails ? 40 : 28 + 18) - Theme.spacingS
height: 34
anchors.horizontalCenter: parent.horizontalCenter
model: selectedDateEvents
radius: Theme.cornerRadius
color: Theme.nestedSurface
border.color: Theme.outlineMedium
border.width: 1
visible: showEventDetails
clip: true
spacing: Theme.spacingXS
delegate: Rectangle {
width: parent ? parent.width : 0
height: eventContent.implicitHeight + Theme.spacingS
radius: Theme.cornerRadius
color: {
if (modelData.url && eventMouseArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
} else if (eventMouseArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06);
}
return Theme.nestedSurface;
}
border.color: {
if (modelData.url && eventMouseArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3);
} else if (eventMouseArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15);
}
return Theme.outlineMedium;
}
border.width: eventMouseArea.containsMouse ? 1 : Theme.layerOutlineWidth
TextInput {
id: taskInput
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
verticalAlignment: TextInput.AlignVCenter
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeSmall
selectByMouse: true
clip: true
Rectangle {
width: 3
height: parent.height - 6
anchors.left: parent.left
anchors.leftMargin: 3
Text {
text: I18n.tr("Add a task...", "placeholder in the new-task input field")
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
visible: !taskInput.text && !taskInput.activeFocus
font.pixelSize: Theme.fontSizeSmall
anchors.verticalCenter: parent.verticalCenter
radius: Theme.cornerRadius
color: Theme.primary
opacity: 0.8
}
Column {
id: eventContent
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingS + 6
anchors.rightMargin: Theme.spacingXS
spacing: 2
StyledText {
width: parent.width
text: modelData.title
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
}
StyledText {
width: parent.width
text: {
if (!modelData || modelData.allDay) {
return I18n.tr("All day");
} else if (modelData.start && modelData.end) {
const timeFormat = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP";
const startTime = Qt.formatTime(modelData.start, timeFormat);
if (modelData.start.toDateString() !== modelData.end.toDateString() || modelData.start.getTime() !== modelData.end.getTime()) {
return startTime + " " + Qt.formatTime(modelData.end, timeFormat);
}
return startTime;
}
return "";
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
font.weight: Font.Normal
visible: text !== ""
}
}
MouseArea {
id: eventMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: modelData.url ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: modelData.url !== ""
onClicked: {
if (modelData.url && modelData.url !== "") {
if (Qt.openUrlExternally(modelData.url) === false) {
log.warn("Failed to open URL: " + modelData.url);
} else {
root.closeDash();
}
}
onAccepted: {
let txt = text.trim();
if (txt !== "") {
CalendarService.addTaskForDate(root.selectedDate, txt);
text = "";
}
}
}
@@ -67,9 +67,6 @@ Card {
return I18n.tr("on Niri");
if (CompositorService.isHyprland)
return I18n.tr("on Hyprland");
// technically they might not be on mangowc, but its what we support in the docs
if (CompositorService.isDwl)
return I18n.tr("on MangoWC");
if (CompositorService.isMango)
return I18n.tr("on MangoWC");
if (CompositorService.isSway)
@@ -101,9 +98,7 @@ Card {
}
StyledText {
text: DgopService.shortUptime
? I18n.tr("up") + DgopService.shortUptime.slice(2)
: I18n.tr("up")
text: DgopService.shortUptime ? I18n.tr("up", "uptime prefix, e.g. 'up 4h 2m'") + DgopService.shortUptime.slice(2) : I18n.tr("up", "uptime prefix, e.g. 'up 4h 2m'")
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.verticalCenter: parent.verticalCenter
@@ -20,17 +20,25 @@ Card {
spacing: Theme.spacingS
visible: !WeatherService.weather.available
DankSpinner {
size: 24
visible: WeatherService.weather.loading
anchors.horizontalCenter: parent.horizontalCenter
}
DankIcon {
name: "cloud_off"
size: 24
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
visible: !WeatherService.weather.loading
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: WeatherService.weather.loading ? I18n.tr("Loading...") : I18n.tr("No Weather")
text: I18n.tr("No Weather")
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: !WeatherService.weather.loading
anchors.horizontalCenter: parent.horizontalCenter
}
+57 -14
View File
@@ -186,13 +186,36 @@ Variants {
return;
}
const presented = dock.visible && (dock.reveal || slideXAnimation.running || slideYAnimation.running) && dock.hasApps;
const phase = !presented ? "hidden" : ((!dock.reveal && (slideXAnimation.running || slideYAnimation.running)) ? "closing" : ((slideXAnimation.running || slideYAnimation.running) ? "opening" : "open"));
const bodyX = dock._dockWindowOriginX() + dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x;
const bodyY = dock._dockWindowOriginY() + dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y;
const bodyW = dock.hasApps ? dockBackground.width : 0;
const bodyH = dock.hasApps ? dockBackground.height : 0;
ConnectedModeState.setDockState(dock._dockScreenName, {
"reveal": dock.visible && (dock.reveal || slideXAnimation.running || slideYAnimation.running) && dock.hasApps,
"kind": "dock",
"screenName": dock._dockScreenName,
"phase": phase,
"visible": presented,
"presented": presented,
"reveal": presented,
"barSide": dock.connectedBarSide,
"bodyX": dock._dockWindowOriginX() + dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x,
"bodyY": dock._dockWindowOriginY() + dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y,
"bodyW": dock.hasApps ? dockBackground.width : 0,
"bodyH": dock.hasApps ? dockBackground.height : 0,
"bodyRect": {
"x": bodyX,
"y": bodyY,
"width": bodyW,
"height": bodyH
},
"animationOffset": {
"x": dockSlide.x,
"y": dockSlide.y
},
"scale": 1,
"opacity": Theme.connectedSurfaceColor.a,
"bodyX": bodyX,
"bodyY": bodyY,
"bodyW": bodyW,
"bodyH": bodyH,
"slideX": dockSlide.x,
"slideY": dockSlide.y
});
@@ -724,16 +747,36 @@ Variants {
onHeightChanged: dock._syncDockChromeState()
}
ConnectedShape {
Item {
id: dockConnectedChrome
visible: Theme.isConnectedEffect && dock.reveal && !SettingsData.connectedFrameModeActive
barSide: dock.connectedBarSide
bodyWidth: dockBackground.width
bodyHeight: dockBackground.height
connectorRadius: Theme.connectedCornerRadius
surfaceRadius: dock.surfaceRadius
fillColor: dock.surfaceColor
x: dockBackground.x - bodyX
y: dockBackground.y - bodyY
readonly property real extraLeft: dock.isVertical ? 0 : Theme.connectedCornerRadius
readonly property real extraTop: dock.isVertical ? Theme.connectedCornerRadius : 0
readonly property real bodyRadius: dock.surfaceRadius
readonly property bool barTop: dock.connectedBarSide === "top"
readonly property bool barBottom: dock.connectedBarSide === "bottom"
readonly property bool barLeft: dock.connectedBarSide === "left"
readonly property bool barRight: dock.connectedBarSide === "right"
x: dockBackground.x - extraLeft
y: dockBackground.y - extraTop
width: dockBackground.width + extraLeft * 2
height: dockBackground.height + extraTop * 2
ShaderEffect {
anchors.fill: parent
fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/connected_chrome.frag.qsb")
property real widthPx: width
property real heightPx: height
property vector4d surfaceColor: Qt.vector4d(dock.surfaceColor.r, dock.surfaceColor.g, dock.surfaceColor.b, dock.surfaceColor.a)
property vector4d shadowColor: Qt.vector4d(0, 0, 0, 0)
property vector4d shadowParam: Qt.vector4d(0, 0, 0, 0)
property vector4d ambientParam: Qt.vector4d(0, 0, 0, 0)
property vector4d bodyRect: Qt.vector4d(dockConnectedChrome.extraLeft, dockConnectedChrome.extraTop, dockBackground.width, dockBackground.height)
property vector4d cornerRadius: Qt.vector4d(dockConnectedChrome.barTop || dockConnectedChrome.barLeft ? 0 : dockConnectedChrome.bodyRadius, dockConnectedChrome.barTop || dockConnectedChrome.barRight ? 0 : dockConnectedChrome.bodyRadius, dockConnectedChrome.barBottom || dockConnectedChrome.barRight ? 0 : dockConnectedChrome.bodyRadius, dockConnectedChrome.barBottom || dockConnectedChrome.barLeft ? 0 : dockConnectedChrome.bodyRadius)
property vector4d edgeParam: Qt.vector4d(dockConnectedChrome.barTop ? 0 : (dockConnectedChrome.barBottom ? 1 : (dockConnectedChrome.barLeft ? 2 : 3)), Theme.connectedCornerRadius, 0, 0)
}
}
Shape {
@@ -236,7 +236,7 @@ Item {
}
IconImage {
visible: SettingsData.dockLauncherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isLabwc)
visible: SettingsData.dockLauncherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isLabwc)
anchors.centerIn: parent
width: actualIconSize + SettingsData.dockLauncherLogoSizeOffset
height: actualIconSize + SettingsData.dockLauncherLogoSizeOffset
@@ -247,8 +247,6 @@ Item {
return "file://" + Theme.shellDir + "/assets/niri.svg";
} else if (CompositorService.isHyprland) {
return "file://" + Theme.shellDir + "/assets/hyprland.svg";
} else if (CompositorService.isDwl) {
return "file://" + Theme.shellDir + "/assets/mango.png";
} else if (CompositorService.isMango) {
return "file://" + Theme.shellDir + "/assets/mango.png";
} else if (CompositorService.isSway) {
+8 -33
View File
@@ -1,9 +1,9 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Effects
import qs.Common
// Frame perimeter ring with rounded cutout (SDF).
Item {
id: root
@@ -16,39 +16,14 @@ Item {
required property real cutoutRadius
property color borderColor: Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity)
Rectangle {
id: borderRect
ShaderEffect {
anchors.fill: parent
// Bake frameOpacity into the color alpha rather than using the `opacity` property
color: root.borderColor
fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/frame_arc.frag.qsb")
layer.enabled: true
layer.effect: MultiEffect {
maskSource: cutoutMask
maskEnabled: true
maskInverted: true
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
}
Item {
id: cutoutMask
anchors.fill: parent
layer.enabled: true
visible: false
Rectangle {
anchors {
fill: parent
topMargin: root.cutoutTopInset
bottomMargin: root.cutoutBottomInset
leftMargin: root.cutoutLeftInset
rightMargin: root.cutoutRightInset
}
radius: root.cutoutRadius
}
property real widthPx: width
property real heightPx: height
property real cutoutRadius: root.cutoutRadius
property vector4d cutout: Qt.vector4d(root.cutoutLeftInset, root.cutoutTopInset, root.width - root.cutoutRightInset, root.height - root.cutoutBottomInset)
property vector4d surfaceColor: Qt.vector4d(root.borderColor.r, root.borderColor.g, root.borderColor.b, root.borderColor.a)
}
}
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More