mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-19 01:25:21 -04:00
Compare commits
35 Commits
e0ab0a6b90
..
hover
| Author | SHA1 | Date | |
|---|---|---|---|
| fc72b6d779 | |||
| 3701b3d7a3 | |||
| bae98daa5c | |||
| b34a04f723 | |||
| 1c0245f2db | |||
| 7777e87dc8 | |||
| 820fa07846 | |||
| 66794582c9 | |||
| 73eb471ae3 | |||
| 0f2f4b96c4 | |||
| d53809cf2b | |||
| 08fd6e26d8 | |||
| 29e8470f2e | |||
| 573785d4ce | |||
| 5483303714 | |||
| 5a5cc4f4e9 | |||
| cd672c341f | |||
| 12438d63c2 | |||
| 35255e4053 | |||
| 8856d45887 | |||
| 38af56c6fd | |||
| 9111e4809d | |||
| d08c7c5e55 | |||
| 69f3dee25a | |||
| 8155970ba2 | |||
| d356957dad | |||
| e7ccb702a3 | |||
| bf3ce6deb2 | |||
| f5295fb35d | |||
| 6c5836722a | |||
| 5716249bd9 | |||
| 4d0aab773b | |||
| e50ac208e3 | |||
| bcb5617194 | |||
| d3c23ba737 |
@@ -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
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -115,3 +115,5 @@ core.*
|
||||
.direnv/
|
||||
quickshell/dms-plugins
|
||||
__pycache__
|
||||
|
||||
.vscode/
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 ©
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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, ¤tState) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user