1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-16 08:05:19 -04:00

Compare commits

..

5 Commits

Author SHA1 Message Date
purian23 1217b25de5 (frame): Add blur support & cleanup 2026-03-31 21:25:51 -04:00
purian23 e913630f90 (frame): Multi-monitor support 2026-03-31 15:31:43 -04:00
purian23 220bb2708b Connected frames & defaults 2026-03-31 15:31:43 -04:00
purian23 e57ab3e1f3 Continue frame implementation 2026-03-31 15:31:43 -04:00
purian23 952ab9b753 Initial framework 2026-03-31 15:31:43 -04:00
119 changed files with 26078 additions and 30499 deletions
+20 -7
View File
@@ -1,13 +1,26 @@
repos: repos:
- repo: https://github.com/golangci/golangci-lint
rev: v2.10.1
hooks:
- id: golangci-lint-fmt
require_serial: true
- id: golangci-lint-full
- id: golangci-lint-config-verify
- repo: local - repo: local
hooks: hooks:
- id: golangci-lint-fmt
name: golangci-lint-fmt
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 fmt
language: system
require_serial: true
types: [go]
pass_filenames: false
- id: golangci-lint-full
name: golangci-lint-full
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 run --fix
language: system
require_serial: true
types: [go]
pass_filenames: false
- id: golangci-lint-config-verify
name: golangci-lint-config-verify
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 config verify
language: system
files: \.golangci\.(?:yml|yaml|toml|json)
pass_filenames: false
- id: go-test - id: go-test
name: go test name: go test
entry: go test ./... entry: go test ./...
+1 -41
View File
@@ -10,7 +10,7 @@ Go-based backend for DankMaterialShell providing system integration, IPC, and in
Command-line interface and daemon for shell management and system control. Command-line interface and daemon for shell management and system control.
**dankinstall** **dankinstall**
Distribution-aware installer for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, and Gentoo. Supports both an interactive TUI and a headless (unattended) mode via CLI flags. Distribution-aware installer with TUI for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, and Gentoo.
## System Integration ## System Integration
@@ -147,50 +147,10 @@ go-wayland-scanner -i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
## Installation via dankinstall ## Installation via dankinstall
**Interactive (TUI):**
```bash ```bash
curl -fsSL https://install.danklinux.com | sh curl -fsSL https://install.danklinux.com | sh
``` ```
**Headless (unattended):**
Headless mode requires cached sudo credentials. Run `sudo -v` first:
```bash
sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c niri -t ghostty -y
sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c hyprland -t kitty --include-deps dms-greeter -y
```
| Flag | Short | Description |
|------|-------|-------------|
| `--compositor <niri|hyprland>` | `-c` | Compositor/WM to install (required for headless) |
| `--term <ghostty|kitty|alacritty>` | `-t` | Terminal emulator (required for headless) |
| `--include-deps <name,...>` | | Enable optional dependencies (e.g. `dms-greeter`) |
| `--exclude-deps <name,...>` | | Skip specific dependencies |
| `--replace-configs <name,...>` | | Replace specific configuration files (mutually exclusive with `--replace-configs-all`) |
| `--replace-configs-all` | | Replace all configuration files (mutually exclusive with `--replace-configs`) |
| `--yes` | `-y` | Required for headless mode — confirms installation without interactive prompts |
Headless mode requires `--yes` to proceed; without it, the installer exits with an error.
Configuration files are not replaced by default unless `--replace-configs` or `--replace-configs-all` is specified.
`dms-greeter` is disabled by default; use `--include-deps dms-greeter` to enable it.
When no flags are provided, `dankinstall` launches the interactive TUI.
### Headless mode validation rules
Headless mode activates when `--compositor` or `--term` is provided.
- Both `--compositor` and `--term` are required; providing only one results in an error.
- Headless-only flags (`--include-deps`, `--exclude-deps`, `--replace-configs`, `--replace-configs-all`, `--yes`) are rejected in TUI mode.
- Positional arguments are not accepted.
### Log file location
`dankinstall` writes logs to `/tmp` by default.
Set the `DANKINSTALL_LOG_DIR` environment variable to override the log directory.
## Supported Distributions ## Supported Distributions
Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo (and derivatives) Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo (and derivatives)
+3 -167
View File
@@ -3,152 +3,20 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/headless"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/tui" "github.com/AvengeMedia/DankMaterialShell/core/internal/tui"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/spf13/cobra"
) )
var Version = "dev" var Version = "dev"
// Flag variables bound via pflag
var (
compositor string
term string
includeDeps []string
excludeDeps []string
replaceConfigs []string
replaceConfigsAll bool
yes bool
)
var rootCmd = &cobra.Command{
Use: "dankinstall",
Short: "Install DankMaterialShell and its dependencies",
Long: `dankinstall sets up DankMaterialShell with your chosen compositor and terminal.
Without flags, it launches an interactive TUI. Providing either --compositor
or --term activates headless (unattended) mode, which requires both flags.
Headless mode requires cached sudo credentials. Run 'sudo -v' beforehand, or
configure passwordless sudo for your user.`,
Args: cobra.NoArgs,
RunE: runDankinstall,
SilenceErrors: true,
SilenceUsage: true,
}
func init() {
rootCmd.Flags().StringVarP(&compositor, "compositor", "c", "", "Compositor/WM to install: niri or hyprland (enables headless mode)")
rootCmd.Flags().StringVarP(&term, "term", "t", "", "Terminal emulator to install: ghostty, kitty, or alacritty (enables headless mode)")
rootCmd.Flags().StringSliceVar(&includeDeps, "include-deps", []string{}, "Optional deps to enable (e.g. dms-greeter)")
rootCmd.Flags().StringSliceVar(&excludeDeps, "exclude-deps", []string{}, "Deps to skip during installation")
rootCmd.Flags().StringSliceVar(&replaceConfigs, "replace-configs", []string{}, "Deploy only named configs (e.g. niri,ghostty)")
rootCmd.Flags().BoolVar(&replaceConfigsAll, "replace-configs-all", false, "Deploy and replace all configurations")
rootCmd.Flags().BoolVarP(&yes, "yes", "y", false, "Auto-confirm all prompts")
}
func main() { func main() {
if os.Getuid() == 0 { if os.Getuid() == 0 {
fmt.Fprintln(os.Stderr, "Error: dankinstall must not be run as root") fmt.Fprintln(os.Stderr, "Error: dankinstall must not be run as root")
os.Exit(1) os.Exit(1)
} }
if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
func runDankinstall(cmd *cobra.Command, args []string) error {
headlessMode := compositor != "" || term != ""
if !headlessMode {
// Reject headless-only flags when running in TUI mode.
headlessOnly := []string{
"include-deps",
"exclude-deps",
"replace-configs",
"replace-configs-all",
"yes",
}
var set []string
for _, name := range headlessOnly {
if cmd.Flags().Changed(name) {
set = append(set, "--"+name)
}
}
if len(set) > 0 {
return fmt.Errorf("flags %s are only valid in headless mode (requires both --compositor and --term)", strings.Join(set, ", "))
}
}
if headlessMode {
return runHeadless()
}
return runTUI()
}
func runHeadless() error {
// Validate required flags
if compositor == "" {
return fmt.Errorf("--compositor is required for headless mode (niri or hyprland)")
}
if term == "" {
return fmt.Errorf("--term is required for headless mode (ghostty, kitty, or alacritty)")
}
cfg := headless.Config{
Compositor: compositor,
Terminal: term,
IncludeDeps: includeDeps,
ExcludeDeps: excludeDeps,
ReplaceConfigs: replaceConfigs,
ReplaceConfigsAll: replaceConfigsAll,
Yes: yes,
}
runner := headless.NewRunner(cfg)
// Set up file logging
fileLogger, err := log.NewFileLogger()
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to create log file: %v\n", err)
}
if fileLogger != nil {
fmt.Printf("Logging to: %s\n", fileLogger.GetLogPath())
fileLogger.StartListening(runner.GetLogChan())
defer func() {
if err := fileLogger.Close(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to close log file: %v\n", err)
}
}()
} else {
// Drain the log channel to prevent blocking sends from deadlocking
// downstream components (distros, config deployer) that write to it.
// Use an explicit stop signal because this code does not own the
// runner log channel and cannot assume it will be closed.
defer drainLogChan(runner.GetLogChan())()
}
if err := runner.Run(); err != nil {
if fileLogger != nil {
fmt.Fprintf(os.Stderr, "\nFull logs are available at: %s\n", fileLogger.GetLogPath())
}
return err
}
if fileLogger != nil {
fmt.Printf("\nFull logs are available at: %s\n", fileLogger.GetLogPath())
}
return nil
}
func runTUI() error {
fileLogger, err := log.NewFileLogger() fileLogger, err := log.NewFileLogger()
if err != nil { if err != nil {
fmt.Printf("Warning: Failed to create log file: %v\n", err) fmt.Printf("Warning: Failed to create log file: %v\n", err)
@@ -170,50 +38,18 @@ func runTUI() error {
if fileLogger != nil { if fileLogger != nil {
fileLogger.StartListening(model.GetLogChan()) fileLogger.StartListening(model.GetLogChan())
} else {
// Drain the log channel to prevent blocking sends from deadlocking
// downstream components (distros, config deployer) that write to it.
// Use an explicit stop signal because this code does not own the
// model log channel and cannot assume it will be closed.
defer drainLogChan(model.GetLogChan())()
} }
p := tea.NewProgram(model, tea.WithAltScreen()) p := tea.NewProgram(model, tea.WithAltScreen())
if _, err := p.Run(); err != nil { if _, err := p.Run(); err != nil {
fmt.Printf("Error running program: %v\n", err)
if logFilePath != "" { if logFilePath != "" {
fmt.Fprintf(os.Stderr, "\nFull logs are available at: %s\n", logFilePath) fmt.Printf("\nFull logs are available at: %s\n", logFilePath)
} }
return fmt.Errorf("error running program: %w", err) os.Exit(1)
} }
if logFilePath != "" { if logFilePath != "" {
fmt.Printf("\nFull logs are available at: %s\n", logFilePath) fmt.Printf("\nFull logs are available at: %s\n", logFilePath)
} }
return nil
}
// drainLogChan starts a goroutine that discards all messages from logCh,
// preventing blocking sends from deadlocking downstream components. It returns
// a cleanup function that signals the goroutine to stop and waits for it to
// exit. Callers should defer the returned function.
func drainLogChan(logCh <-chan string) func() {
drainStop := make(chan struct{})
drainDone := make(chan struct{})
go func() {
defer close(drainDone)
for {
select {
case <-drainStop:
return
case _, ok := <-logCh:
if !ok {
return
}
}
}
}()
return func() {
close(drainStop)
<-drainDone
}
} }
-1
View File
@@ -236,7 +236,6 @@ func runBrightnessSet(cmd *cobra.Command, args []string) {
defer ddc.Close() defer ddc.Close()
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
if err := ddc.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, nil); err == nil { if err := ddc.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, nil); err == nil {
ddc.WaitPending()
fmt.Printf("Set %s to %d%%\n", deviceID, percent) fmt.Printf("Set %s to %d%%\n", deviceID, percent)
return return
} }
+4 -8
View File
@@ -82,7 +82,7 @@ func (ds *DoctorStatus) OKCount() int {
} }
var ( var (
quickshellVersionRegex = regexp.MustCompile(`(?i)quickshell (\d+\.\d+\.\d+)`) quickshellVersionRegex = regexp.MustCompile(`quickshell (\d+\.\d+\.\d+)`)
hyprlandVersionRegex = regexp.MustCompile(`v?(\d+\.\d+\.\d+)`) hyprlandVersionRegex = regexp.MustCompile(`v?(\d+\.\d+\.\d+)`)
niriVersionRegex = regexp.MustCompile(`niri (\d+\.\d+)`) niriVersionRegex = regexp.MustCompile(`niri (\d+\.\d+)`)
swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`) swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`)
@@ -820,14 +820,10 @@ func checkOptionalDependencies() []checkResult {
results = append(results, checkImageFormatPlugins()...) results = append(results, checkImageFormatPlugins()...)
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"} terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
terminals = slices.DeleteFunc(terminals, func(t string) bool { if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
return !utils.CommandExists(t) results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], "", optionalFeaturesURL})
})
if len(terminals) > 0 {
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, strings.Join(terminals, ", "), "", optionalFeaturesURL})
} else { } else {
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, foot or alacritty", optionalFeaturesURL}) results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, or alacritty", optionalFeaturesURL})
} }
networkResult, err := network.DetectNetworkStack() networkResult, err := network.DetectNetworkStack()
+3 -28
View File
@@ -109,41 +109,16 @@ func updateArchLinux() error {
} }
var packageName string var packageName string
var isAUR bool if isArchPackageInstalled("dms-shell-bin") {
if isArchPackageInstalled("dms-shell") { packageName = "dms-shell-bin"
packageName = "dms-shell"
} else if isArchPackageInstalled("dms-shell-git") { } else if isArchPackageInstalled("dms-shell-git") {
packageName = "dms-shell-git" packageName = "dms-shell-git"
isAUR = true
} else if isArchPackageInstalled("dms-shell-bin") {
packageName = "dms-shell-bin"
isAUR = true
} else { } else {
fmt.Println("Info: No dms-shell package found.") fmt.Println("Info: Neither dms-shell-bin nor dms-shell-git package found.")
fmt.Println("Info: Falling back to git-based update method...") fmt.Println("Info: Falling back to git-based update method...")
return updateOtherDistros() return updateOtherDistros()
} }
if !isAUR {
fmt.Printf("This will update %s using pacman.\n", packageName)
if !confirmUpdate() {
return errdefs.ErrUpdateCancelled
}
fmt.Printf("\nRunning: sudo pacman -S %s\n", packageName)
cmd := exec.Command("sudo", "pacman", "-S", "--noconfirm", packageName)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Printf("Error: Failed to update using pacman: %v\n", err)
return err
}
fmt.Println("dms successfully updated")
return nil
}
var helper string var helper string
var updateCmd *exec.Cmd var updateCmd *exec.Cmd
+1 -4
View File
@@ -5,7 +5,6 @@ package main
import ( import (
"os" "os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
) )
@@ -31,9 +30,7 @@ func init() {
} }
func main() { func main() {
clipboard.MaybeServeAndExit() if os.Geteuid() == 0 {
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
log.Fatal("This program should not be run as root. Exiting.") log.Fatal("This program should not be run as root. Exiting.")
} }
+1 -4
View File
@@ -5,7 +5,6 @@ package main
import ( import (
"os" "os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
) )
@@ -28,9 +27,7 @@ func init() {
} }
func main() { func main() {
clipboard.MaybeServeAndExit() if os.Geteuid() == 0 {
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
log.Fatal("This program should not be run as root. Exiting.") log.Fatal("This program should not be run as root. Exiting.")
} }
-16
View File
@@ -7,22 +7,6 @@ import (
"strings" "strings"
) )
// isReadOnlyCommand returns true if the CLI args indicate a command that is
// safe to run as root (e.g. shell completion, help).
func isReadOnlyCommand(args []string) bool {
for _, arg := range args[1:] {
if strings.HasPrefix(arg, "-") {
continue
}
switch arg {
case "completion", "help", "__complete":
return true
}
return false
}
return false
}
func isArchPackageInstalled(packageName string) bool { func isArchPackageInstalled(packageName string) bool {
cmd := exec.Command("pacman", "-Q", packageName) cmd := exec.Command("pacman", "-Q", packageName)
err := cmd.Run() err := cmd.Run()
+84 -119
View File
@@ -1,6 +1,7 @@
package clipboard package clipboard
import ( import (
"bytes"
"fmt" "fmt"
"io" "io"
"os" "os"
@@ -12,142 +13,66 @@ import (
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
) )
const envServe = "_DMS_CLIPBOARD_SERVE"
const envMime = "_DMS_CLIPBOARD_MIME"
const envPasteOnce = "_DMS_CLIPBOARD_PASTE_ONCE"
const envCacheFile = "_DMS_CLIPBOARD_CACHE"
// MaybeServeAndExit intercepts before cobra when re-exec'd as a clipboard
// child. Reads source data into memory, deletes any cache file, then serves.
func MaybeServeAndExit() {
if os.Getenv(envServe) == "" {
return
}
mimeType := os.Getenv(envMime)
pasteOnce := os.Getenv(envPasteOnce) == "1"
cachePath := os.Getenv(envCacheFile)
var data []byte
var err error
switch {
case cachePath != "":
data, err = os.ReadFile(cachePath)
os.Remove(cachePath)
default:
data, err = io.ReadAll(os.Stdin)
}
if err != nil {
fmt.Fprintf(os.Stderr, "clipboard: read source: %v\n", err)
os.Exit(1)
}
if err := serveClipboard(data, mimeType, pasteOnce); err != nil {
fmt.Fprintf(os.Stderr, "clipboard: serve: %v\n", err)
os.Exit(1)
}
os.Exit(0)
}
func Copy(data []byte, mimeType string) error { func Copy(data []byte, mimeType string) error {
return copyForkCached(data, mimeType, false) return CopyReader(bytes.NewReader(data), mimeType, false, false)
} }
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error { func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
if foreground { if foreground {
return serveClipboard(data, mimeType, pasteOnce) return copyServeWithWriter(func(writer io.Writer) error {
total := 0
for total < len(data) {
n, err := writer.Write(data[total:])
total += n
if err != nil {
return err
}
}
if total != len(data) {
return io.ErrShortWrite
}
return nil
}, mimeType, pasteOnce)
} }
return copyForkCached(data, mimeType, pasteOnce) return CopyReader(bytes.NewReader(data), mimeType, foreground, pasteOnce)
} }
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error { func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
if foreground { if !foreground {
buf, err := io.ReadAll(data) return copyFork(data, mimeType, pasteOnce)
if err != nil {
return fmt.Errorf("read source: %w", err)
}
return serveClipboard(buf, mimeType, pasteOnce)
} }
return copyFork(data, mimeType, pasteOnce) return copyServeReader(data, mimeType, pasteOnce)
} }
func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd { func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
cmd := exec.Command(os.Args[0]) args := []string{os.Args[0], "cl", "copy", "--foreground"}
if pasteOnce {
args = append(args, "--paste-once")
}
args = append(args, "--type", mimeType)
cmd := exec.Command(args[0], args[1:]...)
cmd.Stderr = nil cmd.Stderr = nil
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
cmd.Env = append(os.Environ(), cmd.Env = append(os.Environ(), "DMS_CLIP_FORKED=1")
envServe+"=1",
envMime+"="+mimeType,
)
if pasteOnce {
cmd.Env = append(cmd.Env, envPasteOnce+"=1")
}
cmd.Env = append(cmd.Env, extra...)
return cmd
}
func waitReady(cmd *exec.Cmd) error {
stdout, err := cmd.StdoutPipe() stdout, err := cmd.StdoutPipe()
if err != nil { if err != nil {
return fmt.Errorf("stdout pipe: %w", err) return fmt.Errorf("stdout pipe: %w", err)
} }
if err := cmd.Start(); err != nil {
return fmt.Errorf("start: %w", err)
}
var buf [1]byte
if _, err := stdout.Read(buf[:]); err != nil {
return fmt.Errorf("waiting for clipboard ready: %w", err)
}
return nil
}
func copyForkCached(data []byte, mimeType string, pasteOnce bool) error {
cacheFile, err := createClipboardCacheFile()
if err != nil {
return fmt.Errorf("create cache file: %w", err)
}
cachePath := cacheFile.Name()
if _, err := cacheFile.Write(data); err != nil {
cacheFile.Close()
os.Remove(cachePath)
return fmt.Errorf("write cache file: %w", err)
}
if err := cacheFile.Close(); err != nil {
os.Remove(cachePath)
return fmt.Errorf("close cache file: %w", err)
}
cmd := newForkCmd(mimeType, pasteOnce, envCacheFile+"="+cachePath)
cmd.Stdin = nil
if err := waitReady(cmd); err != nil {
os.Remove(cachePath)
return err
}
return nil
}
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
cmd := newForkCmd(mimeType, pasteOnce)
switch src := data.(type) { switch src := data.(type) {
case *os.File: case *os.File:
cmd.Stdin = src cmd.Stdin = src
return waitReady(cmd) if err := cmd.Start(); err != nil {
return fmt.Errorf("start: %w", err)
}
default: default:
stdin, err := cmd.StdinPipe() stdin, err := cmd.StdinPipe()
if err != nil { if err != nil {
return fmt.Errorf("stdin pipe: %w", err) return fmt.Errorf("stdin pipe: %w", err)
} }
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("stdout pipe: %w", err)
}
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
return fmt.Errorf("start: %w", err) return fmt.Errorf("start: %w", err)
} }
@@ -158,22 +83,50 @@ func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
if err := stdin.Close(); err != nil { if err := stdin.Close(); err != nil {
return fmt.Errorf("close stdin: %w", err) return fmt.Errorf("close stdin: %w", err)
} }
var buf [1]byte
if _, err := stdout.Read(buf[:]); err != nil {
return fmt.Errorf("waiting for clipboard ready: %w", err)
}
return nil
} }
var buf [1]byte
if _, err := stdout.Read(buf[:]); err != nil {
return fmt.Errorf("waiting for clipboard ready: %w", err)
}
return nil
} }
func signalReady() { func signalReady() {
if os.Getenv(envServe) == "" { if os.Getenv("DMS_CLIP_FORKED") == "" {
return return
} }
os.Stdout.Write([]byte{1}) os.Stdout.Write([]byte{1})
} }
func copyServeReader(data io.Reader, mimeType string, pasteOnce bool) error {
cachedData, err := createClipboardCacheFile()
if err != nil {
return fmt.Errorf("create clipboard cache file: %w", err)
}
defer os.Remove(cachedData.Name())
if _, err := io.Copy(cachedData, data); err != nil {
return fmt.Errorf("cache clipboard data: %w", err)
}
if err := cachedData.Close(); err != nil {
return fmt.Errorf("close temp cache file: %w", err)
}
return copyServeWithWriter(func(writer io.Writer) error {
cachedFile, err := os.Open(cachedData.Name())
if err != nil {
return fmt.Errorf("open temp cache file: %w", err)
}
defer cachedFile.Close()
if _, err := io.Copy(writer, cachedFile); err != nil {
return fmt.Errorf("write clipboard data: %w", err)
}
return nil
}, mimeType, pasteOnce)
}
func createClipboardCacheFile() (*os.File, error) { func createClipboardCacheFile() (*os.File, error) {
preferredDirs := []string{} preferredDirs := []string{}
@@ -194,7 +147,7 @@ func createClipboardCacheFile() (*os.File, error) {
return os.CreateTemp("", "dms-clipboard-*") return os.CreateTemp("", "dms-clipboard-*")
} }
func serveClipboard(data []byte, mimeType string, pasteOnce bool) error { func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOnce bool) error {
display, err := wlclient.Connect("") display, err := wlclient.Connect("")
if err != nil { if err != nil {
return fmt.Errorf("wayland connect: %w", err) return fmt.Errorf("wayland connect: %w", err)
@@ -236,10 +189,12 @@ func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
if bindErr != nil { if bindErr != nil {
return fmt.Errorf("registry bind: %w", bindErr) return fmt.Errorf("registry bind: %w", bindErr)
} }
if dataControlMgr == nil { if dataControlMgr == nil {
return fmt.Errorf("compositor does not support ext_data_control_manager_v1") return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
} }
defer dataControlMgr.Destroy() defer dataControlMgr.Destroy()
if seat == nil { if seat == nil {
return fmt.Errorf("no seat available") return fmt.Errorf("no seat available")
} }
@@ -278,12 +233,18 @@ func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
cancelled := make(chan struct{}) cancelled := make(chan struct{})
pasted := make(chan struct{}, 1) pasted := make(chan struct{}, 1)
sendErr := make(chan error, 1)
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) { source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
_ = syscall.SetNonblock(e.Fd, false) defer syscall.Close(e.Fd)
file := os.NewFile(uintptr(e.Fd), "pipe") file := os.NewFile(uintptr(e.Fd), "pipe")
defer file.Close() defer file.Close()
_, _ = file.Write(data) if err := writeTo(file); err != nil {
select {
case sendErr <- err:
default:
}
}
select { select {
case pasted <- struct{}{}: case pasted <- struct{}{}:
default: default:
@@ -305,6 +266,8 @@ func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
select { select {
case <-cancelled: case <-cancelled:
return nil return nil
case err := <-sendErr:
return err
case <-pasted: case <-pasted:
if pasteOnce { if pasteOnce {
return nil return nil
@@ -558,10 +521,12 @@ func copyMultiServe(offers []Offer, pasteOnce bool) error {
if bindErr != nil { if bindErr != nil {
return fmt.Errorf("registry bind: %w", bindErr) return fmt.Errorf("registry bind: %w", bindErr)
} }
if dataControlMgr == nil { if dataControlMgr == nil {
return fmt.Errorf("compositor does not support ext_data_control_manager_v1") return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
} }
defer dataControlMgr.Destroy() defer dataControlMgr.Destroy()
if seat == nil { if seat == nil {
return fmt.Errorf("no seat available") return fmt.Errorf("no seat available")
} }
@@ -589,12 +554,12 @@ func copyMultiServe(offers []Offer, pasteOnce bool) error {
pasted := make(chan struct{}, 1) pasted := make(chan struct{}, 1)
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) { source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
_ = syscall.SetNonblock(e.Fd, false) defer syscall.Close(e.Fd)
file := os.NewFile(uintptr(e.Fd), "pipe") file := os.NewFile(uintptr(e.Fd), "pipe")
defer file.Close() defer file.Close()
if data, ok := offerMap[e.MimeType]; ok { if data, ok := offerMap[e.MimeType]; ok {
_, _ = file.Write(data) file.Write(data)
} }
select { select {
+50 -53
View File
@@ -39,10 +39,11 @@ type LayerSurface struct {
wlSurface *client.Surface wlSurface *client.Surface
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1 layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
viewport *wp_viewporter.WpViewport viewport *wp_viewporter.WpViewport
wlPools [2]*client.ShmPool wlPool *client.ShmPool
wlBuffers [2]*client.Buffer wlBuffer *client.Buffer
slotBusy [2]bool bufferBusy bool
needsRedraw bool oldPool *client.ShmPool
oldBuffer *client.Buffer
scopyBuffer *client.Buffer scopyBuffer *client.Buffer
configured bool configured bool
hidden bool hidden bool
@@ -135,7 +136,6 @@ func (p *Picker) Run() (*Color, error) {
break break
} }
p.flushRedraws()
p.checkDone() p.checkDone()
} }
@@ -164,15 +164,6 @@ func (p *Picker) checkDone() {
} }
} }
func (p *Picker) flushRedraws() {
for _, ls := range p.surfaces {
if !ls.needsRedraw {
continue
}
p.redrawSurface(ls)
}
}
func (p *Picker) connect() error { func (p *Picker) connect() error {
display, err := client.Connect("") display, err := client.Connect("")
if err != nil { if err != nil {
@@ -516,45 +507,47 @@ func (p *Picker) captureForSurface(ls *LayerSurface) {
} }
func (p *Picker) redrawSurface(ls *LayerSurface) { func (p *Picker) redrawSurface(ls *LayerSurface) {
slot := ls.state.FrontIndex()
if ls.slotBusy[slot] {
ls.needsRedraw = true
return
}
var renderBuf *ShmBuffer var renderBuf *ShmBuffer
switch { if ls.hidden {
case ls.hidden:
renderBuf = ls.state.RedrawScreenOnly() renderBuf = ls.state.RedrawScreenOnly()
default: } else {
renderBuf = ls.state.Redraw() renderBuf = ls.state.Redraw()
} }
if renderBuf == nil { if renderBuf == nil {
return return
} }
ls.needsRedraw = false if ls.oldBuffer != nil {
ls.oldBuffer.Destroy()
if ls.wlPools[slot] == nil { ls.oldBuffer = nil
pool, err := p.shm.CreatePool(renderBuf.Fd(), int32(renderBuf.Size())) }
if err != nil { if ls.oldPool != nil {
return ls.oldPool.Destroy()
} ls.oldPool = nil
ls.wlPools[slot] = pool
wlBuffer, err := pool.CreateBuffer(0, int32(renderBuf.Width), int32(renderBuf.Height), int32(renderBuf.Stride), uint32(ls.state.ScreenFormat()))
if err != nil {
return
}
ls.wlBuffers[slot] = wlBuffer
s := slot
wlBuffer.SetReleaseHandler(func(e client.BufferReleaseEvent) {
ls.slotBusy[s] = false
})
} }
ls.slotBusy[slot] = true ls.oldPool = ls.wlPool
ls.oldBuffer = ls.wlBuffer
ls.wlPool = nil
ls.wlBuffer = nil
pool, err := p.shm.CreatePool(renderBuf.Fd(), int32(renderBuf.Size()))
if err != nil {
return
}
ls.wlPool = pool
wlBuffer, err := pool.CreateBuffer(0, int32(renderBuf.Width), int32(renderBuf.Height), int32(renderBuf.Stride), uint32(ls.state.ScreenFormat()))
if err != nil {
return
}
ls.wlBuffer = wlBuffer
lsRef := ls
wlBuffer.SetReleaseHandler(func(e client.BufferReleaseEvent) {
lsRef.bufferBusy = false
})
ls.bufferBusy = true
logicalW, logicalH := ls.state.LogicalSize() logicalW, logicalH := ls.state.LogicalSize()
if logicalW == 0 || logicalH == 0 { if logicalW == 0 || logicalH == 0 {
@@ -573,7 +566,7 @@ func (p *Picker) redrawSurface(ls *LayerSurface) {
} }
_ = ls.wlSurface.SetBufferScale(bufferScale) _ = ls.wlSurface.SetBufferScale(bufferScale)
} }
_ = ls.wlSurface.Attach(ls.wlBuffers[slot], 0, 0) _ = ls.wlSurface.Attach(wlBuffer, 0, 0)
_ = ls.wlSurface.Damage(0, 0, int32(logicalW), int32(logicalH)) _ = ls.wlSurface.Damage(0, 0, int32(logicalW), int32(logicalH))
_ = ls.wlSurface.Commit() _ = ls.wlSurface.Commit()
@@ -641,7 +634,7 @@ func (p *Picker) setupPointerHandlers() {
} }
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY) p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
p.activeSurface.needsRedraw = true p.redrawSurface(p.activeSurface)
}) })
p.pointer.SetLeaveHandler(func(e client.PointerLeaveEvent) { p.pointer.SetLeaveHandler(func(e client.PointerLeaveEvent) {
@@ -662,7 +655,7 @@ func (p *Picker) setupPointerHandlers() {
return return
} }
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY) p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
p.activeSurface.needsRedraw = true p.redrawSurface(p.activeSurface)
}) })
p.pointer.SetButtonHandler(func(e client.PointerButtonEvent) { p.pointer.SetButtonHandler(func(e client.PointerButtonEvent) {
@@ -686,13 +679,17 @@ func (p *Picker) cleanup() {
if ls.scopyBuffer != nil { if ls.scopyBuffer != nil {
ls.scopyBuffer.Destroy() ls.scopyBuffer.Destroy()
} }
for i := range ls.wlBuffers { if ls.oldBuffer != nil {
if ls.wlBuffers[i] != nil { ls.oldBuffer.Destroy()
ls.wlBuffers[i].Destroy() }
} if ls.oldPool != nil {
if ls.wlPools[i] != nil { ls.oldPool.Destroy()
ls.wlPools[i].Destroy() }
} if ls.wlBuffer != nil {
ls.wlBuffer.Destroy()
}
if ls.wlPool != nil {
ls.wlPool.Destroy()
} }
if ls.viewport != nil { if ls.viewport != nil {
ls.viewport.Destroy() ls.viewport.Destroy()
-6
View File
@@ -274,12 +274,6 @@ func (s *SurfaceState) FrontRenderBuffer() *ShmBuffer {
return s.renderBufs[s.front] return s.renderBufs[s.front]
} }
func (s *SurfaceState) FrontIndex() int {
s.mu.Lock()
defer s.mu.Unlock()
return s.front
}
func (s *SurfaceState) SwapBuffers() { func (s *SurfaceState) SwapBuffers() {
s.mu.Lock() s.mu.Lock()
s.front ^= 1 s.front ^= 1
+1 -20
View File
@@ -62,31 +62,12 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) { func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
var results []DeploymentResult var results []DeploymentResult
// Primary config file paths used to detect fresh installs.
configPrimaryPaths := map[string]string{
"Niri": filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"),
"Hyprland": filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"),
"Ghostty": filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config"),
"Kitty": filepath.Join(os.Getenv("HOME"), ".config", "kitty", "kitty.conf"),
"Alacritty": filepath.Join(os.Getenv("HOME"), ".config", "alacritty", "alacritty.toml"),
}
shouldReplaceConfig := func(configType string) bool { shouldReplaceConfig := func(configType string) bool {
if replaceConfigs == nil { if replaceConfigs == nil {
return true return true
} }
replace, exists := replaceConfigs[configType] replace, exists := replaceConfigs[configType]
if !exists || replace { return !exists || replace
return true
}
// Config is explicitly set to "don't replace" — but still deploy
// if the config file doesn't exist yet (fresh install scenario).
if primaryPath, ok := configPrimaryPaths[configType]; ok {
if _, err := os.Stat(primaryPath); os.IsNotExist(err) {
return true
}
}
return false
} }
switch wm { switch wm {
-166
View File
@@ -1,7 +1,6 @@
package config package config
import ( import (
"context"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
@@ -625,168 +624,3 @@ func TestAlacrittyConfigDeployment(t *testing.T) {
assert.Contains(t, string(newContent), "decorations = \"None\"") assert.Contains(t, string(newContent), "decorations = \"None\"")
}) })
} }
func TestShouldReplaceConfigDeployIfMissing(t *testing.T) {
allFalse := map[string]bool{
"Niri": false,
"Hyprland": false,
"Ghostty": false,
"Kitty": false,
"Alacritty": false,
}
t.Run("replaceConfigs nil deploys config", func(t *testing.T) {
tempDir, err := os.MkdirTemp("", "dankinstall-replace-nil-test")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
originalHome := os.Getenv("HOME")
os.Setenv("HOME", tempDir)
defer os.Setenv("HOME", originalHome)
logChan := make(chan string, 100)
cd := NewConfigDeployer(logChan)
results, err := cd.DeployConfigurationsSelectiveWithReinstalls(
context.Background(),
deps.WindowManagerNiri,
deps.TerminalGhostty,
nil, // installedDeps
nil, // replaceConfigs
nil, // reinstallItems
)
require.NoError(t, err)
// With replaceConfigs=nil, all configs should be deployed
hasDeployed := false
for _, r := range results {
if r.Deployed {
hasDeployed = true
break
}
}
assert.True(t, hasDeployed, "expected at least one config to be deployed when replaceConfigs is nil")
})
t.Run("replaceConfigs all false and config missing deploys config", func(t *testing.T) {
tempDir, err := os.MkdirTemp("", "dankinstall-replace-missing-test")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
originalHome := os.Getenv("HOME")
os.Setenv("HOME", tempDir)
defer os.Setenv("HOME", originalHome)
logChan := make(chan string, 100)
cd := NewConfigDeployer(logChan)
results, err := cd.DeployConfigurationsSelectiveWithReinstalls(
context.Background(),
deps.WindowManagerNiri,
deps.TerminalGhostty,
nil, // installedDeps
allFalse, // replaceConfigs — all false
nil, // reinstallItems
)
require.NoError(t, err)
// Config files don't exist on disk, so they should still be deployed
hasDeployed := false
for _, r := range results {
if r.Deployed {
hasDeployed = true
break
}
}
assert.True(t, hasDeployed, "expected configs to be deployed when files are missing, even with replaceConfigs all false")
})
t.Run("replaceConfigs false and config exists skips config", func(t *testing.T) {
tempDir, err := os.MkdirTemp("", "dankinstall-replace-exists-test")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
originalHome := os.Getenv("HOME")
os.Setenv("HOME", tempDir)
defer os.Setenv("HOME", originalHome)
// Create the Ghostty primary config file so shouldReplaceConfig returns false
ghosttyPath := filepath.Join(tempDir, ".config", "ghostty", "config")
err = os.MkdirAll(filepath.Dir(ghosttyPath), 0o755)
require.NoError(t, err)
err = os.WriteFile(ghosttyPath, []byte("# existing ghostty config\n"), 0o644)
require.NoError(t, err)
// Also create the Niri primary config file
niriPath := filepath.Join(tempDir, ".config", "niri", "config.kdl")
err = os.MkdirAll(filepath.Dir(niriPath), 0o755)
require.NoError(t, err)
err = os.WriteFile(niriPath, []byte("// existing niri config\n"), 0o644)
require.NoError(t, err)
logChan := make(chan string, 100)
cd := NewConfigDeployer(logChan)
results, err := cd.DeployConfigurationsSelectiveWithReinstalls(
context.Background(),
deps.WindowManagerNiri,
deps.TerminalGhostty,
nil, // installedDeps
allFalse, // replaceConfigs — all false
nil, // reinstallItems
)
require.NoError(t, err)
// Both Niri and Ghostty config files exist, so with all false they should be skipped
for _, r := range results {
assert.Fail(t, "expected no configs to be deployed", "got deployed config: %s", r.ConfigType)
}
})
t.Run("replaceConfigs true and config exists deploys config", func(t *testing.T) {
tempDir, err := os.MkdirTemp("", "dankinstall-replace-true-test")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
originalHome := os.Getenv("HOME")
os.Setenv("HOME", tempDir)
defer os.Setenv("HOME", originalHome)
// Create the Ghostty primary config file
ghosttyPath := filepath.Join(tempDir, ".config", "ghostty", "config")
err = os.MkdirAll(filepath.Dir(ghosttyPath), 0o755)
require.NoError(t, err)
err = os.WriteFile(ghosttyPath, []byte("# existing ghostty config\n"), 0o644)
require.NoError(t, err)
logChan := make(chan string, 100)
cd := NewConfigDeployer(logChan)
replaceConfigs := map[string]bool{
"Niri": false,
"Hyprland": false,
"Ghostty": true, // explicitly true
"Kitty": false,
"Alacritty": false,
}
results, err := cd.DeployConfigurationsSelectiveWithReinstalls(
context.Background(),
deps.WindowManagerNiri,
deps.TerminalGhostty,
nil, // installedDeps
replaceConfigs, // Ghostty=true, rest=false
nil, // reinstallItems
)
require.NoError(t, err)
// Ghostty should be deployed because replaceConfigs["Ghostty"]=true
foundGhostty := false
for _, r := range results {
if r.ConfigType == "Ghostty" && r.Deployed {
foundGhostty = true
}
}
assert.True(t, foundGhostty, "expected Ghostty config to be deployed when replaceConfigs is true")
})
}
@@ -137,7 +137,7 @@ bind = SUPER, bracketright, layoutmsg, preselect r
# === Sizing & Layout === # === Sizing & Layout ===
bind = SUPER, R, layoutmsg, togglesplit bind = SUPER, R, layoutmsg, togglesplit
bind = SUPER CTRL, F, resizeactive, exact 100% 100% bind = SUPER CTRL, F, resizeactive, exact 100%
# === Move/resize windows with mainMod + LMB/RMB and dragging === # === Move/resize windows with mainMod + LMB/RMB and dragging ===
bindmd = SUPER, mouse:272, Move window, movewindow bindmd = SUPER, mouse:272, Move window, movewindow
@@ -94,7 +94,6 @@ windowrule = tile on, match:class ^(gnome-control-center)$
windowrule = tile on, match:class ^(pavucontrol)$ windowrule = tile on, match:class ^(pavucontrol)$
windowrule = tile on, match:class ^(nm-connection-editor)$ windowrule = tile on, match:class ^(nm-connection-editor)$
windowrule = float on, match:class ^(org\.gnome\.Calculator)$
windowrule = float on, match:class ^(gnome-calculator)$ windowrule = float on, match:class ^(gnome-calculator)$
windowrule = float on, match:class ^(galculator)$ windowrule = float on, match:class ^(galculator)$
windowrule = float on, match:class ^(blueman-manager)$ windowrule = float on, match:class ^(blueman-manager)$
-1
View File
@@ -224,7 +224,6 @@ window-rule {
open-floating false open-floating false
} }
window-rule { window-rule {
match app-id=r#"^org\.gnome\.Calculator$"#
match app-id=r#"^gnome-calculator$"# match app-id=r#"^gnome-calculator$"#
match app-id=r#"^galculator$"# match app-id=r#"^galculator$"#
match app-id=r#"^blueman-manager$"# match app-id=r#"^blueman-manager$"#
+51 -47
View File
@@ -242,7 +242,11 @@ func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMap
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR} return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
} }
return PackageMapping{Name: "dms-shell", Repository: RepoTypeSystem} if a.packageInstalled("dms-shell-bin") {
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
}
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
} }
func (a *ArchDistribution) detectXwaylandSatellite() deps.Dependency { func (a *ArchDistribution) detectXwaylandSatellite() deps.Dependency {
@@ -324,13 +328,6 @@ func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []d
systemPkgs, aurPkgs, manualPkgs, variantMap := a.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags) systemPkgs, aurPkgs, manualPkgs, variantMap := a.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
if slices.Contains(aurPkgs, "quickshell-git") && slices.Contains(systemPkgs, "dms-shell") {
if err := a.preinstallQuickshellGit(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to preinstall quickshell-git: %w", err)
}
aurPkgs = slices.DeleteFunc(aurPkgs, func(p string) bool { return p == "quickshell-git" })
}
// Phase 3: System Packages // Phase 3: System Packages
if len(systemPkgs) > 0 { if len(systemPkgs) > 0 {
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
@@ -448,37 +445,6 @@ func (a *ArchDistribution) categorizePackages(dependencies []deps.Dependency, wm
return systemPkgs, aurPkgs, manualPkgs, variantMap return systemPkgs, aurPkgs, manualPkgs, variantMap
} }
func (a *ArchDistribution) preinstallQuickshellGit(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if a.packageInstalled("quickshell-git") {
return nil
}
if a.packageInstalled("quickshell") {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.15,
Step: "Removing stable quickshell...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: "sudo pacman -Rdd --noconfirm quickshell",
LogOutput: "Removing stable quickshell so quickshell-git can be installed",
}
cmd := ExecSudoCommand(ctx, sudoPassword, "pacman -Rdd --noconfirm quickshell")
if err := a.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.15, 0.18); err != nil {
return fmt.Errorf("failed to remove stable quickshell: %w", err)
}
}
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.18,
Step: "Building quickshell-git before system packages...",
IsComplete: false,
CommandInfo: "Installing quickshell-git ahead of dms-shell to avoid conflict",
}
return a.installSingleAURPackage(ctx, "quickshell-git", sudoPassword, progressChan, 0.18, 0.32)
}
func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
if len(packages) == 0 { if len(packages) == 0 {
return nil return nil
@@ -487,9 +453,6 @@ func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages [
a.log(fmt.Sprintf("Installing system packages: %s", strings.Join(packages, ", "))) a.log(fmt.Sprintf("Installing system packages: %s", strings.Join(packages, ", ")))
args := []string{"pacman", "-S", "--needed", "--noconfirm"} args := []string{"pacman", "-S", "--needed", "--noconfirm"}
if slices.Contains(packages, "dms-shell") {
args = append(args, "--assume-installed", "dms-shell-compositor=1")
}
args = append(args, packages...) args = append(args, packages...)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
@@ -577,7 +540,7 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
var dmsShell []string var dmsShell []string
for _, pkg := range packages { for _, pkg := range packages {
if pkg == "dms-shell-git" { if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
dmsShell = append(dmsShell, pkg) dmsShell = append(dmsShell, pkg)
} else { } else {
isDep := false isDep := false
@@ -658,7 +621,7 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
} }
} }
if pkg == "dms-shell-git" { if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
srcinfoPath := filepath.Join(packageDir, ".SRCINFO") srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
depsToRemove := []string{ depsToRemove := []string{
"depends = quickshell", "depends = quickshell",
@@ -681,7 +644,15 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
} }
srcinfoPath = filepath.Join(packageDir, ".SRCINFO") srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
{ if pkg == "dms-shell-bin" {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.35*(endProgress-startProgress),
Step: fmt.Sprintf("Skipping dependency installation for %s (manually managed)...", pkg),
IsComplete: false,
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
}
} else {
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages, Phase: PhaseAURPackages,
Progress: startProgress + 0.3*(endProgress-startProgress), Progress: startProgress + 0.3*(endProgress-startProgress),
@@ -768,9 +739,42 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
CommandInfo: "sudo pacman -U built-package", CommandInfo: "sudo pacman -U built-package",
} }
// Find .pkg.tar* files - for split packages, install the base and any installed compositor variants
var files []string var files []string
matches, _ := filepath.Glob(filepath.Join(packageDir, "*.pkg.tar*")) if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
files = matches // For DMS split packages, install base package
pattern := filepath.Join(packageDir, fmt.Sprintf("%s-%s*.pkg.tar*", pkg, "*"))
matches, err := filepath.Glob(pattern)
if err == nil {
for _, match := range matches {
basename := filepath.Base(match)
// Always include base package
if !strings.Contains(basename, "hyprland") && !strings.Contains(basename, "niri") {
files = append(files, match)
}
}
}
// Also update compositor-specific packages if they're installed
if strings.HasSuffix(pkg, "-git") {
if a.packageInstalled("dms-shell-hyprland-git") {
hyprlandPattern := filepath.Join(packageDir, "dms-shell-hyprland-git-*.pkg.tar*")
if hyprlandMatches, err := filepath.Glob(hyprlandPattern); err == nil && len(hyprlandMatches) > 0 {
files = append(files, hyprlandMatches[0])
}
}
if a.packageInstalled("dms-shell-niri-git") {
niriPattern := filepath.Join(packageDir, "dms-shell-niri-git-*.pkg.tar*")
if niriMatches, err := filepath.Glob(niriPattern); err == nil && len(niriMatches) > 0 {
files = append(files, niriMatches[0])
}
}
}
} else {
// For other packages, install all built packages
matches, _ := filepath.Glob(filepath.Join(packageDir, "*.pkg.tar*"))
files = matches
}
if len(files) == 0 { if len(files) == 0 {
return fmt.Errorf("no package files found after building %s", pkg) return fmt.Errorf("no package files found after building %s", pkg)
-418
View File
@@ -1,418 +0,0 @@
package headless
import (
"context"
"fmt"
"os"
"os/exec"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
)
// ErrConfirmationRequired is returned when --yes is not set and the user
// must explicitly confirm the operation.
var ErrConfirmationRequired = fmt.Errorf("confirmation required: pass --yes to proceed")
// validConfigNames maps lowercase CLI input to the deployer key used in
// replaceConfigs. Keep in sync with the config types checked by
// shouldReplaceConfig in deployer.go.
var validConfigNames = map[string]string{
"niri": "Niri",
"hyprland": "Hyprland",
"ghostty": "Ghostty",
"kitty": "Kitty",
"alacritty": "Alacritty",
}
// orderedConfigNames defines the canonical order for config names in output.
// Must be kept in sync with validConfigNames.
var orderedConfigNames = []string{"niri", "hyprland", "ghostty", "kitty", "alacritty"}
// Config holds all CLI parameters for unattended installation.
type Config struct {
Compositor string // "niri" or "hyprland"
Terminal string // "ghostty", "kitty", or "alacritty"
IncludeDeps []string
ExcludeDeps []string
ReplaceConfigs []string // specific configs to deploy (e.g. "niri", "ghostty")
ReplaceConfigsAll bool // deploy/replace all configurations
Yes bool
}
// Runner orchestrates unattended (headless) installation.
type Runner struct {
cfg Config
logChan chan string
}
// NewRunner creates a new headless runner.
func NewRunner(cfg Config) *Runner {
return &Runner{
cfg: cfg,
logChan: make(chan string, 1000),
}
}
// GetLogChan returns the log channel for file logging.
func (r *Runner) GetLogChan() <-chan string {
return r.logChan
}
// Run executes the full unattended installation flow.
func (r *Runner) Run() error {
r.log("Starting headless installation")
// 1. Parse compositor and terminal selections
wm, err := r.parseWindowManager()
if err != nil {
return err
}
terminal, err := r.parseTerminal()
if err != nil {
return err
}
// 2. Build replace-configs map
replaceConfigs, err := r.buildReplaceConfigs()
if err != nil {
return err
}
// 3. Detect OS
r.log("Detecting operating system...")
osInfo, err := distros.GetOSInfo()
if err != nil {
return fmt.Errorf("OS detection failed: %w", err)
}
if distros.IsUnsupportedDistro(osInfo.Distribution.ID, osInfo.VersionID) {
return fmt.Errorf("unsupported distribution: %s %s", osInfo.PrettyName, osInfo.VersionID)
}
fmt.Fprintf(os.Stdout, "Detected: %s (%s)\n", osInfo.PrettyName, osInfo.Architecture)
// 4. Create distribution instance
distro, err := distros.NewDistribution(osInfo.Distribution.ID, r.logChan)
if err != nil {
return fmt.Errorf("failed to initialize distribution: %w", err)
}
// 5. Detect dependencies
r.log("Detecting dependencies...")
fmt.Fprintln(os.Stdout, "Detecting dependencies...")
dependencies, err := distro.DetectDependenciesWithTerminal(context.Background(), wm, terminal)
if err != nil {
return fmt.Errorf("dependency detection failed: %w", err)
}
// 5. Apply include/exclude filters and build the disabled-items map.
// Headless mode does not currently collect any explicit reinstall selections,
// so keep reinstallItems nil instead of constructing an always-empty map.
disabledItems, err := r.buildDisabledItems(dependencies)
if err != nil {
return err
}
var reinstallItems map[string]bool
// Print dependency summary
fmt.Fprintln(os.Stdout, "\nDependencies:")
for _, dep := range dependencies {
marker := " "
status := ""
if disabledItems[dep.Name] {
marker = " SKIP "
status = "(disabled)"
} else {
switch dep.Status {
case deps.StatusInstalled:
marker = " OK "
status = "(installed)"
case deps.StatusMissing:
marker = " NEW "
status = "(will install)"
case deps.StatusNeedsUpdate:
marker = " UPD "
status = "(will update)"
case deps.StatusNeedsReinstall:
marker = " RE "
status = "(will reinstall)"
}
}
fmt.Fprintf(os.Stdout, "%s%-30s %s\n", marker, dep.Name, status)
}
fmt.Fprintln(os.Stdout)
// 6b. Require explicit confirmation unless --yes is set
if !r.cfg.Yes {
if replaceConfigs == nil {
// --replace-configs-all
fmt.Fprintln(os.Stdout, "Packages will be installed and all configurations will be replaced.")
fmt.Fprintln(os.Stdout, "Existing config files will be backed up before replacement.")
} else if r.anyConfigEnabled(replaceConfigs) {
var names []string
for _, cliName := range orderedConfigNames {
deployerKey := validConfigNames[cliName]
if replaceConfigs[deployerKey] {
names = append(names, deployerKey)
}
}
fmt.Fprintf(os.Stdout, "Packages will be installed. The following configurations will be replaced (with backups): %s\n", strings.Join(names, ", "))
} else {
fmt.Fprintln(os.Stdout, "Packages will be installed. No configurations will be deployed.")
}
fmt.Fprintln(os.Stdout, "Re-run with --yes (-y) to proceed.")
r.log("Aborted: --yes not set")
return ErrConfirmationRequired
}
// 7. Authenticate sudo
sudoPassword, err := r.resolveSudoPassword()
if err != nil {
return err
}
// 8. Install packages
fmt.Fprintln(os.Stdout, "Installing packages...")
r.log("Starting package installation")
progressChan := make(chan distros.InstallProgressMsg, 100)
installErr := make(chan error, 1)
go func() {
defer close(progressChan)
installErr <- distro.InstallPackages(
context.Background(),
dependencies,
wm,
sudoPassword,
reinstallItems,
disabledItems,
false, // skipGlobalUseFlags
progressChan,
)
}()
// Consume progress messages and print them
for msg := range progressChan {
if msg.Error != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", msg.Error)
} else if msg.Step != "" {
fmt.Fprintf(os.Stdout, " [%3.0f%%] %s\n", msg.Progress*100, msg.Step)
}
if msg.LogOutput != "" {
r.log(msg.LogOutput)
fmt.Fprintf(os.Stdout, " %s\n", msg.LogOutput)
}
}
if err := <-installErr; err != nil {
return fmt.Errorf("package installation failed: %w", err)
}
// 9. Greeter setup (if dms-greeter was included)
if !disabledItems["dms-greeter"] && r.depExists(dependencies, "dms-greeter") {
compositorName := "niri"
if wm == deps.WindowManagerHyprland {
compositorName = "Hyprland"
}
fmt.Fprintln(os.Stdout, "Configuring DMS greeter...")
logFunc := func(line string) {
r.log(line)
fmt.Fprintf(os.Stdout, " greeter: %s\n", line)
}
if err := greeter.AutoSetupGreeter(compositorName, sudoPassword, logFunc); err != nil {
// Non-fatal, matching TUI behavior
fmt.Fprintf(os.Stderr, "Warning: greeter setup issue (non-fatal): %v\n", err)
}
}
// 10. Deploy configurations
fmt.Fprintln(os.Stdout, "Deploying configurations...")
r.log("Starting configuration deployment")
deployer := config.NewConfigDeployer(r.logChan)
results, err := deployer.DeployConfigurationsSelectiveWithReinstalls(
context.Background(),
wm,
terminal,
dependencies,
replaceConfigs,
reinstallItems,
)
if err != nil {
return fmt.Errorf("configuration deployment failed: %w", err)
}
for _, result := range results {
if result.Deployed {
msg := fmt.Sprintf(" Deployed: %s", result.ConfigType)
if result.BackupPath != "" {
msg += fmt.Sprintf(" (backup: %s)", result.BackupPath)
}
fmt.Fprintln(os.Stdout, msg)
}
if result.Error != nil {
fmt.Fprintf(os.Stderr, " Error deploying %s: %v\n", result.ConfigType, result.Error)
}
}
fmt.Fprintln(os.Stdout, "\nInstallation complete!")
r.log("Headless installation completed successfully")
return nil
}
// buildDisabledItems computes the set of dependencies that should be skipped
// during installation, applying the --include-deps and --exclude-deps filters.
// dms-greeter is disabled by default (opt-in), matching TUI behavior.
func (r *Runner) buildDisabledItems(dependencies []deps.Dependency) (map[string]bool, error) {
disabledItems := make(map[string]bool)
// dms-greeter is opt-in (disabled by default), matching TUI behavior
for i := range dependencies {
if dependencies[i].Name == "dms-greeter" {
disabledItems["dms-greeter"] = true
break
}
}
// Process --include-deps (enable items that are disabled by default)
for _, name := range r.cfg.IncludeDeps {
name = strings.TrimSpace(name)
if name == "" {
continue
}
if !r.depExists(dependencies, name) {
return nil, fmt.Errorf("--include-deps: unknown dependency %q", name)
}
delete(disabledItems, name)
}
// Process --exclude-deps (disable items)
for _, name := range r.cfg.ExcludeDeps {
name = strings.TrimSpace(name)
if name == "" {
continue
}
if !r.depExists(dependencies, name) {
return nil, fmt.Errorf("--exclude-deps: unknown dependency %q", name)
}
// Don't allow excluding DMS itself
if name == "dms (DankMaterialShell)" {
return nil, fmt.Errorf("--exclude-deps: cannot exclude required package %q", name)
}
disabledItems[name] = true
}
return disabledItems, nil
}
// buildReplaceConfigs converts the --replace-configs / --replace-configs-all
// flags into the map[string]bool consumed by the config deployer.
//
// Returns:
// - nil when --replace-configs-all is set (deployer treats nil as "replace all")
// - a map with all known configs set to false when neither flag is set (deploy only if config file is missing on disk)
// - a map with requested configs true, all others false for --replace-configs
// - an error when both flags are set or an invalid config name is given
func (r *Runner) buildReplaceConfigs() (map[string]bool, error) {
hasSpecific := len(r.cfg.ReplaceConfigs) > 0
if hasSpecific && r.cfg.ReplaceConfigsAll {
return nil, fmt.Errorf("--replace-configs and --replace-configs-all are mutually exclusive")
}
if r.cfg.ReplaceConfigsAll {
return nil, nil
}
// Build a map with all known configs explicitly set to false
result := make(map[string]bool, len(validConfigNames))
for _, cliName := range orderedConfigNames {
result[validConfigNames[cliName]] = false
}
// Enable only the requested configs
for _, name := range r.cfg.ReplaceConfigs {
name = strings.TrimSpace(name)
if name == "" {
continue
}
deployerKey, ok := validConfigNames[strings.ToLower(name)]
if !ok {
return nil, fmt.Errorf("--replace-configs: unknown config %q; valid values: niri, hyprland, ghostty, kitty, alacritty", name)
}
result[deployerKey] = true
}
return result, nil
}
func (r *Runner) log(message string) {
select {
case r.logChan <- message:
default:
}
}
func (r *Runner) parseWindowManager() (deps.WindowManager, error) {
switch strings.ToLower(r.cfg.Compositor) {
case "niri":
return deps.WindowManagerNiri, nil
case "hyprland":
return deps.WindowManagerHyprland, nil
default:
return 0, fmt.Errorf("invalid --compositor value %q: must be 'niri' or 'hyprland'", r.cfg.Compositor)
}
}
func (r *Runner) parseTerminal() (deps.Terminal, error) {
switch strings.ToLower(r.cfg.Terminal) {
case "ghostty":
return deps.TerminalGhostty, nil
case "kitty":
return deps.TerminalKitty, nil
case "alacritty":
return deps.TerminalAlacritty, nil
default:
return 0, fmt.Errorf("invalid --term value %q: must be 'ghostty', 'kitty', or 'alacritty'", r.cfg.Terminal)
}
}
func (r *Runner) resolveSudoPassword() (string, error) {
// Check if sudo credentials are cached (via sudo -v or NOPASSWD)
cmd := exec.Command("sudo", "-n", "true")
if err := cmd.Run(); err == nil {
r.log("sudo cache is valid, no password needed")
fmt.Fprintln(os.Stdout, "sudo: using cached credentials")
return "", nil
}
return "", fmt.Errorf(
"sudo authentication required but no cached credentials found\n" +
"Options:\n" +
" 1. Run 'sudo -v' before dankinstall to cache credentials\n" +
" 2. Configure passwordless sudo for your user",
)
}
func (r *Runner) anyConfigEnabled(m map[string]bool) bool {
for _, v := range m {
if v {
return true
}
}
return false
}
func (r *Runner) depExists(dependencies []deps.Dependency, name string) bool {
for _, dep := range dependencies {
if dep.Name == name {
return true
}
}
return false
}
-459
View File
@@ -1,459 +0,0 @@
package headless
import (
"strings"
"testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
)
func TestParseWindowManager(t *testing.T) {
tests := []struct {
name string
input string
want deps.WindowManager
wantErr bool
}{
{"niri lowercase", "niri", deps.WindowManagerNiri, false},
{"niri mixed case", "Niri", deps.WindowManagerNiri, false},
{"hyprland lowercase", "hyprland", deps.WindowManagerHyprland, false},
{"hyprland mixed case", "Hyprland", deps.WindowManagerHyprland, false},
{"invalid", "sway", 0, true},
{"empty", "", 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := NewRunner(Config{Compositor: tt.input})
got, err := r.parseWindowManager()
if (err != nil) != tt.wantErr {
t.Errorf("parseWindowManager() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && got != tt.want {
t.Errorf("parseWindowManager() = %v, want %v", got, tt.want)
}
})
}
}
func TestParseTerminal(t *testing.T) {
tests := []struct {
name string
input string
want deps.Terminal
wantErr bool
}{
{"ghostty lowercase", "ghostty", deps.TerminalGhostty, false},
{"ghostty mixed case", "Ghostty", deps.TerminalGhostty, false},
{"kitty lowercase", "kitty", deps.TerminalKitty, false},
{"alacritty lowercase", "alacritty", deps.TerminalAlacritty, false},
{"alacritty uppercase", "ALACRITTY", deps.TerminalAlacritty, false},
{"invalid", "wezterm", 0, true},
{"empty", "", 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := NewRunner(Config{Terminal: tt.input})
got, err := r.parseTerminal()
if (err != nil) != tt.wantErr {
t.Errorf("parseTerminal() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && got != tt.want {
t.Errorf("parseTerminal() = %v, want %v", got, tt.want)
}
})
}
}
func TestDepExists(t *testing.T) {
dependencies := []deps.Dependency{
{Name: "niri", Status: deps.StatusInstalled},
{Name: "ghostty", Status: deps.StatusMissing},
{Name: "dms (DankMaterialShell)", Status: deps.StatusInstalled},
{Name: "dms-greeter", Status: deps.StatusMissing},
}
tests := []struct {
name string
dep string
want bool
}{
{"existing dep", "niri", true},
{"existing dep with special chars", "dms (DankMaterialShell)", true},
{"existing optional dep", "dms-greeter", true},
{"non-existing dep", "firefox", false},
{"empty name", "", false},
}
r := NewRunner(Config{})
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := r.depExists(dependencies, tt.dep); got != tt.want {
t.Errorf("depExists(%q) = %v, want %v", tt.dep, got, tt.want)
}
})
}
}
func TestNewRunner(t *testing.T) {
cfg := Config{
Compositor: "niri",
Terminal: "ghostty",
IncludeDeps: []string{"dms-greeter"},
ExcludeDeps: []string{"some-pkg"},
Yes: true,
}
r := NewRunner(cfg)
if r == nil {
t.Fatal("NewRunner returned nil")
}
if r.cfg.Compositor != "niri" {
t.Errorf("cfg.Compositor = %q, want %q", r.cfg.Compositor, "niri")
}
if r.cfg.Terminal != "ghostty" {
t.Errorf("cfg.Terminal = %q, want %q", r.cfg.Terminal, "ghostty")
}
if !r.cfg.Yes {
t.Error("cfg.Yes = false, want true")
}
if r.logChan == nil {
t.Error("logChan is nil")
}
}
func TestGetLogChan(t *testing.T) {
r := NewRunner(Config{})
ch := r.GetLogChan()
if ch == nil {
t.Fatal("GetLogChan returned nil")
}
// Verify the channel is readable by sending a message
go func() {
r.logChan <- "test message"
}()
msg := <-ch
if msg != "test message" {
t.Errorf("received %q, want %q", msg, "test message")
}
}
func TestLog(t *testing.T) {
r := NewRunner(Config{})
// log should not block even if channel is full
for i := 0; i < 1100; i++ {
r.log("message")
}
// If we reach here without hanging, the non-blocking send works
}
func TestRunRequiresYes(t *testing.T) {
// Verify that ErrConfirmationRequired is a distinct sentinel error
if ErrConfirmationRequired == nil {
t.Fatal("ErrConfirmationRequired should not be nil")
}
expected := "confirmation required: pass --yes to proceed"
if ErrConfirmationRequired.Error() != expected {
t.Errorf("ErrConfirmationRequired = %q, want %q", ErrConfirmationRequired.Error(), expected)
}
}
func TestConfigYesStoredCorrectly(t *testing.T) {
// Yes=false (default) should be stored
rNo := NewRunner(Config{Compositor: "niri", Terminal: "ghostty", Yes: false})
if rNo.cfg.Yes {
t.Error("cfg.Yes = true, want false")
}
// Yes=true should be stored
rYes := NewRunner(Config{Compositor: "niri", Terminal: "ghostty", Yes: true})
if !rYes.cfg.Yes {
t.Error("cfg.Yes = false, want true")
}
}
func TestValidConfigNamesCompleteness(t *testing.T) {
// orderedConfigNames and validConfigNames must stay in sync.
if len(orderedConfigNames) != len(validConfigNames) {
t.Fatalf("orderedConfigNames has %d entries but validConfigNames has %d",
len(orderedConfigNames), len(validConfigNames))
}
// Every entry in orderedConfigNames must exist in validConfigNames.
for _, name := range orderedConfigNames {
if _, ok := validConfigNames[name]; !ok {
t.Errorf("orderedConfigNames contains %q which is missing from validConfigNames", name)
}
}
// validConfigNames must have no extra keys not in orderedConfigNames.
ordered := make(map[string]bool, len(orderedConfigNames))
for _, name := range orderedConfigNames {
ordered[name] = true
}
for key := range validConfigNames {
if !ordered[key] {
t.Errorf("validConfigNames contains %q which is missing from orderedConfigNames", key)
}
}
}
func TestBuildReplaceConfigs(t *testing.T) {
allDeployerKeys := []string{"Niri", "Hyprland", "Ghostty", "Kitty", "Alacritty"}
tests := []struct {
name string
replaceConfigs []string
replaceAll bool
wantNil bool // expect nil (replace all)
wantEnabled []string // deployer keys that should be true
wantErr bool
}{
{
name: "neither flag set",
wantNil: false,
wantEnabled: nil, // all should be false
},
{
name: "replace-configs-all",
replaceAll: true,
wantNil: true,
},
{
name: "specific configs",
replaceConfigs: []string{"niri", "ghostty"},
wantNil: false,
wantEnabled: []string{"Niri", "Ghostty"},
},
{
name: "both flags set",
replaceConfigs: []string{"niri"},
replaceAll: true,
wantErr: true,
},
{
name: "invalid config name",
replaceConfigs: []string{"foo"},
wantErr: true,
},
{
name: "case insensitive",
replaceConfigs: []string{"NIRI", "Ghostty"},
wantNil: false,
wantEnabled: []string{"Niri", "Ghostty"},
},
{
name: "single config",
replaceConfigs: []string{"kitty"},
wantNil: false,
wantEnabled: []string{"Kitty"},
},
{
name: "whitespace entry",
replaceConfigs: []string{" ", "niri"},
wantNil: false,
wantEnabled: []string{"Niri"},
},
{
name: "duplicate entry",
replaceConfigs: []string{"niri", "niri"},
wantNil: false,
wantEnabled: []string{"Niri"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := NewRunner(Config{
ReplaceConfigs: tt.replaceConfigs,
ReplaceConfigsAll: tt.replaceAll,
})
got, err := r.buildReplaceConfigs()
if (err != nil) != tt.wantErr {
t.Fatalf("buildReplaceConfigs() error = %v, wantErr %v", err, tt.wantErr)
}
if tt.wantErr {
return
}
if tt.wantNil {
if got != nil {
t.Fatalf("buildReplaceConfigs() = %v, want nil", got)
}
return
}
if got == nil {
t.Fatal("buildReplaceConfigs() = nil, want non-nil map")
}
// All known deployer keys must be present
for _, key := range allDeployerKeys {
if _, exists := got[key]; !exists {
t.Errorf("missing deployer key %q in result map", key)
}
}
// Build enabled set for easy lookup
enabledSet := make(map[string]bool)
for _, k := range tt.wantEnabled {
enabledSet[k] = true
}
for _, key := range allDeployerKeys {
want := enabledSet[key]
if got[key] != want {
t.Errorf("replaceConfigs[%q] = %v, want %v", key, got[key], want)
}
}
})
}
}
func TestConfigReplaceConfigsStoredCorrectly(t *testing.T) {
r := NewRunner(Config{
Compositor: "niri",
Terminal: "ghostty",
ReplaceConfigs: []string{"niri", "ghostty"},
ReplaceConfigsAll: false,
})
if len(r.cfg.ReplaceConfigs) != 2 {
t.Errorf("len(ReplaceConfigs) = %d, want 2", len(r.cfg.ReplaceConfigs))
}
if r.cfg.ReplaceConfigsAll {
t.Error("ReplaceConfigsAll = true, want false")
}
r2 := NewRunner(Config{
Compositor: "niri",
Terminal: "ghostty",
ReplaceConfigsAll: true,
})
if !r2.cfg.ReplaceConfigsAll {
t.Error("ReplaceConfigsAll = false, want true")
}
if len(r2.cfg.ReplaceConfigs) != 0 {
t.Errorf("len(ReplaceConfigs) = %d, want 0", len(r2.cfg.ReplaceConfigs))
}
}
func TestBuildDisabledItems(t *testing.T) {
dependencies := []deps.Dependency{
{Name: "niri", Status: deps.StatusInstalled},
{Name: "ghostty", Status: deps.StatusMissing},
{Name: "dms (DankMaterialShell)", Status: deps.StatusInstalled},
{Name: "dms-greeter", Status: deps.StatusMissing},
{Name: "waybar", Status: deps.StatusMissing},
}
tests := []struct {
name string
includeDeps []string
excludeDeps []string
deps []deps.Dependency // nil means use the shared fixture
wantErr bool
errContains string // substring expected in error message
wantDisabled []string // dep names that should be in disabledItems
wantEnabled []string // dep names that should NOT be in disabledItems (extra check)
}{
{
name: "no flags set, dms-greeter disabled by default",
wantDisabled: []string{"dms-greeter"},
wantEnabled: []string{"niri", "ghostty", "waybar"},
},
{
name: "include dms-greeter enables it",
includeDeps: []string{"dms-greeter"},
wantEnabled: []string{"dms-greeter"},
},
{
name: "exclude a regular dep",
excludeDeps: []string{"waybar"},
wantDisabled: []string{"dms-greeter", "waybar"},
},
{
name: "include unknown dep returns error",
includeDeps: []string{"nonexistent"},
wantErr: true,
errContains: "--include-deps",
},
{
name: "exclude unknown dep returns error",
excludeDeps: []string{"nonexistent"},
wantErr: true,
errContains: "--exclude-deps",
},
{
name: "exclude DMS itself is forbidden",
excludeDeps: []string{"dms (DankMaterialShell)"},
wantErr: true,
errContains: "cannot exclude required package",
},
{
name: "include and exclude same dep",
includeDeps: []string{"dms-greeter"},
excludeDeps: []string{"dms-greeter"},
wantDisabled: []string{"dms-greeter"},
},
{
name: "whitespace entries are skipped",
includeDeps: []string{" ", "dms-greeter"},
wantEnabled: []string{"dms-greeter"},
},
{
name: "no dms-greeter in deps, nothing disabled by default",
deps: []deps.Dependency{
{Name: "niri", Status: deps.StatusInstalled},
},
wantEnabled: []string{"niri"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := NewRunner(Config{
IncludeDeps: tt.includeDeps,
ExcludeDeps: tt.excludeDeps,
})
d := tt.deps
if d == nil {
d = dependencies
}
got, err := r.buildDisabledItems(d)
if (err != nil) != tt.wantErr {
t.Fatalf("buildDisabledItems() error = %v, wantErr %v", err, tt.wantErr)
}
if tt.wantErr {
if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("error %q does not contain %q", err.Error(), tt.errContains)
}
return
}
if got == nil {
t.Fatal("buildDisabledItems() returned nil map, want non-nil")
}
// Check expected disabled items
for _, name := range tt.wantDisabled {
if !got[name] {
t.Errorf("expected %q to be disabled, but it is not", name)
}
}
// Check expected enabled items (should not be in the map or be false)
for _, name := range tt.wantEnabled {
if got[name] {
t.Errorf("expected %q to NOT be disabled, but it is", name)
}
}
// If wantDisabled is empty, the map should have length 0
if len(tt.wantDisabled) == 0 && len(got) != 0 {
t.Errorf("expected empty disabledItems map, got %v", got)
}
})
}
}
+1 -11
View File
@@ -4,7 +4,6 @@ import (
"bufio" "bufio"
"fmt" "fmt"
"os" "os"
"path/filepath"
"regexp" "regexp"
"sync" "sync"
"time" "time"
@@ -22,16 +21,7 @@ type FileLogger struct {
func NewFileLogger() (*FileLogger, error) { func NewFileLogger() (*FileLogger, error) {
timestamp := time.Now().Unix() timestamp := time.Now().Unix()
logPath := fmt.Sprintf("/tmp/dankinstall-%d.log", timestamp)
// Use DANKINSTALL_LOG_DIR if set, otherwise fall back to /tmp.
logDir := os.Getenv("DANKINSTALL_LOG_DIR")
if logDir == "" {
logDir = "/tmp"
}
if err := os.MkdirAll(logDir, 0o755); err != nil {
return nil, fmt.Errorf("failed to create log directory: %w", err)
}
logPath := filepath.Join(logDir, fmt.Sprintf("dankinstall-%d.log", timestamp))
file, err := os.Create(logPath) file, err := os.Create(logPath)
if err != nil { if err != nil {
-16
View File
@@ -218,9 +218,7 @@ func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential
if timer, exists := b.debounceTimers[id]; exists { if timer, exists := b.debounceTimers[id]; exists {
timer.Reset(200 * time.Millisecond) timer.Reset(200 * time.Millisecond)
} else { } else {
b.debounceWg.Add(1)
b.debounceTimers[id] = time.AfterFunc(200*time.Millisecond, func() { b.debounceTimers[id] = time.AfterFunc(200*time.Millisecond, func() {
defer b.debounceWg.Done()
b.debounceMutex.Lock() b.debounceMutex.Lock()
pending, exists := b.debouncePending[id] pending, exists := b.debouncePending[id]
if exists { if exists {
@@ -492,19 +490,5 @@ func (b *DDCBackend) valueToPercent(value int, max int, exponential bool) int {
return percent return percent
} }
func (b *DDCBackend) WaitPending() {
done := make(chan struct{})
go func() {
b.debounceWg.Wait()
close(done)
}()
select {
case <-done:
case <-time.After(5 * time.Second):
log.Debug("WaitPending timed out waiting for DDC writes")
}
}
func (b *DDCBackend) Close() { func (b *DDCBackend) Close() {
} }
-1
View File
@@ -84,7 +84,6 @@ type DDCBackend struct {
debounceMutex sync.Mutex debounceMutex sync.Mutex
debounceTimers map[string]*time.Timer debounceTimers map[string]*time.Timer
debouncePending map[string]ddcPendingSet debouncePending map[string]ddcPendingSet
debounceWg sync.WaitGroup
} }
type ddcPendingSet struct { type ddcPendingSet struct {
@@ -158,26 +158,18 @@ func (b *NetworkManagerBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInfo
channel := frequencyToChannel(freq) channel := frequencyToChannel(freq)
isConnected := ssid == currentSSID && bssid == currentBSSID
rate := maxBitrate / 1000
if isConnected {
if devBitrate, err := w.GetPropertyBitrate(); err == nil && devBitrate > 0 {
rate = devBitrate / 1000
}
}
network := WiFiNetwork{ network := WiFiNetwork{
SSID: ssid, SSID: ssid,
BSSID: bssid, BSSID: bssid,
Signal: strength, Signal: strength,
Secured: secured, Secured: secured,
Enterprise: enterprise, Enterprise: enterprise,
Connected: isConnected, Connected: ssid == currentSSID && bssid == currentBSSID,
Saved: savedSSIDs[ssid], Saved: savedSSIDs[ssid],
Autoconnect: autoconnectMap[ssid], Autoconnect: autoconnectMap[ssid],
Frequency: freq, Frequency: freq,
Mode: modeStr, Mode: modeStr,
Rate: rate, Rate: maxBitrate / 1000,
Channel: channel, Channel: channel,
} }
@@ -522,27 +514,19 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
channel := frequencyToChannel(freq) channel := frequencyToChannel(freq)
isConnected := ssid == currentSSID
rate := maxBitrate / 1000
if isConnected {
if devBitrate, err := w.GetPropertyBitrate(); err == nil && devBitrate > 0 {
rate = devBitrate / 1000
}
}
network := WiFiNetwork{ network := WiFiNetwork{
SSID: ssid, SSID: ssid,
BSSID: bssid, BSSID: bssid,
Signal: strength, Signal: strength,
Secured: secured, Secured: secured,
Enterprise: enterprise, Enterprise: enterprise,
Connected: isConnected, Connected: ssid == currentSSID,
Saved: savedSSIDs[ssid], Saved: savedSSIDs[ssid],
Autoconnect: autoconnectMap[ssid], Autoconnect: autoconnectMap[ssid],
Hidden: hiddenSSIDs[ssid], Hidden: hiddenSSIDs[ssid],
Frequency: freq, Frequency: freq,
Mode: modeStr, Mode: modeStr,
Rate: rate, Rate: maxBitrate / 1000,
Channel: channel, Channel: channel,
} }
@@ -1078,27 +1062,19 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
channel := frequencyToChannel(freq) channel := frequencyToChannel(freq)
isConnected := connected && apSSID == ssid
rate := maxBitrate / 1000
if isConnected {
if devBitrate, err := devInfo.wireless.GetPropertyBitrate(); err == nil && devBitrate > 0 {
rate = devBitrate / 1000
}
}
network := WiFiNetwork{ network := WiFiNetwork{
SSID: apSSID, SSID: apSSID,
BSSID: apBSSID, BSSID: apBSSID,
Signal: strength, Signal: strength,
Secured: secured, Secured: secured,
Enterprise: enterprise, Enterprise: enterprise,
Connected: isConnected, Connected: connected && apSSID == ssid,
Saved: savedSSIDs[apSSID], Saved: savedSSIDs[apSSID],
Autoconnect: autoconnectMap[apSSID], Autoconnect: autoconnectMap[apSSID],
Hidden: hiddenSSIDs[apSSID], Hidden: hiddenSSIDs[apSSID],
Frequency: freq, Frequency: freq,
Mode: modeStr, Mode: modeStr,
Rate: rate, Rate: maxBitrate / 1000,
Channel: channel, Channel: channel,
Device: name, Device: name,
} }
-29
View File
@@ -31,7 +31,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/trayrecovery"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
@@ -73,7 +72,6 @@ var clipboardManager *clipboard.Manager
var dbusManager *serverDbus.Manager var dbusManager *serverDbus.Manager
var wlContext *wlcontext.SharedContext var wlContext *wlcontext.SharedContext
var themeModeManager *thememode.Manager var themeModeManager *thememode.Manager
var trayRecoveryManager *trayrecovery.Manager
var locationManager *location.Manager var locationManager *location.Manager
var geoClientInstance geolocation.Client var geoClientInstance geolocation.Client
@@ -396,18 +394,6 @@ func InitializeThemeModeManager() error {
return nil return nil
} }
func InitializeTrayRecoveryManager() error {
manager, err := trayrecovery.NewManager()
if err != nil {
return err
}
trayRecoveryManager = manager
log.Info("TrayRecovery manager initialized")
return nil
}
func InitializeLocationManager(geoClient geolocation.Client) error { func InitializeLocationManager(geoClient geolocation.Client) error {
manager, err := location.NewManager(geoClient) manager, err := location.NewManager(geoClient)
if err != nil { if err != nil {
@@ -1339,9 +1325,6 @@ func cleanupManagers() {
if themeModeManager != nil { if themeModeManager != nil {
themeModeManager.Close() themeModeManager.Close()
} }
if trayRecoveryManager != nil {
trayRecoveryManager.Close()
}
if wlContext != nil { if wlContext != nil {
wlContext.Close() wlContext.Close()
} }
@@ -1627,18 +1610,6 @@ func Start(printDocs bool) error {
}() }()
} }
go func() {
<-loginctlReady
if loginctlManager == nil {
return
}
if err := InitializeTrayRecoveryManager(); err != nil {
log.Warnf("TrayRecovery manager unavailable: %v", err)
} else {
trayRecoveryManager.WatchLoginctl(loginctlManager)
}
}()
go func() { go func() {
geoClient := geolocation.NewClient() geoClient := geolocation.NewClient()
geoClientInstance = geoClient geoClientInstance = geoClient
@@ -1,93 +0,0 @@
package trayrecovery
import (
"fmt"
"sync"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
"github.com/godbus/dbus/v5"
)
const resumeDelay = 3 * time.Second
type Manager struct {
conn *dbus.Conn
stopChan chan struct{}
wg sync.WaitGroup
}
func NewManager() (*Manager, error) {
conn, err := dbus.ConnectSessionBus()
if err != nil {
return nil, fmt.Errorf("failed to connect to session bus: %w", err)
}
m := &Manager{
conn: conn,
stopChan: make(chan struct{}),
}
// Run a startup scan after a delay — covers the case where the process
// was killed during suspend and restarted by systemd (Type=dbus).
// The fresh process never sees the PrepareForSleep true→false transition,
// so the loginctl watcher alone is not enough.
go m.scheduleRecovery()
return m, nil
}
// WatchLoginctl subscribes to loginctl session state changes and triggers
// tray recovery after resume from suspend (PrepareForSleep false transition).
// This handles the case where the process survives suspend.
func (m *Manager) WatchLoginctl(lm *loginctl.Manager) {
ch := lm.Subscribe("tray-recovery")
m.wg.Add(1)
go func() {
defer m.wg.Done()
defer lm.Unsubscribe("tray-recovery")
wasSleeping := false
for {
select {
case <-m.stopChan:
return
case state, ok := <-ch:
if !ok {
return
}
if state.PreparingForSleep {
wasSleeping = true
continue
}
if wasSleeping {
wasSleeping = false
go m.scheduleRecovery()
}
}
}
}()
}
func (m *Manager) scheduleRecovery() {
select {
case <-time.After(resumeDelay):
m.recoverTrayItems()
case <-m.stopChan:
}
}
func (m *Manager) Close() {
select {
case <-m.stopChan:
return
default:
close(m.stopChan)
}
m.wg.Wait()
if m.conn != nil {
m.conn.Close()
}
log.Info("TrayRecovery manager closed")
}
@@ -1,262 +0,0 @@
package trayrecovery
import (
"context"
"strings"
"sync"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/godbus/dbus/v5"
)
const (
sniWatcherDest = "org.kde.StatusNotifierWatcher"
sniWatcherPath = "/StatusNotifierWatcher"
sniWatcherIface = "org.kde.StatusNotifierWatcher"
sniItemIface = "org.kde.StatusNotifierItem"
dbusIface = "org.freedesktop.DBus"
propsIface = "org.freedesktop.DBus.Properties"
probeTimeout = 300 * time.Millisecond
connProbeTimeout = 150 * time.Millisecond
batchSize = 30
)
var excludedPrefixes = []string{
"org.freedesktop.",
"org.gnome.",
"org.kde.StatusNotifier",
"com.canonical.AppMenu",
"org.mpris.",
"org.pipewire.",
"org.pulseaudio",
"fi.epitaph",
"quickshell",
"org.kde.quickshell",
}
func (m *Manager) recoverTrayItems() {
registeredItems := m.getRegisteredItems()
allNames := m.getDBusNames()
if allNames == nil {
return
}
registeredConnIDs := m.buildRegisteredConnIDs(registeredItems)
count := len(registeredItems)
log.Infof("TrayRecoveryService: scanning DBus for unregistered SNI items (%d already registered)...", count)
m.scanWellKnownNames(allNames, registeredItems, registeredConnIDs)
m.scanConnectionIDs(allNames, registeredItems, registeredConnIDs)
}
func (m *Manager) getRegisteredItems() []string {
obj := m.conn.Object(sniWatcherDest, sniWatcherPath)
variant, err := obj.GetProperty(sniWatcherIface + ".RegisteredStatusNotifierItems")
if err != nil {
log.Warnf("TrayRecoveryService: failed to get registered items: %v", err)
return nil
}
switch v := variant.Value().(type) {
case []string:
return v
case []any:
items := make([]string, 0, len(v))
for _, elem := range v {
if s, ok := elem.(string); ok {
items = append(items, s)
}
}
return items
}
return nil
}
func (m *Manager) getDBusNames() []string {
var names []string
err := m.conn.BusObject().Call(dbusIface+".ListNames", 0).Store(&names)
if err != nil {
log.Warnf("TrayRecoveryService: failed to list bus names: %v", err)
return nil
}
return names
}
func (m *Manager) getNameOwner(name string) string {
var owner string
err := m.conn.BusObject().Call(dbusIface+".GetNameOwner", 0, name).Store(&owner)
if err != nil {
return ""
}
return owner
}
// buildRegisteredConnIDs resolves every registered SNI item (well-known name
// or :1.xxx connection ID) to a canonical connection ID. This prevents
// duplicates in both directions.
func (m *Manager) buildRegisteredConnIDs(registeredItems []string) map[string]bool {
connIDs := make(map[string]bool, len(registeredItems))
for _, item := range registeredItems {
name := extractName(item)
if strings.HasPrefix(name, ":1.") {
connIDs[name] = true
} else {
owner := m.getNameOwner(name)
if owner != "" {
connIDs[owner] = true
}
}
}
return connIDs
}
// scanWellKnownNames probes well-known names (e.g. DinoX, nm-applet) for
// unregistered SNI items and re-registers them.
func (m *Manager) scanWellKnownNames(allNames []string, registeredItems []string, registeredConnIDs map[string]bool) {
registeredRaw := strings.Join(registeredItems, "\n")
for _, name := range allNames {
if strings.HasPrefix(name, ":") {
continue
}
if strings.Contains(registeredRaw, name) {
continue
}
// Skip if this name's connection ID is already in the registered set
// (handles the case where the app registered via connection ID instead)
connForName := m.getNameOwner(name)
if connForName != "" && registeredConnIDs[connForName] {
continue
}
if isExcludedName(name) {
continue
}
short := shortName(name)
objectPaths := []string{
"/StatusNotifierItem",
"/org/ayatana/NotificationItem/" + short,
}
for _, objPath := range objectPaths {
if m.probeSNI(name, objPath, probeTimeout) {
m.registerSNI(name)
// Update set so the connection-ID section won't double-register this app
if connForName != "" {
registeredConnIDs[connForName] = true
}
break
}
}
}
}
// scanConnectionIDs probes all :1.xxx connections in parallel for unregistered
// SNI items (e.g. Vesktop, Electron apps). Most non-SNI connections return an
// error instantly, so this is fast.
func (m *Manager) scanConnectionIDs(allNames []string, registeredItems []string, registeredConnIDs map[string]bool) {
registeredRaw := strings.Join(registeredItems, "\n")
registeredLower := strings.ToLower(registeredRaw)
var wg sync.WaitGroup
sem := make(chan struct{}, batchSize)
for _, name := range allNames {
if !strings.HasPrefix(name, ":1.") {
continue
}
if registeredConnIDs[name] {
continue
}
sem <- struct{}{}
wg.Add(1)
go func(conn string) {
defer wg.Done()
defer func() { <-sem }()
sniID := m.getSNIId(conn, connProbeTimeout)
if sniID == "" {
return
}
// Skip if an item with the same Id is already registered (case-insensitive)
if strings.Contains(registeredLower, strings.ToLower(sniID)) {
return
}
m.registerSNI(conn)
log.Infof("TrayRecovery: re-registered %s (Id: %s)", conn, sniID)
}(name)
}
wg.Wait()
}
func (m *Manager) probeSNI(dest, path string, timeout time.Duration) bool {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
obj := m.conn.Object(dest, dbus.ObjectPath(path))
var props map[string]dbus.Variant
err := obj.CallWithContext(ctx, propsIface+".GetAll", 0, sniItemIface).Store(&props)
if err != nil {
return false
}
_, hasID := props["Id"]
return hasID
}
func (m *Manager) getSNIId(dest string, timeout time.Duration) string {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
obj := m.conn.Object(dest, "/StatusNotifierItem")
var variant dbus.Variant
err := obj.CallWithContext(ctx, propsIface+".Get", 0, sniItemIface, "Id").Store(&variant)
if err != nil {
return ""
}
id, _ := variant.Value().(string)
return id
}
func (m *Manager) registerSNI(name string) {
obj := m.conn.Object(sniWatcherDest, sniWatcherPath)
call := obj.Call(sniWatcherIface+".RegisterStatusNotifierItem", 0, name)
if call.Err != nil {
log.Warnf("TrayRecovery: failed to register %s: %v", name, call.Err)
return
}
log.Infof("TrayRecovery: re-registered %s", name)
}
func extractName(item string) string {
if idx := strings.IndexByte(item, '/'); idx != -1 {
return item[:idx]
}
return item
}
func shortName(name string) string {
parts := strings.Split(name, ".")
if len(parts) > 0 {
return parts[len(parts)-1]
}
return name
}
func isExcludedName(name string) bool {
for _, prefix := range excludedPrefixes {
if strings.HasPrefix(name, prefix) {
return true
}
}
return false
}
+48 -178
View File
@@ -3,11 +3,8 @@ package wayland
import ( import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"errors"
"fmt" "fmt"
"io"
"os" "os"
"slices"
"syscall" "syscall"
"time" "time"
@@ -76,10 +73,7 @@ func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error
m.post(func() { m.post(func() {
log.Info("Gamma control enabled at startup") log.Info("Gamma control enabled at startup")
gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1) gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
m.availOutputsMu.RLock() if err := m.setupOutputControls(m.availableOutputs, gammaMgr); err != nil {
outs := slices.Clone(m.availableOutputs)
m.availOutputsMu.RUnlock()
if err := m.setupOutputControls(outs, gammaMgr); err != nil {
log.Errorf("Failed to initialize gamma controls: %v", err) log.Errorf("Failed to initialize gamma controls: %v", err)
return return
} }
@@ -176,7 +170,6 @@ func (m *Manager) setupRegistry() error {
}) })
if gammaMgr != nil { if gammaMgr != nil {
outputs = append(outputs, output) outputs = append(outputs, output)
m.addAvailableOutput(output)
} }
m.outputRegNames.Store(outputID, e.Name) m.outputRegNames.Store(outputID, e.Name)
@@ -211,11 +204,6 @@ func (m *Manager) setupRegistry() error {
} }
if foundOut.gammaControl != nil { if foundOut.gammaControl != nil {
foundOut.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1).Destroy() foundOut.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1).Destroy()
foundOut.gammaControl = nil
}
m.removeAvailableOutput(foundOut.output)
if foundOut.output != nil && !foundOut.output.IsZombie() {
_ = foundOut.output.Release()
} }
m.outputs.Delete(foundID) m.outputs.Delete(foundID)
@@ -300,28 +288,14 @@ func (m *Manager) setupControlHandlers(state *outputState, control *wlr_gamma_co
if !ok { if !ok {
return return
} }
if ctrl, ok := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1); ok && ctrl != nil && !ctrl.IsZombie() {
ctrl.Destroy()
}
out.gammaControl = nil
out.failed = true out.failed = true
out.rampSize = 0 out.rampSize = 0
out.retryCount++ out.retryCount++
out.lastFailTime = time.Now() out.lastFailTime = time.Now()
if !m.outputStillValid(out) {
return
}
backoff := time.Duration(300<<uint(min(out.retryCount-1, 4))) * time.Millisecond backoff := time.Duration(300<<uint(min(out.retryCount-1, 4))) * time.Millisecond
time.AfterFunc(backoff, func() { time.AfterFunc(backoff, func() {
m.post(func() { m.post(func() {
if !m.outputStillValid(out) {
return
}
if _, stillTracked := m.outputs.Load(outputID); !stillTracked {
return
}
m.recreateOutputControl(out) m.recreateOutputControl(out)
}) })
}) })
@@ -329,75 +303,12 @@ func (m *Manager) setupControlHandlers(state *outputState, control *wlr_gamma_co
}) })
} }
func (m *Manager) addAvailableOutput(o *wlclient.Output) {
if o == nil {
return
}
m.availOutputsMu.Lock()
defer m.availOutputsMu.Unlock()
if slices.Contains(m.availableOutputs, o) {
return
}
m.availableOutputs = append(m.availableOutputs, o)
}
func (m *Manager) removeAvailableOutput(o *wlclient.Output) {
if o == nil {
return
}
m.availOutputsMu.Lock()
defer m.availOutputsMu.Unlock()
m.availableOutputs = slices.DeleteFunc(m.availableOutputs, func(existing *wlclient.Output) bool {
return existing == o
})
}
func (m *Manager) outputStillValid(out *outputState) bool {
switch {
case out == nil:
return false
case out.output == nil:
return false
case out.output.IsZombie():
return false
}
m.availOutputsMu.RLock()
defer m.availOutputsMu.RUnlock()
return slices.Contains(m.availableOutputs, out.output)
}
func isConnectionDeadErr(err error) bool {
switch {
case err == nil:
return false
case errors.Is(err, syscall.EPIPE):
return true
case errors.Is(err, syscall.ECONNRESET):
return true
case errors.Is(err, syscall.EBADF):
return true
case errors.Is(err, io.EOF):
return true
}
return false
}
func (m *Manager) addOutputControl(output *wlclient.Output) error { func (m *Manager) addOutputControl(output *wlclient.Output) error {
switch {
case m.connectionDead.Load():
return nil
case output == nil || output.IsZombie():
return nil
}
outputID := output.ID() outputID := output.ID()
gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1) gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
control, err := gammaMgr.GetGammaControl(output) control, err := gammaMgr.GetGammaControl(output)
if err != nil { if err != nil {
if isConnectionDeadErr(err) {
m.markConnectionDead(err)
}
return err return err
} }
@@ -418,37 +329,26 @@ func (m *Manager) recreateOutputControl(out *outputState) error {
enabled := m.config.Enabled enabled := m.config.Enabled
m.configMutex.RUnlock() m.configMutex.RUnlock()
switch { if !enabled || !m.controlsInitialized {
case m.connectionDead.Load():
return nil
case !enabled || !m.controlsInitialized:
return nil
case out.isVirtual:
return nil
case out.retryCount >= 10:
return nil
case !m.outputStillValid(out):
return nil return nil
} }
if _, ok := m.outputs.Load(out.id); !ok { if _, ok := m.outputs.Load(out.id); !ok {
return nil return nil
} }
if out.isVirtual {
return nil
}
if out.retryCount >= 10 {
return nil
}
gammaMgr, ok := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1) gammaMgr, ok := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
if !ok { if !ok {
return fmt.Errorf("no gamma manager") return fmt.Errorf("no gamma manager")
} }
if existing, ok := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1); ok && existing != nil && !existing.IsZombie() {
existing.Destroy()
out.gammaControl = nil
}
control, err := gammaMgr.GetGammaControl(out.output) control, err := gammaMgr.GetGammaControl(out.output)
if err != nil { if err != nil {
if isConnectionDeadErr(err) {
m.markConnectionDead(err)
}
return err return err
} }
@@ -458,13 +358,6 @@ func (m *Manager) recreateOutputControl(out *outputState) error {
return nil return nil
} }
func (m *Manager) markConnectionDead(err error) {
if m.connectionDead.Swap(true) {
return
}
log.Errorf("gamma: wayland connection appears dead (%v); pausing gamma operations", err)
}
func (m *Manager) recalcSchedule(now time.Time) { func (m *Manager) recalcSchedule(now time.Time) {
m.configMutex.RLock() m.configMutex.RLock()
config := m.config config := m.config
@@ -797,12 +690,11 @@ func (m *Manager) applyGamma(temp int) {
gamma := m.config.Gamma gamma := m.config.Gamma
m.configMutex.RUnlock() m.configMutex.RUnlock()
switch { if !m.controlsInitialized {
case m.connectionDead.Load():
return return
case !m.controlsInitialized: }
return
case m.lastAppliedTemp == temp && m.lastAppliedGamma == gamma: if m.lastAppliedTemp == temp && m.lastAppliedGamma == gamma {
return return
} }
@@ -822,14 +714,7 @@ func (m *Manager) applyGamma(temp int) {
var jobs []job var jobs []job
for _, out := range outs { for _, out := range outs {
switch { if out.failed || out.rampSize == 0 {
case out.failed:
continue
case out.rampSize == 0:
continue
case out.gammaControl == nil:
continue
case !m.outputStillValid(out):
continue continue
} }
ramp := GenerateGammaRamp(out.rampSize, temp, gamma) ramp := GenerateGammaRamp(out.rampSize, temp, gamma)
@@ -847,16 +732,18 @@ func (m *Manager) applyGamma(temp int) {
} }
for _, j := range jobs { for _, j := range jobs {
err := m.setGammaBytes(j.out, j.data) if err := m.setGammaBytes(j.out, j.data); err != nil {
if err == nil { log.Warnf("gamma: failed to set output %d: %v", j.out.id, err)
continue j.out.failed = true
} j.out.rampSize = 0
log.Warnf("gamma: failed to set output %d: %v", j.out.id, err) outID := j.out.id
j.out.failed = true time.AfterFunc(300*time.Millisecond, func() {
j.out.rampSize = 0 m.post(func() {
if isConnectionDeadErr(err) { if out, ok := m.outputs.Load(outID); ok && out.failed {
m.markConnectionDead(err) m.recreateOutputControl(out)
return }
})
})
} }
} }
@@ -865,14 +752,6 @@ func (m *Manager) applyGamma(temp int) {
} }
func (m *Manager) setGammaBytes(out *outputState, data []byte) error { func (m *Manager) setGammaBytes(out *outputState, data []byte) error {
if out.gammaControl == nil {
return fmt.Errorf("no gamma control")
}
ctrl, ok := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1)
if !ok || ctrl == nil || ctrl.IsZombie() {
return fmt.Errorf("gamma control invalid")
}
fd, err := MemfdCreate("gamma-ramp", 0) fd, err := MemfdCreate("gamma-ramp", 0)
if err != nil { if err != nil {
return err return err
@@ -895,6 +774,7 @@ func (m *Manager) setGammaBytes(out *outputState, data []byte) error {
} }
syscall.Seek(fd, 0, 0) syscall.Seek(fd, 0, 0)
ctrl := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1)
return ctrl.SetGamma(fd) return ctrl.SetGamma(fd)
} }
@@ -1002,10 +882,10 @@ func (m *Manager) dbusMonitor() {
} }
func (m *Manager) handleDBusSignal(sig *dbus.Signal) { func (m *Manager) handleDBusSignal(sig *dbus.Signal) {
switch { if sig.Name != "org.freedesktop.login1.Manager.PrepareForSleep" {
case sig.Name != "org.freedesktop.login1.Manager.PrepareForSleep":
return return
case len(sig.Body) == 0: }
if len(sig.Body) == 0 {
return return
} }
preparing, ok := sig.Body[0].(bool) preparing, ok := sig.Body[0].(bool)
@@ -1019,34 +899,27 @@ func (m *Manager) handleDBusSignal(sig *dbus.Signal) {
return return
} }
time.AfterFunc(500*time.Millisecond, func() { time.AfterFunc(500*time.Millisecond, func() {
m.post(m.handleResume) m.post(func() {
m.configMutex.RLock()
stillEnabled := m.config.Enabled
m.configMutex.RUnlock()
if !stillEnabled || !m.controlsInitialized {
return
}
m.outputs.Range(func(_ uint32, out *outputState) bool {
if out.gammaControl != nil {
out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1).Destroy()
out.gammaControl = nil
}
out.retryCount = 0
out.failed = false
m.recreateOutputControl(out)
return true
})
})
}) })
} }
func (m *Manager) handleResume() {
m.configMutex.RLock()
stillEnabled := m.config.Enabled
m.configMutex.RUnlock()
switch {
case !stillEnabled:
return
case !m.controlsInitialized:
return
case m.connectionDead.Load():
return
}
// Compositors (Niri, Hyprland, wlroots-based) re-apply the cached gamma
// ramp to DRM on resume; gamma_control objects stay valid. We just need
// to force a resend so the schedule catches up with the current time of
// day — the original #1235 regression was caused by lastAppliedTemp
// matching and the send being skipped.
m.recalcSchedule(time.Now())
m.lastAppliedTemp = 0
m.applyCurrentTemp("resume")
}
func (m *Manager) triggerUpdate() { func (m *Manager) triggerUpdate() {
select { select {
case m.updateTrigger <- struct{}{}: case m.updateTrigger <- struct{}{}:
@@ -1185,10 +1058,7 @@ func (m *Manager) SetEnabled(enabled bool) {
case enabled && !m.controlsInitialized: case enabled && !m.controlsInitialized:
m.post(func() { m.post(func() {
gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1) gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
m.availOutputsMu.RLock() if err := m.setupOutputControls(m.availableOutputs, gammaMgr); err != nil {
outs := slices.Clone(m.availableOutputs)
m.availOutputsMu.RUnlock()
if err := m.setupOutputControls(outs, gammaMgr); err != nil {
log.Errorf("gamma: failed to create controls: %v", err) log.Errorf("gamma: failed to create controls: %v", err)
return return
} }
-3
View File
@@ -3,7 +3,6 @@ package wayland
import ( import (
"math" "math"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
@@ -72,11 +71,9 @@ type Manager struct {
registry *wlclient.Registry registry *wlclient.Registry
gammaControl any gammaControl any
availableOutputs []*wlclient.Output availableOutputs []*wlclient.Output
availOutputsMu sync.RWMutex
outputRegNames syncmap.Map[uint32, uint32] outputRegNames syncmap.Map[uint32, uint32]
outputs syncmap.Map[uint32, *outputState] outputs syncmap.Map[uint32, *outputState]
controlsInitialized bool controlsInitialized bool
connectionDead atomic.Bool
cmdq chan cmd cmdq chan cmd
alive bool alive bool
+1 -1
View File
@@ -139,7 +139,7 @@ func dmsPackageName(distroID string, dependencies []deps.Dependency) string {
if isGit { if isGit {
return "dms-shell-git" return "dms-shell-git"
} }
return "dms-shell" return "dms-shell-bin"
case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE: case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE:
if isGit { if isGit {
return "dms-git" return "dms-git"
-179
View File
@@ -1,179 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
// AnimVariants Central tuning for animation and Motion Effects variants
// (Material/Fluent/Dynamic) (Standard/Directional/Depth)
Singleton {
id: root
readonly property list<real> variantEnterCurve: {
if (typeof SettingsData === "undefined")
return Anims.expressiveDefaultSpatial;
switch (SettingsData.animationVariant) {
case 1:
return Anims.standardDecel;
case 2:
return Anims.expressiveFastSpatial;
default:
return Anims.expressiveDefaultSpatial;
}
}
readonly property list<real> variantExitCurve: {
if (typeof SettingsData === "undefined")
return Anims.emphasized;
switch (SettingsData.animationVariant) {
case 1:
return Anims.standard;
case 2:
return Anims.emphasized;
default:
return Anims.emphasized;
}
}
// Modal-specific entry curve
readonly property list<real> variantModalEnterCurve: {
if (typeof SettingsData === "undefined")
return Anims.expressiveDefaultSpatial;
if (isDirectionalEffect) {
if (SettingsData.animationVariant === 1)
return Anims.standardDecel;
if (SettingsData.animationVariant === 2)
return Anims.expressiveFastSpatial;
}
return variantEnterCurve;
}
readonly property list<real> variantModalExitCurve: {
if (typeof SettingsData === "undefined")
return Anims.emphasized;
if (isDirectionalEffect) {
if (SettingsData.animationVariant === 1)
return Anims.emphasizedAccel;
if (SettingsData.animationVariant === 2)
return Anims.emphasizedAccel;
}
return variantExitCurve;
}
// Popout-specific entry curve
readonly property list<real> variantPopoutEnterCurve: {
if (typeof SettingsData === "undefined")
return Anims.expressiveDefaultSpatial;
if (isDirectionalEffect) {
if (SettingsData.animationVariant === 1)
return Anims.standardDecel;
if (SettingsData.animationVariant === 2)
return Anims.expressiveFastSpatial;
return Anims.standardDecel;
}
return variantEnterCurve;
}
readonly property list<real> variantPopoutExitCurve: {
if (typeof SettingsData === "undefined")
return Anims.emphasized;
if (isDirectionalEffect) {
if (SettingsData.animationVariant === 1)
return Anims.emphasizedAccel;
if (SettingsData.animationVariant === 2)
return Anims.emphasizedAccel;
}
return variantExitCurve;
}
readonly property real variantEnterDurationFactor: {
if (typeof SettingsData === "undefined")
return 1.0;
switch (SettingsData.animationVariant) {
case 1:
return 0.9;
case 2:
return 1.08;
default:
return 1.0;
}
}
readonly property real variantExitDurationFactor: {
if (typeof SettingsData === "undefined")
return 1.0;
switch (SettingsData.animationVariant) {
case 1:
return 0.85;
case 2:
return 0.92;
default:
return 1.0;
}
}
// Fluent: opacity at ~55% of duration; Material/Dynamic: 1:1 with position
readonly property real variantOpacityDurationScale: {
if (typeof SettingsData === "undefined")
return 1.0;
return SettingsData.animationVariant === 1 ? 0.55 : 1.0;
}
function variantDuration(baseDuration, entering) {
const factor = entering ? variantEnterDurationFactor : variantExitDurationFactor;
return Math.max(0, Math.round(baseDuration * factor));
}
function variantExitCleanupPadding() {
if (typeof SettingsData === "undefined")
return 50;
switch (SettingsData.motionEffect) {
case 1:
return 8;
case 2:
return 24;
default:
return 50;
}
}
function variantCloseInterval(baseDuration) {
return variantDuration(baseDuration, false) + variantExitCleanupPadding();
}
readonly property bool isDirectionalEffect: isConnectedEffect
|| (typeof SettingsData !== "undefined" && SettingsData.motionEffect === 1)
readonly property bool isDepthEffect: typeof SettingsData !== "undefined" && SettingsData.motionEffect === 2
readonly property bool isConnectedEffect: typeof SettingsData !== "undefined"
&& SettingsData.frameEnabled
&& SettingsData.motionEffect === 1
&& SettingsData.directionalAnimationMode === 3
readonly property real effectScaleCollapsed: {
if (typeof SettingsData === "undefined")
return 0.96;
switch (SettingsData.motionEffect) {
case 1:
return 1.0;
case 2:
return 0.88;
default:
return 0.96;
}
}
readonly property real effectAnimOffset: {
if (typeof SettingsData === "undefined")
return 16;
switch (SettingsData.motionEffect) {
case 1:
return 144;
case 2:
return 56;
default:
return 16;
}
}
}
-5
View File
@@ -22,9 +22,4 @@ Singleton {
readonly property var standard: [0.20, 0.00, 0.00, 1.00, 1.00, 1.00] readonly property var standard: [0.20, 0.00, 0.00, 1.00, 1.00, 1.00]
readonly property var standardDecel: [0.00, 0.00, 0.00, 1.00, 1.00, 1.00] readonly property var standardDecel: [0.00, 0.00, 0.00, 1.00, 1.00, 1.00]
readonly property var standardAccel: [0.30, 0.00, 1.00, 1.00, 1.00, 1.00] readonly property var standardAccel: [0.30, 0.00, 1.00, 1.00, 1.00, 1.00]
// Used by AnimVariants for variant/effect logic
readonly property var expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
readonly property var expressiveFastSpatial: [0.34, 1.5, 0.2, 1.0, 1.0, 1.0]
readonly property var expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
} }
-220
View File
@@ -1,220 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
Singleton {
id: root
readonly property var emptyDockState: ({
"reveal": false,
"barSide": "bottom",
"bodyX": 0,
"bodyY": 0,
"bodyW": 0,
"bodyH": 0,
"slideX": 0,
"slideY": 0
})
// Popout state (updated by DankPopout when connectedFrameModeActive)
property string popoutOwnerId: ""
property bool popoutVisible: false
property string popoutBarSide: "top"
property real popoutBodyX: 0
property real popoutBodyY: 0
property real popoutBodyW: 0
property real popoutBodyH: 0
property real popoutAnimX: 0
property real popoutAnimY: 0
property string popoutScreen: ""
// 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: ({})
function hasPopoutOwner(claimId) {
return !!claimId && popoutOwnerId === claimId;
}
function claimPopout(claimId, state) {
if (!claimId)
return false;
popoutOwnerId = claimId;
return updatePopout(claimId, state);
}
function updatePopout(claimId, state) {
if (!hasPopoutOwner(claimId) || !state)
return false;
if (state.visible !== undefined)
popoutVisible = !!state.visible;
if (state.barSide !== undefined)
popoutBarSide = state.barSide || "top";
if (state.bodyX !== undefined)
popoutBodyX = Number(state.bodyX);
if (state.bodyY !== undefined)
popoutBodyY = Number(state.bodyY);
if (state.bodyW !== undefined)
popoutBodyW = Number(state.bodyW);
if (state.bodyH !== undefined)
popoutBodyH = Number(state.bodyH);
if (state.animX !== undefined)
popoutAnimX = Number(state.animX);
if (state.animY !== undefined)
popoutAnimY = Number(state.animY);
if (state.screen !== undefined)
popoutScreen = state.screen || "";
return true;
}
function releasePopout(claimId) {
if (!hasPopoutOwner(claimId))
return false;
popoutOwnerId = "";
popoutVisible = false;
popoutBarSide = "top";
popoutBodyX = 0;
popoutBodyY = 0;
popoutBodyW = 0;
popoutBodyH = 0;
popoutAnimX = 0;
popoutAnimY = 0;
popoutScreen = "";
return true;
}
function setPopoutAnim(claimId, animX, animY) {
if (!hasPopoutOwner(claimId))
return false;
if (animX !== undefined) {
const nextX = Number(animX);
if (!isNaN(nextX) && popoutAnimX !== nextX)
popoutAnimX = nextX;
}
if (animY !== undefined) {
const nextY = Number(animY);
if (!isNaN(nextY) && popoutAnimY !== nextY)
popoutAnimY = nextY;
}
return true;
}
function _cloneDockStates() {
const next = {};
for (const screenName in dockStates)
next[screenName] = dockStates[screenName];
return next;
}
function _normalizeDockState(state) {
return {
"reveal": !!(state && state.reveal),
"barSide": state && state.barSide ? state.barSide : "bottom",
"bodyX": Number(state && state.bodyX !== undefined ? state.bodyX : 0),
"bodyY": Number(state && state.bodyY !== undefined ? state.bodyY : 0),
"bodyW": Number(state && state.bodyW !== undefined ? state.bodyW : 0),
"bodyH": Number(state && state.bodyH !== undefined ? state.bodyH : 0),
"slideX": Number(state && state.slideX !== undefined ? state.slideX : 0),
"slideY": Number(state && state.slideY !== undefined ? state.slideY : 0)
};
}
function setDockState(screenName, state) {
if (!screenName || !state)
return false;
const next = _cloneDockStates();
next[screenName] = _normalizeDockState(state);
dockStates = next;
return true;
}
function clearDockState(screenName) {
if (!screenName || !dockStates[screenName])
return false;
const next = _cloneDockStates();
delete next[screenName];
dockStates = next;
// Also clear corresponding slide
if (dockSlides[screenName]) {
const nextSlides = {};
for (const k in dockSlides)
nextSlides[k] = dockSlides[k];
delete nextSlides[screenName];
dockSlides = nextSlides;
}
return true;
}
function setDockSlide(screenName, x, y) {
if (!screenName)
return false;
const next = {};
for (const k in dockSlides)
next[k] = dockSlides[k];
next[screenName] = { "x": Number(x), "y": Number(y) };
dockSlides = next;
return true;
}
// Notification state (per screen, updated by NotificationSurface)
readonly property var emptyNotificationState: ({
"visible": false,
"barSide": "top",
"bodyX": 0,
"bodyY": 0,
"bodyW": 0,
"bodyH": 0
})
property var notificationStates: ({})
function _cloneNotificationStates() {
const next = {};
for (const screenName in notificationStates)
next[screenName] = notificationStates[screenName];
return next;
}
function _normalizeNotificationState(state) {
return {
"visible": !!(state && state.visible),
"barSide": state && state.barSide ? state.barSide : "top",
"bodyX": Number(state && state.bodyX !== undefined ? state.bodyX : 0),
"bodyY": Number(state && state.bodyY !== undefined ? state.bodyY : 0),
"bodyW": Number(state && state.bodyW !== undefined ? state.bodyW : 0),
"bodyH": Number(state && state.bodyH !== undefined ? state.bodyH : 0)
};
}
function setNotificationState(screenName, state) {
if (!screenName || !state)
return false;
const next = _cloneNotificationStates();
next[screenName] = _normalizeNotificationState(state);
notificationStates = next;
return true;
}
function clearNotificationState(screenName) {
if (!screenName || !notificationStates[screenName])
return false;
const next = _cloneNotificationStates();
delete next[screenName];
notificationStates = next;
return true;
}
}
+1 -10
View File
@@ -13,13 +13,8 @@ Item {
property color targetColor: "white" property color targetColor: "white"
property real targetRadius: Theme.cornerRadius property real targetRadius: Theme.cornerRadius
property real topLeftRadius: targetRadius
property real topRightRadius: targetRadius
property real bottomLeftRadius: targetRadius
property real bottomRightRadius: targetRadius
property color borderColor: "transparent" property color borderColor: "transparent"
property real borderWidth: 0 property real borderWidth: 0
property bool useCustomSource: false
property bool shadowEnabled: Theme.elevationEnabled property bool shadowEnabled: Theme.elevationEnabled
property real shadowBlurPx: level && level.blurPx !== undefined ? level.blurPx : 0 property real shadowBlurPx: level && level.blurPx !== undefined ? level.blurPx : 0
@@ -51,11 +46,7 @@ Item {
Rectangle { Rectangle {
id: sourceRect id: sourceRect
anchors.fill: parent anchors.fill: parent
visible: !root.useCustomSource radius: root.targetRadius
topLeftRadius: root.topLeftRadius
topRightRadius: root.topRightRadius
bottomLeftRadius: root.bottomLeftRadius
bottomRightRadius: root.bottomRightRadius
color: root.targetColor color: root.targetColor
border.color: root.borderColor border.color: root.borderColor
border.width: root.borderWidth border.width: root.borderWidth
-2
View File
@@ -124,8 +124,6 @@ Singleton {
property string vpnLastConnected: "" property string vpnLastConnected: ""
property string lastPlayerIdentity: ""
property var deviceMaxVolumes: ({}) property var deviceMaxVolumes: ({})
property var hiddenOutputDeviceNames: [] property var hiddenOutputDeviceNames: []
property var hiddenInputDeviceNames: [] property var hiddenInputDeviceNames: []
+27 -220
View File
@@ -37,18 +37,6 @@ Singleton {
Custom Custom
} }
enum AnimationVariant {
Material,
Fluent,
Dynamic
}
enum AnimationEffect {
Standard, // 0 M3: scale-in, rises from below
Directional, // 1 pure large slide, no scale
Depth // 2 medium slide with deep depth scale pop
}
enum SuspendBehavior { enum SuspendBehavior {
Suspend, Suspend,
Hibernate, Hibernate,
@@ -180,12 +168,6 @@ Singleton {
property int modalCustomAnimationDuration: 150 property int modalCustomAnimationDuration: 150
property bool enableRippleEffects: true property bool enableRippleEffects: true
onEnableRippleEffectsChanged: saveSettings() onEnableRippleEffectsChanged: saveSettings()
property int animationVariant: SettingsData.AnimationVariant.Material
onAnimationVariantChanged: saveSettings()
property int motionEffect: SettingsData.AnimationEffect.Standard
onMotionEffectChanged: saveSettings()
property int directionalAnimationMode: 0
onDirectionalAnimationModeChanged: saveSettings()
property bool m3ElevationEnabled: true property bool m3ElevationEnabled: true
onM3ElevationEnabledChanged: saveSettings() onM3ElevationEnabledChanged: saveSettings()
property int m3ElevationIntensity: 12 property int m3ElevationIntensity: 12
@@ -235,23 +217,6 @@ Singleton {
onFrameShowOnOverviewChanged: saveSettings() onFrameShowOnOverviewChanged: saveSettings()
property bool frameBlurEnabled: true property bool frameBlurEnabled: true
onFrameBlurEnabledChanged: saveSettings() onFrameBlurEnabledChanged: saveSettings()
property int previousDirectionalMode: 1
onPreviousDirectionalModeChanged: saveSettings()
property var connectedFrameBarStyleBackups: ({})
onConnectedFrameBarStyleBackupsChanged: saveSettings()
readonly property bool connectedFrameModeActive: frameEnabled
&& motionEffect === SettingsData.AnimationEffect.Directional
&& directionalAnimationMode === 3
onConnectedFrameModeActiveChanged: {
if (_loading)
return;
if (connectedFrameModeActive) {
_captureConnectedFrameBarStyleBackups(barConfigs, true);
_enforceConnectedModeBarStyleReset();
} else {
_restoreConnectedFrameBarStyleBackups();
}
}
readonly property color effectiveFrameColor: { readonly property color effectiveFrameColor: {
const fc = frameColor; const fc = frameColor;
@@ -364,7 +329,6 @@ Singleton {
property var workspaceNameIcons: ({}) property var workspaceNameIcons: ({})
property bool waveProgressEnabled: true property bool waveProgressEnabled: true
property bool scrollTitleEnabled: true property bool scrollTitleEnabled: true
property bool mediaAdaptiveWidthEnabled: true
property bool audioVisualizerEnabled: true property bool audioVisualizerEnabled: true
property string audioScrollMode: "volume" property string audioScrollMode: "volume"
property int audioWheelScrollAmount: 5 property int audioWheelScrollAmount: 5
@@ -422,8 +386,6 @@ Singleton {
property string dankLauncherV2BorderColor: "primary" property string dankLauncherV2BorderColor: "primary"
property bool dankLauncherV2ShowFooter: true property bool dankLauncherV2ShowFooter: true
property bool dankLauncherV2UnloadOnClose: false property bool dankLauncherV2UnloadOnClose: false
property bool dankLauncherV2IncludeFilesInAll: false
property bool dankLauncherV2IncludeFoldersInAll: false
property string _legacyWeatherLocation: "New York, NY" property string _legacyWeatherLocation: "New York, NY"
property string _legacyWeatherCoordinates: "40.7128,-74.0060" property string _legacyWeatherCoordinates: "40.7128,-74.0060"
@@ -500,7 +462,6 @@ Singleton {
property bool soundNewNotification: true property bool soundNewNotification: true
property bool soundVolumeChanged: true property bool soundVolumeChanged: true
property bool soundPluggedIn: true property bool soundPluggedIn: true
property bool soundLogin: false
property int acMonitorTimeout: 0 property int acMonitorTimeout: 0
property int acLockTimeout: 0 property int acLockTimeout: 0
@@ -621,24 +582,24 @@ Singleton {
property bool enableFprint: false property bool enableFprint: false
property int maxFprintTries: 15 property int maxFprintTries: 15
readonly property bool fprintdAvailable: Processes.fprintdAvailable property bool fprintdAvailable: false
readonly property bool lockFingerprintCanEnable: Processes.lockFingerprintCanEnable property bool lockFingerprintCanEnable: false
readonly property bool lockFingerprintReady: Processes.lockFingerprintReady property bool lockFingerprintReady: false
readonly property string lockFingerprintReason: Processes.lockFingerprintReason property string lockFingerprintReason: "probe_failed"
readonly property bool greeterFingerprintCanEnable: Processes.greeterFingerprintCanEnable property bool greeterFingerprintCanEnable: false
readonly property bool greeterFingerprintReady: Processes.greeterFingerprintReady property bool greeterFingerprintReady: false
readonly property string greeterFingerprintReason: Processes.greeterFingerprintReason property string greeterFingerprintReason: "probe_failed"
readonly property string greeterFingerprintSource: Processes.greeterFingerprintSource property string greeterFingerprintSource: "none"
property bool enableU2f: false property bool enableU2f: false
property string u2fMode: "or" property string u2fMode: "or"
readonly property bool u2fAvailable: Processes.u2fAvailable property bool u2fAvailable: false
readonly property bool lockU2fCanEnable: Processes.lockU2fCanEnable property bool lockU2fCanEnable: false
readonly property bool lockU2fReady: Processes.lockU2fReady property bool lockU2fReady: false
readonly property string lockU2fReason: Processes.lockU2fReason property string lockU2fReason: "probe_failed"
readonly property bool greeterU2fCanEnable: Processes.greeterU2fCanEnable property bool greeterU2fCanEnable: false
readonly property bool greeterU2fReady: Processes.greeterU2fReady property bool greeterU2fReady: false
readonly property string greeterU2fReason: Processes.greeterU2fReason property string greeterU2fReason: "probe_failed"
readonly property string greeterU2fSource: Processes.greeterU2fSource property string greeterU2fSource: "none"
property string lockScreenActiveMonitor: "all" property string lockScreenActiveMonitor: "all"
property string lockScreenInactiveColor: "#000000" property string lockScreenInactiveColor: "#000000"
property int lockScreenNotificationMode: 0 property int lockScreenNotificationMode: 0
@@ -1128,6 +1089,7 @@ Singleton {
function refreshAuthAvailability() { function refreshAuthAvailability() {
if (isGreeterMode) if (isGreeterMode)
return; return;
Processes.settingsRoot = root;
Processes.detectAuthCapabilities(); Processes.detectAuthCapabilities();
} }
@@ -1349,7 +1311,6 @@ Singleton {
_loading = false; _loading = false;
} }
loadPluginSettings(); loadPluginSettings();
Qt.callLater(() => _reconcileConnectedFrameBarStyles());
} }
property var _pendingMigration: null property var _pendingMigration: null
@@ -1463,149 +1424,6 @@ Singleton {
pluginSettingsFile.setText(JSON.stringify(pluginSettings, null, 2)); pluginSettingsFile.setText(JSON.stringify(pluginSettings, null, 2));
} }
function _connectedFrameBarStyleSnapshot(config) {
return {
"shadowIntensity": config?.shadowIntensity ?? 0,
"squareCorners": config?.squareCorners ?? false,
"gothCornersEnabled": config?.gothCornersEnabled ?? false,
"borderEnabled": config?.borderEnabled ?? false
};
}
function _hasConnectedFrameBarStyleBackups() {
return connectedFrameBarStyleBackups && Object.keys(connectedFrameBarStyleBackups).length > 0;
}
function _captureConnectedFrameBarStyleBackups(configs, overwriteExisting) {
if (!Array.isArray(configs))
return;
const nextBackups = JSON.parse(JSON.stringify(connectedFrameBarStyleBackups || {}));
const validIds = {};
let changed = false;
for (let i = 0; i < configs.length; i++) {
const config = configs[i];
if (!config?.id)
continue;
validIds[config.id] = true;
if (!overwriteExisting && nextBackups[config.id] !== undefined)
continue;
const snapshot = _connectedFrameBarStyleSnapshot(config);
if (JSON.stringify(nextBackups[config.id]) !== JSON.stringify(snapshot)) {
nextBackups[config.id] = snapshot;
changed = true;
}
}
if (overwriteExisting) {
for (const barId in nextBackups) {
if (validIds[barId])
continue;
delete nextBackups[barId];
changed = true;
}
}
if (changed)
connectedFrameBarStyleBackups = nextBackups;
}
function _restoreConnectedFrameBarStyleBackups() {
if (!_hasConnectedFrameBarStyleBackups())
return;
const backups = connectedFrameBarStyleBackups || {};
const configs = JSON.parse(JSON.stringify(barConfigs));
let changed = false;
for (let i = 0; i < configs.length; i++) {
const backup = backups[configs[i].id];
if (!backup)
continue;
for (const key in backup) {
if (configs[i][key] === backup[key])
continue;
configs[i][key] = backup[key];
changed = true;
}
}
if (changed)
barConfigs = configs;
connectedFrameBarStyleBackups = ({});
if (changed)
updateBarConfigs();
}
function _reconcileConnectedFrameBarStyles() {
if (connectedFrameModeActive) {
if (!_hasConnectedFrameBarStyleBackups())
_captureConnectedFrameBarStyleBackups(barConfigs, true);
_enforceConnectedModeBarStyleReset();
return;
}
_restoreConnectedFrameBarStyleBackups();
}
function _sanitizeBarConfigForConnectedFrame(config) {
if (!connectedFrameModeActive || !config)
return config;
let changed = false;
const sanitized = Object.assign({}, config);
if ((sanitized.shadowIntensity ?? 0) !== 0) {
sanitized.shadowIntensity = 0;
changed = true;
}
if (sanitized.squareCorners ?? false) {
sanitized.squareCorners = false;
changed = true;
}
if (sanitized.gothCornersEnabled ?? false) {
sanitized.gothCornersEnabled = false;
changed = true;
}
if (sanitized.borderEnabled ?? false) {
sanitized.borderEnabled = false;
changed = true;
}
return changed ? sanitized : config;
}
function _sanitizeBarConfigsForConnectedFrame(configs) {
if (!connectedFrameModeActive || !Array.isArray(configs))
return {
"configs": configs,
"changed": false
};
let changed = false;
const sanitizedConfigs = configs.map(config => {
const sanitized = _sanitizeBarConfigForConnectedFrame(config);
if (sanitized !== config)
changed = true;
return sanitized;
});
return {
"configs": changed ? sanitizedConfigs : configs,
"changed": changed
};
}
function _enforceConnectedModeBarStyleReset() {
const result = _sanitizeBarConfigsForConnectedFrame(barConfigs);
if (!result.changed)
return;
barConfigs = result.configs;
updateBarConfigs();
}
function detectAvailableIconThemes() { function detectAvailableIconThemes() {
const xdgDataDirs = Quickshell.env("XDG_DATA_DIRS") || ""; const xdgDataDirs = Quickshell.env("XDG_DATA_DIRS") || "";
const localData = Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation)); const localData = Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation));
@@ -1753,37 +1571,35 @@ Singleton {
const spacing = barSpacing !== undefined ? barSpacing : (defaultBar?.spacing ?? 4); const spacing = barSpacing !== undefined ? barSpacing : (defaultBar?.spacing ?? 4);
const position = barPosition !== undefined ? barPosition : (defaultBar?.position ?? SettingsData.Position.Top); const position = barPosition !== undefined ? barPosition : (defaultBar?.position ?? SettingsData.Position.Top);
const rawBottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : (defaultBar?.bottomGap ?? 0)) : (defaultBar?.bottomGap ?? 0); const rawBottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : (defaultBar?.bottomGap ?? 0)) : (defaultBar?.bottomGap ?? 0);
const isConnected = connectedFrameModeActive; const bottomGap = Math.max(0, rawBottomGap);
const bottomGap = isConnected ? 0 : Math.max(0, rawBottomGap);
const useAutoGaps = (barConfig && barConfig.popupGapsAuto !== undefined) ? barConfig.popupGapsAuto : (defaultBar?.popupGapsAuto ?? true); const useAutoGaps = (barConfig && barConfig.popupGapsAuto !== undefined) ? barConfig.popupGapsAuto : (defaultBar?.popupGapsAuto ?? true);
const manualGapValue = (barConfig && barConfig.popupGapsManual !== undefined) ? barConfig.popupGapsManual : (defaultBar?.popupGapsManual ?? 4); const manualGapValue = (barConfig && barConfig.popupGapsManual !== undefined) ? barConfig.popupGapsManual : (defaultBar?.popupGapsManual ?? 4);
const popupGap = isConnected ? 0 : (useAutoGaps ? Math.max(4, spacing) : manualGapValue); const popupGap = useAutoGaps ? Math.max(4, spacing) : manualGapValue;
const edgeSpacing = isConnected ? 0 : spacing;
switch (position) { switch (position) {
case SettingsData.Position.Left: case SettingsData.Position.Left:
return { return {
"x": barThickness + edgeSpacing + popupGap, "x": barThickness + spacing + popupGap,
"y": relativeY, "y": relativeY,
"width": widgetWidth "width": widgetWidth
}; };
case SettingsData.Position.Right: case SettingsData.Position.Right:
return { return {
"x": (screen?.width || 0) - (barThickness + edgeSpacing + popupGap), "x": (screen?.width || 0) - (barThickness + spacing + popupGap),
"y": relativeY, "y": relativeY,
"width": widgetWidth "width": widgetWidth
}; };
case SettingsData.Position.Bottom: case SettingsData.Position.Bottom:
return { return {
"x": relativeX, "x": relativeX,
"y": (screen?.height || 0) - (barThickness + edgeSpacing + bottomGap + popupGap), "y": (screen?.height || 0) - (barThickness + spacing + bottomGap + popupGap),
"width": widgetWidth "width": widgetWidth
}; };
default: default:
return { return {
"x": relativeX, "x": relativeX,
"y": barThickness + edgeSpacing + bottomGap + popupGap, "y": barThickness + spacing + bottomGap + popupGap,
"width": widgetWidth "width": widgetWidth
}; };
} }
@@ -1877,9 +1693,7 @@ Singleton {
const screenWidth = screen.width; const screenWidth = screen.width;
const screenHeight = screen.height; const screenHeight = screen.height;
const position = barPosition !== undefined ? barPosition : (defaultBar?.position ?? SettingsData.Position.Top); const position = barPosition !== undefined ? barPosition : (defaultBar?.position ?? SettingsData.Position.Top);
const isConnected = connectedFrameModeActive; const bottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : (defaultBar?.bottomGap ?? 0)) : (defaultBar?.bottomGap ?? 0);
const rawBottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : (defaultBar?.bottomGap ?? 0)) : (defaultBar?.bottomGap ?? 0);
const bottomGap = isConnected ? 0 : rawBottomGap;
let topOffset = 0; let topOffset = 0;
let bottomOffset = 0; let bottomOffset = 0;
@@ -1901,7 +1715,7 @@ Singleton {
const otherSpacing = other.spacing !== undefined ? other.spacing : (defaultBar?.spacing ?? 4); const otherSpacing = other.spacing !== undefined ? other.spacing : (defaultBar?.spacing ?? 4);
const otherPadding = other.innerPadding !== undefined ? other.innerPadding : (defaultBar?.innerPadding ?? 4); const otherPadding = other.innerPadding !== undefined ? other.innerPadding : (defaultBar?.innerPadding ?? 4);
const otherThickness = Math.max(26 + otherPadding * 0.6, Theme.barHeight - 4 - (8 - otherPadding)) + otherSpacing + wingSize; const otherThickness = Math.max(26 + otherPadding * 0.6, Theme.barHeight - 4 - (8 - otherPadding)) + otherSpacing + wingSize;
const otherBottomGap = isConnected ? 0 : (other.bottomGap !== undefined ? other.bottomGap : (defaultBar?.bottomGap ?? 0)); const otherBottomGap = other.bottomGap !== undefined ? other.bottomGap : (defaultBar?.bottomGap ?? 0);
switch (other.position) { switch (other.position) {
case SettingsData.Position.Top: case SettingsData.Position.Top:
@@ -1992,9 +1806,7 @@ Singleton {
function addBarConfig(config) { function addBarConfig(config) {
const configs = JSON.parse(JSON.stringify(barConfigs)); const configs = JSON.parse(JSON.stringify(barConfigs));
configs.push(config); configs.push(config);
if (connectedFrameModeActive) barConfigs = configs;
_captureConnectedFrameBarStyleBackups(configs, false);
barConfigs = _sanitizeBarConfigsForConnectedFrame(configs).configs;
updateBarConfigs(); updateBarConfigs();
} }
@@ -2006,7 +1818,7 @@ Singleton {
const positionChanged = updates.position !== undefined && configs[index].position !== updates.position; const positionChanged = updates.position !== undefined && configs[index].position !== updates.position;
Object.assign(configs[index], updates); Object.assign(configs[index], updates);
barConfigs = _sanitizeBarConfigsForConnectedFrame(configs).configs; barConfigs = configs;
updateBarConfigs(); updateBarConfigs();
if (positionChanged) { if (positionChanged) {
@@ -2060,11 +1872,6 @@ Singleton {
return; return;
const configs = barConfigs.filter(cfg => cfg.id !== barId); const configs = barConfigs.filter(cfg => cfg.id !== barId);
barConfigs = configs; barConfigs = configs;
if (connectedFrameBarStyleBackups?.[barId] !== undefined) {
const nextBackups = JSON.parse(JSON.stringify(connectedFrameBarStyleBackups || {}));
delete nextBackups[barId];
connectedFrameBarStyleBackups = nextBackups;
}
updateBarConfigs(); updateBarConfigs();
} }
+1 -47
View File
@@ -960,40 +960,6 @@ Singleton {
"expressiveEffects": [0.34, 0.8, 0.34, 1, 1, 1] "expressiveEffects": [0.34, 0.8, 0.34, 1, 1, 1]
} }
// Delegates to AnimVariants.qml for curves, timing, scale, and offsets.
readonly property list<real> variantEnterCurve: AnimVariants.variantEnterCurve
readonly property list<real> variantExitCurve: AnimVariants.variantExitCurve
readonly property list<real> variantModalEnterCurve: AnimVariants.variantModalEnterCurve
readonly property list<real> variantModalExitCurve: AnimVariants.variantModalExitCurve
readonly property list<real> variantPopoutEnterCurve: AnimVariants.variantPopoutEnterCurve
readonly property list<real> variantPopoutExitCurve: AnimVariants.variantPopoutExitCurve
readonly property real variantEnterDurationFactor: AnimVariants.variantEnterDurationFactor
readonly property real variantExitDurationFactor: AnimVariants.variantExitDurationFactor
readonly property real variantOpacityDurationScale: AnimVariants.variantOpacityDurationScale
readonly property bool isDirectionalEffect: AnimVariants.isDirectionalEffect
readonly property bool isDepthEffect: AnimVariants.isDepthEffect
readonly property bool isConnectedEffect: AnimVariants.isConnectedEffect
readonly property real connectedCornerRadius: {
if (typeof SettingsData === "undefined") return 12;
return SettingsData.connectedFrameModeActive ? SettingsData.frameRounding : cornerRadius;
}
readonly property color connectedSurfaceColor: {
if (typeof SettingsData === "undefined")
return withAlpha(surfaceContainer, popupTransparency);
return isConnectedEffect
? Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity)
: withAlpha(surfaceContainer, popupTransparency);
}
readonly property real connectedSurfaceRadius: isConnectedEffect ? connectedCornerRadius : cornerRadius
readonly property bool connectedSurfaceBlurEnabled: (typeof SettingsData === "undefined")
? true
: (!isConnectedEffect || SettingsData.frameBlurEnabled)
readonly property real effectScaleCollapsed: AnimVariants.effectScaleCollapsed
readonly property real effectAnimOffset: AnimVariants.effectAnimOffset
function variantDuration(baseDuration, entering) { return AnimVariants.variantDuration(baseDuration, entering); }
function variantExitCleanupPadding() { return AnimVariants.variantExitCleanupPadding(); }
function variantCloseInterval(baseDuration) { return AnimVariants.variantCloseInterval(baseDuration); }
readonly property var animationPresetDurations: { readonly property var animationPresetDurations: {
"none": 0, "none": 0,
"short": 250, "short": 250,
@@ -1159,13 +1125,7 @@ Singleton {
property real iconSizeLarge: 32 property real iconSizeLarge: 32
property real panelTransparency: 0.85 property real panelTransparency: 0.85
property real popupTransparency: { property real popupTransparency: typeof SettingsData !== "undefined" && SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 1.0
if (typeof SettingsData === "undefined")
return 1.0;
if (isConnectedEffect)
return SettingsData.frameOpacity !== undefined ? SettingsData.frameOpacity : 1.0;
return SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 1.0;
}
function screenTransition() { function screenTransition() {
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
@@ -1864,12 +1824,6 @@ Singleton {
return Qt.rgba(c.r, c.g, c.b, a); return Qt.rgba(c.r, c.g, c.b, a);
} }
function popupLayerColor(baseColor) {
if (isConnectedEffect)
return connectedSurfaceColor;
return withAlpha(baseColor, popupTransparency);
}
function blendAlpha(c, a) { function blendAlpha(c, a) {
return Qt.rgba(c.r, c.g, c.b, c.a * a); return Qt.rgba(c.r, c.g, c.b, c.a * a);
} }
+332 -225
View File
@@ -25,10 +25,16 @@ Singleton {
property string fingerprintProbeOutput: "" property string fingerprintProbeOutput: ""
property int fingerprintProbeExitCode: 0 property int fingerprintProbeExitCode: 0
property bool fingerprintProbeFinalized: false property bool fingerprintProbeStreamFinished: false
property bool fingerprintProbeExited: false
property string fingerprintProbeState: "probe_failed"
property string pamProbeOutput: "" property string pamSupportProbeOutput: ""
property bool pamProbeFinalized: false property bool pamSupportProbeStreamFinished: false
property bool pamSupportProbeExited: false
property int pamSupportProbeExitCode: 0
property bool pamFprintSupportDetected: false
property bool pamU2fSupportDetected: false
readonly property string homeDir: Quickshell.env("HOME") || "" readonly property string homeDir: Quickshell.env("HOME") || ""
readonly property string u2fKeysPath: homeDir ? homeDir + "/.config/Yubico/u2f_keys" : "" readonly property string u2fKeysPath: homeDir ? homeDir + "/.config/Yubico/u2f_keys" : ""
@@ -48,189 +54,40 @@ Singleton {
readonly property var forcedFprintAvailable: envFlag("DMS_FORCE_FPRINT_AVAILABLE") readonly property var forcedFprintAvailable: envFlag("DMS_FORCE_FPRINT_AVAILABLE")
readonly property var forcedU2fAvailable: envFlag("DMS_FORCE_U2F_AVAILABLE") readonly property var forcedU2fAvailable: envFlag("DMS_FORCE_U2F_AVAILABLE")
property bool authApplyRunning: false
property bool authApplyQueued: false
property bool authApplyRerunRequested: false
property bool authApplyTerminalFallbackFromPrecheck: false
property string authApplyStdout: ""
property string authApplyStderr: ""
property string authApplySudoProbeStderr: ""
property string authApplyTerminalFallbackStderr: ""
// --- Derived auth probe state --- function detectQtTools() {
qtToolsDetectionProcess.running = true;
readonly property bool pamFprintSupportDetected: pamProbeFinalized && pamProbeOutput.includes("pam_fprintd.so:true")
readonly property bool pamU2fSupportDetected: pamProbeFinalized && pamProbeOutput.includes("pam_u2f.so:true")
readonly property string fingerprintProbeState: {
if (forcedFprintAvailable !== null)
return forcedFprintAvailable ? "ready" : "probe_failed";
if (!fingerprintProbeFinalized)
return "probe_failed";
return parseFingerprintProbe(fingerprintProbeExitCode, fingerprintProbeOutput, pamFprintSupportDetected);
} }
// --- Lock fingerprint capabilities ---
readonly property bool lockFingerprintCanEnable: {
if (forcedFprintAvailable !== null)
return forcedFprintAvailable;
switch (fingerprintProbeState) {
case "ready":
case "missing_enrollment":
return true;
default:
return false;
}
}
readonly property bool lockFingerprintReady: {
if (forcedFprintAvailable !== null)
return forcedFprintAvailable;
return fingerprintProbeState === "ready";
}
readonly property string lockFingerprintReason: {
if (forcedFprintAvailable !== null)
return forcedFprintAvailable ? "ready" : "probe_failed";
return fingerprintProbeState;
}
// --- Greeter fingerprint capabilities ---
readonly property bool greeterFingerprintCanEnable: {
if (forcedFprintAvailable !== null)
return forcedFprintAvailable;
if (greeterPamHasFprint)
return fingerprintProbeState !== "missing_reader";
switch (fingerprintProbeState) {
case "ready":
case "missing_enrollment":
return true;
default:
return false;
}
}
readonly property bool greeterFingerprintReady: {
if (forcedFprintAvailable !== null)
return forcedFprintAvailable;
return fingerprintProbeState === "ready";
}
readonly property string greeterFingerprintReason: {
if (forcedFprintAvailable !== null)
return forcedFprintAvailable ? "ready" : "probe_failed";
if (greeterPamHasFprint) {
switch (fingerprintProbeState) {
case "ready":
return "configured_externally";
case "missing_enrollment":
return "missing_enrollment";
case "missing_reader":
return "missing_reader";
default:
return "probe_failed";
}
}
return fingerprintProbeState;
}
readonly property string greeterFingerprintSource: {
if (forcedFprintAvailable !== null)
return forcedFprintAvailable ? "dms" : "none";
if (greeterPamHasFprint)
return "pam";
switch (fingerprintProbeState) {
case "ready":
case "missing_enrollment":
return "dms";
default:
return "none";
}
}
// --- Lock U2F capabilities ---
readonly property bool lockU2fReady: {
if (forcedU2fAvailable !== null)
return forcedU2fAvailable;
return lockU2fCustomConfigDetected || homeU2fKeysDetected;
}
readonly property bool lockU2fCanEnable: {
if (forcedU2fAvailable !== null)
return forcedU2fAvailable;
return lockU2fReady || pamU2fSupportDetected;
}
readonly property string lockU2fReason: {
if (forcedU2fAvailable !== null)
return forcedU2fAvailable ? "ready" : "probe_failed";
if (lockU2fReady)
return "ready";
if (lockU2fCanEnable)
return "missing_key_registration";
return "missing_pam_support";
}
// --- Greeter U2F capabilities ---
readonly property bool greeterU2fReady: {
if (forcedU2fAvailable !== null)
return forcedU2fAvailable;
if (greeterPamHasU2f)
return true;
return homeU2fKeysDetected;
}
readonly property bool greeterU2fCanEnable: {
if (forcedU2fAvailable !== null)
return forcedU2fAvailable;
if (greeterPamHasU2f)
return true;
return greeterU2fReady || pamU2fSupportDetected;
}
readonly property string greeterU2fReason: {
if (forcedU2fAvailable !== null)
return forcedU2fAvailable ? "ready" : "probe_failed";
if (greeterPamHasU2f)
return "configured_externally";
if (greeterU2fReady)
return "ready";
if (greeterU2fCanEnable)
return "missing_key_registration";
return "missing_pam_support";
}
readonly property string greeterU2fSource: {
if (forcedU2fAvailable !== null)
return forcedU2fAvailable ? "dms" : "none";
if (greeterPamHasU2f)
return "pam";
if (greeterU2fCanEnable)
return "dms";
return "none";
}
// --- Aggregates ---
readonly property bool fprintdAvailable: lockFingerprintReady || greeterFingerprintReady
readonly property bool u2fAvailable: lockU2fReady || greeterU2fReady
// --- Auth detection ---
readonly property var _fprintProbeCommand: ["sh", "-c", "if command -v fprintd-list >/dev/null 2>&1; then fprintd-list \"${USER:-$(id -un)}\" 2>&1; else printf '__missing_command__\\n'; exit 127; fi"]
readonly property var _pamProbeCommand: ["sh", "-c", "for module in pam_fprintd.so pam_u2f.so; do found=false; for dir in /usr/lib64/security /usr/lib/security /lib/security /lib/x86_64-linux-gnu/security /usr/lib/x86_64-linux-gnu/security /usr/lib/aarch64-linux-gnu/security /run/current-system/sw/lib/security; do if [ -f \"$dir/$module\" ]; then found=true; break; fi; done; printf '%s:%s\\n' \"$module\" \"$found\"; done"]
function detectAuthCapabilities() { function detectAuthCapabilities() {
if (!settingsRoot)
return;
if (forcedFprintAvailable === null) { if (forcedFprintAvailable === null) {
fingerprintProbeFinalized = false; fingerprintProbeOutput = "";
Proc.runCommand("fprint-probe", _fprintProbeCommand, (output, exitCode) => { fingerprintProbeStreamFinished = false;
fingerprintProbeOutput = output || ""; fingerprintProbeExited = false;
fingerprintProbeExitCode = exitCode; fingerprintProbeProcess.running = true;
fingerprintProbeFinalized = true; } else {
}, 0); fingerprintProbeState = forcedFprintAvailable ? "ready" : "probe_failed";
} }
pamProbeFinalized = false; pamFprintSupportDetected = false;
Proc.runCommand("pam-probe", _pamProbeCommand, (output, _exitCode) => { pamU2fSupportDetected = false;
pamProbeOutput = output || ""; pamSupportProbeOutput = "";
pamProbeFinalized = true; pamSupportProbeStreamFinished = false;
}, 0); pamSupportProbeExited = false;
pamSupportDetectionProcess.running = true;
recomputeAuthCapabilities();
} }
function detectFprintd() { function detectFprintd() {
@@ -241,16 +98,9 @@ Singleton {
detectAuthCapabilities(); detectAuthCapabilities();
} }
// --- Auth apply pipeline --- function checkPluginSettings() {
pluginSettingsCheckProcess.running = true;
property bool authApplyRunning: false }
property bool authApplyQueued: false
property bool authApplyRerunRequested: false
property bool authApplyTerminalFallbackFromPrecheck: false
property string authApplyStdout: ""
property string authApplyStderr: ""
property string authApplySudoProbeStderr: ""
property string authApplyTerminalFallbackStderr: ""
function scheduleAuthApply() { function scheduleAuthApply() {
if (!settingsRoot || settingsRoot.isGreeterMode) if (!settingsRoot || settingsRoot.isGreeterMode)
@@ -296,8 +146,6 @@ Singleton {
authApplyDebounce.restart(); authApplyDebounce.restart();
} }
// --- PAM parsing helpers ---
function stripPamComment(line) { function stripPamComment(line) {
if (!line) if (!line)
return ""; return "";
@@ -341,7 +189,15 @@ Singleton {
function greeterPamStackHasModule(moduleName) { function greeterPamStackHasModule(moduleName) {
if (pamModuleEnabled(greetdPamText, moduleName)) if (pamModuleEnabled(greetdPamText, moduleName))
return true; return true;
const includedPamStacks = [["system-auth", systemAuthPamText], ["common-auth", commonAuthPamText], ["password-auth", passwordAuthPamText], ["system-login", systemLoginPamText], ["system-local-login", systemLocalLoginPamText], ["common-auth-pc", commonAuthPcPamText], ["login", loginPamText]]; const includedPamStacks = [
["system-auth", systemAuthPamText],
["common-auth", commonAuthPamText],
["password-auth", passwordAuthPamText],
["system-login", systemLoginPamText],
["system-local-login", systemLocalLoginPamText],
["common-auth-pc", commonAuthPcPamText],
["login", loginPamText]
];
for (let i = 0; i < includedPamStacks.length; i++) { for (let i = 0; i < includedPamStacks.length; i++) {
const stack = includedPamStacks[i]; const stack = includedPamStacks[i];
if (pamTextIncludesFile(greetdPamText, stack[0]) && pamModuleEnabled(stack[1], moduleName)) if (pamTextIncludesFile(greetdPamText, stack[0]) && pamModuleEnabled(stack[1], moduleName))
@@ -350,8 +206,6 @@ Singleton {
return false; return false;
} }
// --- Fingerprint probe output parsing ---
function hasEnrolledFingerprintOutput(output) { function hasEnrolledFingerprintOutput(output) {
const lower = (output || "").toLowerCase(); const lower = (output || "").toLowerCase();
if (lower.includes("has fingers enrolled") || lower.includes("has fingerprints enrolled")) if (lower.includes("has fingers enrolled") || lower.includes("has fingerprints enrolled"))
@@ -369,15 +223,21 @@ Singleton {
function hasMissingFingerprintEnrollmentOutput(output) { function hasMissingFingerprintEnrollmentOutput(output) {
const lower = (output || "").toLowerCase(); const lower = (output || "").toLowerCase();
return lower.includes("no fingers enrolled") || lower.includes("no fingerprints enrolled") || lower.includes("no prints enrolled"); return lower.includes("no fingers enrolled")
|| lower.includes("no fingerprints enrolled")
|| lower.includes("no prints enrolled");
} }
function hasMissingFingerprintReaderOutput(output) { function hasMissingFingerprintReaderOutput(output) {
const lower = (output || "").toLowerCase(); const lower = (output || "").toLowerCase();
return lower.includes("no devices available") || lower.includes("no device available") || lower.includes("no devices found") || lower.includes("list_devices failed") || lower.includes("no device"); return lower.includes("no devices available")
|| lower.includes("no device available")
|| lower.includes("no devices found")
|| lower.includes("list_devices failed")
|| lower.includes("no device");
} }
function parseFingerprintProbe(exitCode, output, pamFprintDetected) { function parseFingerprintProbe(exitCode, output) {
if (hasEnrolledFingerprintOutput(output)) if (hasEnrolledFingerprintOutput(output))
return "ready"; return "ready";
if (hasMissingFingerprintEnrollmentOutput(output)) if (hasMissingFingerprintEnrollmentOutput(output))
@@ -388,17 +248,164 @@ Singleton {
return "missing_enrollment"; return "missing_enrollment";
if (exitCode === 127 || (output || "").includes("__missing_command__")) if (exitCode === 127 || (output || "").includes("__missing_command__"))
return "probe_failed"; return "probe_failed";
return pamFprintDetected ? "probe_failed" : "missing_pam_support"; return pamFprintSupportDetected ? "probe_failed" : "missing_pam_support";
} }
// --- Qt tools detection --- function setLockFingerprintCapability(canEnable, ready, reason) {
settingsRoot.lockFingerprintCanEnable = canEnable;
function detectQtTools() { settingsRoot.lockFingerprintReady = ready;
qtToolsDetectionProcess.running = true; settingsRoot.lockFingerprintReason = reason;
} }
function checkPluginSettings() { function setLockU2fCapability(canEnable, ready, reason) {
pluginSettingsCheckProcess.running = true; settingsRoot.lockU2fCanEnable = canEnable;
settingsRoot.lockU2fReady = ready;
settingsRoot.lockU2fReason = reason;
}
function setGreeterFingerprintCapability(canEnable, ready, reason, source) {
settingsRoot.greeterFingerprintCanEnable = canEnable;
settingsRoot.greeterFingerprintReady = ready;
settingsRoot.greeterFingerprintReason = reason;
settingsRoot.greeterFingerprintSource = source;
}
function setGreeterU2fCapability(canEnable, ready, reason, source) {
settingsRoot.greeterU2fCanEnable = canEnable;
settingsRoot.greeterU2fReady = ready;
settingsRoot.greeterU2fReason = reason;
settingsRoot.greeterU2fSource = source;
}
function recomputeFingerprintCapabilities() {
if (forcedFprintAvailable !== null) {
const reason = forcedFprintAvailable ? "ready" : "probe_failed";
const source = forcedFprintAvailable ? "dms" : "none";
setLockFingerprintCapability(forcedFprintAvailable, forcedFprintAvailable, reason);
setGreeterFingerprintCapability(forcedFprintAvailable, forcedFprintAvailable, reason, source);
return;
}
const state = fingerprintProbeState;
switch (state) {
case "ready":
setLockFingerprintCapability(true, true, "ready");
break;
case "missing_enrollment":
setLockFingerprintCapability(true, false, "missing_enrollment");
break;
case "missing_reader":
setLockFingerprintCapability(false, false, "missing_reader");
break;
case "missing_pam_support":
setLockFingerprintCapability(false, false, "missing_pam_support");
break;
default:
setLockFingerprintCapability(false, false, "probe_failed");
break;
}
if (greeterPamHasFprint) {
switch (state) {
case "ready":
setGreeterFingerprintCapability(true, true, "configured_externally", "pam");
break;
case "missing_enrollment":
setGreeterFingerprintCapability(true, false, "missing_enrollment", "pam");
break;
case "missing_reader":
setGreeterFingerprintCapability(false, false, "missing_reader", "pam");
break;
default:
setGreeterFingerprintCapability(true, false, "probe_failed", "pam");
break;
}
return;
}
switch (state) {
case "ready":
setGreeterFingerprintCapability(true, true, "ready", "dms");
break;
case "missing_enrollment":
setGreeterFingerprintCapability(true, false, "missing_enrollment", "dms");
break;
case "missing_reader":
setGreeterFingerprintCapability(false, false, "missing_reader", "none");
break;
case "missing_pam_support":
setGreeterFingerprintCapability(false, false, "missing_pam_support", "none");
break;
default:
setGreeterFingerprintCapability(false, false, "probe_failed", "none");
break;
}
}
function recomputeU2fCapabilities() {
if (forcedU2fAvailable !== null) {
const reason = forcedU2fAvailable ? "ready" : "probe_failed";
const source = forcedU2fAvailable ? "dms" : "none";
setLockU2fCapability(forcedU2fAvailable, forcedU2fAvailable, reason);
setGreeterU2fCapability(forcedU2fAvailable, forcedU2fAvailable, reason, source);
return;
}
const lockReady = lockU2fCustomConfigDetected || homeU2fKeysDetected;
const lockCanEnable = lockReady || pamU2fSupportDetected;
const lockReason = lockReady ? "ready" : (lockCanEnable ? "missing_key_registration" : "missing_pam_support");
setLockU2fCapability(lockCanEnable, lockReady, lockReason);
if (greeterPamHasU2f) {
setGreeterU2fCapability(true, true, "configured_externally", "pam");
return;
}
const greeterReady = homeU2fKeysDetected;
const greeterCanEnable = greeterReady || pamU2fSupportDetected;
const greeterReason = greeterReady ? "ready" : (greeterCanEnable ? "missing_key_registration" : "missing_pam_support");
setGreeterU2fCapability(greeterCanEnable, greeterReady, greeterReason, greeterCanEnable ? "dms" : "none");
}
function recomputeAuthCapabilities() {
if (!settingsRoot)
return;
recomputeFingerprintCapabilities();
recomputeU2fCapabilities();
settingsRoot.fprintdAvailable = settingsRoot.lockFingerprintReady || settingsRoot.greeterFingerprintReady;
settingsRoot.u2fAvailable = settingsRoot.lockU2fReady || settingsRoot.greeterU2fReady;
}
function finalizeFingerprintProbe() {
if (!fingerprintProbeStreamFinished || !fingerprintProbeExited)
return;
fingerprintProbeState = parseFingerprintProbe(fingerprintProbeExitCode, fingerprintProbeOutput);
recomputeAuthCapabilities();
}
function finalizePamSupportProbe() {
if (!pamSupportProbeStreamFinished || !pamSupportProbeExited)
return;
pamFprintSupportDetected = false;
pamU2fSupportDetected = false;
const lines = (pamSupportProbeOutput || "").trim().split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const parts = lines[i].split(":");
if (parts.length !== 2)
continue;
if (parts[0] === "pam_fprintd.so")
pamFprintSupportDetected = parts[1] === "true";
else if (parts[0] === "pam_u2f.so")
pamU2fSupportDetected = parts[1] === "true";
}
if (forcedFprintAvailable === null && fingerprintProbeState === "missing_pam_support")
fingerprintProbeState = parseFingerprintProbe(fingerprintProbeExitCode, fingerprintProbeOutput);
recomputeAuthCapabilities();
} }
property var qtToolsDetectionProcess: Process { property var qtToolsDetectionProcess: Process {
@@ -426,6 +433,44 @@ Singleton {
} }
} }
property var fingerprintProbeProcess: Process {
command: ["sh", "-c", "if command -v fprintd-list >/dev/null 2>&1; then fprintd-list \"${USER:-$(id -un)}\" 2>&1; else printf '__missing_command__\\n'; exit 127; fi"]
running: false
stdout: StdioCollector {
onStreamFinished: {
root.fingerprintProbeOutput = text || "";
root.fingerprintProbeStreamFinished = true;
root.finalizeFingerprintProbe();
}
}
onExited: function (exitCode) {
root.fingerprintProbeExitCode = exitCode;
root.fingerprintProbeExited = true;
root.finalizeFingerprintProbe();
}
}
property var pamSupportDetectionProcess: Process {
command: ["sh", "-c", "for module in pam_fprintd.so pam_u2f.so; do found=false; for dir in /usr/lib64/security /usr/lib/security /lib/security /lib/x86_64-linux-gnu/security /usr/lib/x86_64-linux-gnu/security /usr/lib/aarch64-linux-gnu/security /run/current-system/sw/lib/security; do if [ -f \"$dir/$module\" ]; then found=true; break; fi; done; printf '%s:%s\\n' \"$module\" \"$found\"; done"]
running: false
stdout: StdioCollector {
onStreamFinished: {
root.pamSupportProbeOutput = text || "";
root.pamSupportProbeStreamFinished = true;
root.finalizePamSupportProbe();
}
}
onExited: function (exitCode) {
root.pamSupportProbeExitCode = exitCode;
root.pamSupportProbeExited = true;
root.finalizePamSupportProbe();
}
}
Timer { Timer {
id: authApplyDebounce id: authApplyDebounce
interval: 300 interval: 300
@@ -499,7 +544,9 @@ Singleton {
onExited: exitCode => { onExited: exitCode => {
if (exitCode === 0) { if (exitCode === 0) {
const message = root.authApplyTerminalFallbackFromPrecheck ? I18n.tr("Terminal opened. Complete authentication setup there; it will close automatically when done.") : I18n.tr("Terminal fallback opened. Complete authentication setup there; it will close automatically when done."); const message = root.authApplyTerminalFallbackFromPrecheck
? I18n.tr("Terminal opened. Complete authentication setup there; it will close automatically when done.")
: I18n.tr("Terminal fallback opened. Complete authentication setup there; it will close automatically when done.");
ToastService.showInfo(message, "", "", "auth-sync"); ToastService.showInfo(message, "", "", "auth-sync");
} else { } else {
let details = (root.authApplyTerminalFallbackStderr || "").trim(); let details = (root.authApplyTerminalFallbackStderr || "").trim();
@@ -513,80 +560,140 @@ Singleton {
id: greetdPamWatcher id: greetdPamWatcher
path: "/etc/pam.d/greetd" path: "/etc/pam.d/greetd"
printErrors: false printErrors: false
onLoaded: root.greetdPamText = text() onLoaded: {
onLoadFailed: root.greetdPamText = "" root.greetdPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.greetdPamText = "";
root.recomputeAuthCapabilities();
}
} }
FileView { FileView {
id: systemAuthPamWatcher id: systemAuthPamWatcher
path: "/etc/pam.d/system-auth" path: "/etc/pam.d/system-auth"
printErrors: false printErrors: false
onLoaded: root.systemAuthPamText = text() onLoaded: {
onLoadFailed: root.systemAuthPamText = "" root.systemAuthPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.systemAuthPamText = "";
root.recomputeAuthCapabilities();
}
} }
FileView { FileView {
id: commonAuthPamWatcher id: commonAuthPamWatcher
path: "/etc/pam.d/common-auth" path: "/etc/pam.d/common-auth"
printErrors: false printErrors: false
onLoaded: root.commonAuthPamText = text() onLoaded: {
onLoadFailed: root.commonAuthPamText = "" root.commonAuthPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.commonAuthPamText = "";
root.recomputeAuthCapabilities();
}
} }
FileView { FileView {
id: passwordAuthPamWatcher id: passwordAuthPamWatcher
path: "/etc/pam.d/password-auth" path: "/etc/pam.d/password-auth"
printErrors: false printErrors: false
onLoaded: root.passwordAuthPamText = text() onLoaded: {
onLoadFailed: root.passwordAuthPamText = "" root.passwordAuthPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.passwordAuthPamText = "";
root.recomputeAuthCapabilities();
}
} }
FileView { FileView {
id: systemLoginPamWatcher id: systemLoginPamWatcher
path: "/etc/pam.d/system-login" path: "/etc/pam.d/system-login"
printErrors: false printErrors: false
onLoaded: root.systemLoginPamText = text() onLoaded: {
onLoadFailed: root.systemLoginPamText = "" root.systemLoginPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.systemLoginPamText = "";
root.recomputeAuthCapabilities();
}
} }
FileView { FileView {
id: systemLocalLoginPamWatcher id: systemLocalLoginPamWatcher
path: "/etc/pam.d/system-local-login" path: "/etc/pam.d/system-local-login"
printErrors: false printErrors: false
onLoaded: root.systemLocalLoginPamText = text() onLoaded: {
onLoadFailed: root.systemLocalLoginPamText = "" root.systemLocalLoginPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.systemLocalLoginPamText = "";
root.recomputeAuthCapabilities();
}
} }
FileView { FileView {
id: commonAuthPcPamWatcher id: commonAuthPcPamWatcher
path: "/etc/pam.d/common-auth-pc" path: "/etc/pam.d/common-auth-pc"
printErrors: false printErrors: false
onLoaded: root.commonAuthPcPamText = text() onLoaded: {
onLoadFailed: root.commonAuthPcPamText = "" root.commonAuthPcPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.commonAuthPcPamText = "";
root.recomputeAuthCapabilities();
}
} }
FileView { FileView {
id: loginPamWatcher id: loginPamWatcher
path: "/etc/pam.d/login" path: "/etc/pam.d/login"
printErrors: false printErrors: false
onLoaded: root.loginPamText = text() onLoaded: {
onLoadFailed: root.loginPamText = "" root.loginPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.loginPamText = "";
root.recomputeAuthCapabilities();
}
} }
FileView { FileView {
id: dankshellU2fPamWatcher id: dankshellU2fPamWatcher
path: "/etc/pam.d/dankshell-u2f" path: "/etc/pam.d/dankshell-u2f"
printErrors: false printErrors: false
onLoaded: root.dankshellU2fPamText = text() onLoaded: {
onLoadFailed: root.dankshellU2fPamText = "" root.dankshellU2fPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.dankshellU2fPamText = "";
root.recomputeAuthCapabilities();
}
} }
FileView { FileView {
id: u2fKeysWatcher id: u2fKeysWatcher
path: root.u2fKeysPath path: root.u2fKeysPath
printErrors: false printErrors: false
onLoaded: root.u2fKeysText = text() onLoaded: {
onLoadFailed: root.u2fKeysText = "" root.u2fKeysText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.u2fKeysText = "";
root.recomputeAuthCapabilities();
}
} }
property var pluginSettingsCheckProcess: Process { property var pluginSettingsCheckProcess: Process {
@@ -75,8 +75,6 @@ var SPEC = {
vpnLastConnected: { def: "" }, vpnLastConnected: { def: "" },
lastPlayerIdentity: { def: "" },
deviceMaxVolumes: { def: {} }, deviceMaxVolumes: { def: {} },
hiddenOutputDeviceNames: { def: [] }, hiddenOutputDeviceNames: { def: [] },
hiddenInputDeviceNames: { def: [] }, hiddenInputDeviceNames: { def: [] },
+16 -9
View File
@@ -49,10 +49,6 @@ var SPEC = {
modalAnimationSpeed: { def: 1 }, modalAnimationSpeed: { def: 1 },
modalCustomAnimationDuration: { def: 150 }, modalCustomAnimationDuration: { def: 150 },
enableRippleEffects: { def: true }, enableRippleEffects: { def: true },
animationVariant: { def: 0 },
motionEffect: { def: 0 },
directionalAnimationMode: { def: 0 },
previousDirectionalMode: { def: 1 },
m3ElevationEnabled: { def: true }, m3ElevationEnabled: { def: true },
m3ElevationIntensity: { def: 12 }, m3ElevationIntensity: { def: 12 },
m3ElevationOpacity: { def: 30 }, m3ElevationOpacity: { def: 30 },
@@ -144,7 +140,6 @@ var SPEC = {
workspaceNameIcons: { def: {} }, workspaceNameIcons: { def: {} },
waveProgressEnabled: { def: true }, waveProgressEnabled: { def: true },
scrollTitleEnabled: { def: true }, scrollTitleEnabled: { def: true },
mediaAdaptiveWidthEnabled: { def: true },
audioVisualizerEnabled: { def: true }, audioVisualizerEnabled: { def: true },
audioScrollMode: { def: "volume" }, audioScrollMode: { def: "volume" },
audioWheelScrollAmount: { def: 5 }, audioWheelScrollAmount: { def: 5 },
@@ -208,8 +203,6 @@ var SPEC = {
dankLauncherV2BorderColor: { def: "primary" }, dankLauncherV2BorderColor: { def: "primary" },
dankLauncherV2ShowFooter: { def: true }, dankLauncherV2ShowFooter: { def: true },
dankLauncherV2UnloadOnClose: { def: false }, dankLauncherV2UnloadOnClose: { def: false },
dankLauncherV2IncludeFilesInAll: { def: false },
dankLauncherV2IncludeFoldersInAll: { def: false },
useAutoLocation: { def: false }, useAutoLocation: { def: false },
weatherEnabled: { def: true }, weatherEnabled: { def: true },
@@ -249,7 +242,6 @@ var SPEC = {
soundsEnabled: { def: true }, soundsEnabled: { def: true },
useSystemSoundTheme: { def: false }, useSystemSoundTheme: { def: false },
soundLogin: { def: false },
soundNewNotification: { def: true }, soundNewNotification: { def: true },
soundVolumeChanged: { def: true }, soundVolumeChanged: { def: true },
soundPluggedIn: { def: true }, soundPluggedIn: { def: true },
@@ -368,8 +360,24 @@ var SPEC = {
lockAtStartup: { def: false }, lockAtStartup: { def: false },
enableFprint: { def: false, onChange: "scheduleAuthApply" }, enableFprint: { def: false, onChange: "scheduleAuthApply" },
maxFprintTries: { def: 15 }, maxFprintTries: { def: 15 },
fprintdAvailable: { def: false, persist: false },
lockFingerprintCanEnable: { def: false, persist: false },
lockFingerprintReady: { def: false, persist: false },
lockFingerprintReason: { def: "probe_failed", persist: false },
greeterFingerprintCanEnable: { def: false, persist: false },
greeterFingerprintReady: { def: false, persist: false },
greeterFingerprintReason: { def: "probe_failed", persist: false },
greeterFingerprintSource: { def: "none", persist: false },
enableU2f: { def: false, onChange: "scheduleAuthApply" }, enableU2f: { def: false, onChange: "scheduleAuthApply" },
u2fMode: { def: "or" }, u2fMode: { def: "or" },
u2fAvailable: { def: false, persist: false },
lockU2fCanEnable: { def: false, persist: false },
lockU2fReady: { def: false, persist: false },
lockU2fReason: { def: "probe_failed", persist: false },
greeterU2fCanEnable: { def: false, persist: false },
greeterU2fReady: { def: false, persist: false },
greeterU2fReason: { def: "probe_failed", persist: false },
greeterU2fSource: { def: "none", persist: false },
lockScreenActiveMonitor: { def: "all" }, lockScreenActiveMonitor: { def: "all" },
lockScreenInactiveColor: { def: "#000000" }, lockScreenInactiveColor: { def: "#000000" },
lockScreenNotificationMode: { def: 0 }, lockScreenNotificationMode: { def: 0 },
@@ -433,7 +441,6 @@ var SPEC = {
displayProfileAutoSelect: { def: false }, displayProfileAutoSelect: { def: false },
displayShowDisconnected: { def: false }, displayShowDisconnected: { def: false },
displaySnapToEdge: { def: true }, displaySnapToEdge: { def: true },
connectedFrameBarStyleBackups: { def: {} },
barConfigs: { barConfigs: {
def: [{ def: [{
-12
View File
@@ -224,22 +224,10 @@ Item {
} }
} }
Timer {
id: loginSoundTimer
// Half a second delay before playing login sound, otherwise the sound may be cut off
// 50 is the minimum that seems to work, but 500 is safer
interval: 500
repeat: false
onTriggered: {
AudioService.playLoginSoundIfApplicable();
}
}
Component.onCompleted: { Component.onCompleted: {
dockRecreateDebounce.start(); dockRecreateDebounce.start();
// Force PolkitService singleton to initialize // Force PolkitService singleton to initialize
PolkitService.polkitAvailable; PolkitService.polkitAvailable;
loginSoundTimer.start();
} }
Loader { Loader {
+3 -1
View File
@@ -369,7 +369,9 @@ Item {
} }
function previous(): void { function previous(): void {
MprisController.previousOrRewind(); if (MprisController.activePlayer && MprisController.activePlayer.canGoPrevious) {
MprisController.activePlayer.previous();
}
} }
function next(): void { function next(): void {
@@ -122,7 +122,7 @@ Item {
} }
StyledText { StyledText {
text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No recent clipboard entries found") : I18n.tr("Connecting to clipboard service…") text: I18n.tr("No recent clipboard entries found")
anchors.centerIn: parent anchors.centerIn: parent
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
@@ -181,7 +181,7 @@ Item {
} }
StyledText { StyledText {
text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No saved clipboard entries") : I18n.tr("Connecting to clipboard service…") text: I18n.tr("No saved clipboard entries")
anchors.centerIn: parent anchors.centerIn: parent
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
@@ -60,23 +60,18 @@ DankModal {
} }
function show() { function show() {
if (!clipboardAvailable) {
ToastService.showError(I18n.tr("Clipboard service not available"));
return;
}
open(); open();
activeImageLoads = 0; activeImageLoads = 0;
shouldHaveFocus = true; shouldHaveFocus = true;
ClipboardService.reset(); ClipboardService.reset();
ClipboardService.refresh();
keyboardController.reset(); keyboardController.reset();
Qt.callLater(function () { Qt.callLater(function () {
if (clipboardAvailable) {
if (Theme.isConnectedEffect) {
Qt.callLater(() => {
if (clipboardHistoryModal.shouldBeVisible)
ClipboardService.refresh();
});
} else {
ClipboardService.refresh();
}
}
if (contentLoader.item?.searchField) { if (contentLoader.item?.searchField) {
contentLoader.item.searchField.text = ""; contentLoader.item.searchField.text = "";
contentLoader.item.searchField.forceActiveFocus(); contentLoader.item.searchField.forceActiveFocus();
@@ -50,9 +50,14 @@ DankPopout {
} }
function show() { function show() {
if (!clipboardAvailable) {
ToastService.showError(I18n.tr("Clipboard service not available"));
return;
}
open(); open();
activeImageLoads = 0; activeImageLoads = 0;
ClipboardService.reset(); ClipboardService.reset();
ClipboardService.refresh();
keyboardController.reset(); keyboardController.reset();
Qt.callLater(function () { Qt.callLater(function () {
@@ -117,18 +122,10 @@ DankPopout {
onBackgroundClicked: hide() onBackgroundClicked: hide()
onShouldBeVisibleChanged: { onShouldBeVisibleChanged: {
if (!shouldBeVisible) if (!shouldBeVisible) {
return; return;
if (clipboardAvailable) {
if (Theme.isConnectedEffect) {
Qt.callLater(() => {
if (root.shouldBeVisible)
ClipboardService.refresh();
});
} else {
ClipboardService.refresh();
}
} }
ClipboardService.refresh();
keyboardController.reset(); keyboardController.reset();
Qt.callLater(function () { Qt.callLater(function () {
if (contentLoader.item?.searchField) { if (contentLoader.item?.searchField) {
+76 -191
View File
@@ -26,22 +26,15 @@ Item {
property bool closeOnEscapeKey: true property bool closeOnEscapeKey: true
property bool closeOnBackgroundClick: true property bool closeOnBackgroundClick: true
property string animationType: "scale" property string animationType: "scale"
readonly property bool connectedMotionParity: Theme.isConnectedEffect property int animationDuration: Theme.modalAnimationDuration
property int animationDuration: connectedMotionParity ? Theme.popoutAnimationDuration : Theme.modalAnimationDuration property real animationScaleCollapsed: 0.96
property real animationScaleCollapsed: Theme.effectScaleCollapsed property real animationOffset: Theme.spacingL
property real animationOffset: Theme.effectAnimOffset property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
property list<real> animationEnterCurve: connectedMotionParity ? Theme.variantPopoutEnterCurve : Theme.variantModalEnterCurve property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
property list<real> animationExitCurve: connectedMotionParity ? Theme.variantPopoutExitCurve : Theme.variantModalExitCurve property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
property color backgroundColor: Theme.surfaceContainer
property color borderColor: Theme.outlineMedium property color borderColor: Theme.outlineMedium
property real borderWidth: 0 property real borderWidth: 0
property real cornerRadius: Theme.cornerRadius property real cornerRadius: Theme.cornerRadius
readonly property bool connectedSurfaceOverride: Theme.isConnectedEffect
readonly property color effectiveBackgroundColor: connectedSurfaceOverride ? Theme.connectedSurfaceColor : backgroundColor
readonly property color effectiveBorderColor: connectedSurfaceOverride ? "transparent" : borderColor
readonly property real effectiveBorderWidth: connectedSurfaceOverride ? 0 : borderWidth
readonly property real effectiveCornerRadius: connectedSurfaceOverride ? Theme.connectedSurfaceRadius : cornerRadius
readonly property bool effectiveBlurEnabled: Theme.connectedSurfaceBlurEnabled
property bool enableShadow: true property bool enableShadow: true
property alias modalFocusScope: focusScope property alias modalFocusScope: focusScope
property bool shouldBeVisible: false property bool shouldBeVisible: false
@@ -52,13 +45,11 @@ Item {
property bool keepPopoutsOpen: false property bool keepPopoutsOpen: false
property var customKeyboardFocus: null property var customKeyboardFocus: null
property bool useOverlayLayer: false property bool useOverlayLayer: false
property real frozenMotionOffsetX: 0
property real frozenMotionOffsetY: 0
readonly property alias contentWindow: contentWindow readonly property alias contentWindow: contentWindow
readonly property alias clickCatcher: clickCatcher readonly property alias clickCatcher: clickCatcher
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground
readonly property bool useSingleWindow: CompositorService.isHyprland readonly property bool useSingleWindow: CompositorService.isHyprland || useBackground
signal opened signal opened
signal dialogClosed signal dialogClosed
@@ -68,34 +59,33 @@ Item {
function open() { function open() {
closeTimer.stop(); closeTimer.stop();
animationsEnabled = false;
frozenMotionOffsetX = modalContainer ? modalContainer.offsetX : 0;
frozenMotionOffsetY = modalContainer ? modalContainer.offsetY : animationOffset;
const focusedScreen = CompositorService.getFocusedScreen(); const focusedScreen = CompositorService.getFocusedScreen();
const screenChanged = focusedScreen && contentWindow.screen !== focusedScreen;
if (focusedScreen) { if (focusedScreen) {
if (screenChanged)
contentWindow.visible = false;
contentWindow.screen = focusedScreen; contentWindow.screen = focusedScreen;
if (!useSingleWindow) if (!useSingleWindow) {
if (screenChanged)
clickCatcher.visible = false;
clickCatcher.screen = focusedScreen; clickCatcher.screen = focusedScreen;
}
} }
if (screenChanged) {
Qt.callLater(() => root._finishOpen());
} else {
_finishOpen();
}
}
if (Theme.isDirectionalEffect || root.useBackground) { function _finishOpen() {
if (!useSingleWindow)
clickCatcher.visible = true;
contentWindow.visible = true;
}
ModalManager.openModal(root); ModalManager.openModal(root);
shouldBeVisible = true;
Qt.callLater(() => { if (!useSingleWindow)
animationsEnabled = true; clickCatcher.visible = true;
shouldBeVisible = true; contentWindow.visible = true;
if (!useSingleWindow && !clickCatcher.visible) shouldHaveFocus = false;
clickCatcher.visible = true; Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
if (!contentWindow.visible)
contentWindow.visible = true;
shouldHaveFocus = false;
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
});
} }
function close() { function close() {
@@ -156,7 +146,7 @@ Item {
Timer { Timer {
id: closeTimer id: closeTimer
interval: Theme.variantCloseInterval(animationDuration) interval: animationDuration + 50
onTriggered: { onTriggered: {
if (shouldBeVisible) if (shouldBeVisible)
return; return;
@@ -170,19 +160,7 @@ Item {
readonly property var shadowLevel: Theme.elevationLevel3 readonly property var shadowLevel: Theme.elevationLevel3
readonly property real shadowFallbackOffset: 6 readonly property real shadowFallbackOffset: 6
readonly property real shadowRenderPadding: (root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0 readonly property real shadowRenderPadding: (root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
readonly property real shadowMotionPadding: { readonly property real shadowMotionPadding: animationType === "slide" ? 30 : Math.max(0, animationOffset)
if (Theme.isConnectedEffect)
return 0;
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0 && Theme.isDirectionalEffect)
return 0; // Wayland native overlap mask
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 shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
readonly property real alignedWidth: Theme.px(modalWidth, dpr) readonly property real alignedWidth: Theme.px(modalWidth, dpr)
readonly property real alignedHeight: Theme.px(modalHeight, dpr) readonly property real alignedHeight: Theme.px(modalHeight, dpr)
@@ -242,26 +220,9 @@ Item {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
enabled: !root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible enabled: root.closeOnBackgroundClick && root.shouldBeVisible
onClicked: root.backgroundClicked() 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 { PanelWindow {
@@ -271,13 +232,12 @@ Item {
WindowBlur { WindowBlur {
targetWindow: contentWindow targetWindow: contentWindow
blurEnabled: root.effectiveBlurEnabled
readonly property real s: Math.min(1, modalContainer.scaleValue) readonly property real s: Math.min(1, modalContainer.scaleValue)
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr) 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) blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
blurWidth: (root.shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.width * s : 0 blurWidth: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.width * s : 0
blurHeight: (root.shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.height * s : 0 blurHeight: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.height * s : 0
blurRadius: root.effectiveCornerRadius blurRadius: root.cornerRadius
} }
WlrLayershell.namespace: root.layerNamespace WlrLayershell.namespace: root.layerNamespace
@@ -315,12 +275,9 @@ Item {
bottom: root.useSingleWindow bottom: root.useSingleWindow
} }
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 { WlrLayershell.margins {
left: actualMarginLeft left: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
top: actualMarginTop top: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
right: 0 right: 0
bottom: 0 bottom: 0
} }
@@ -350,14 +307,13 @@ Item {
anchors.fill: parent anchors.fill: parent
z: -1 z: -1
color: "black" 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 visible: root.useBackground
Behavior on opacity { Behavior on opacity {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect) enabled: root.animationsEnabled
NumberAnimation { DankAnim {
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale) duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
@@ -365,8 +321,8 @@ Item {
Item { Item {
id: modalContainer id: modalContainer
x: (root.useSingleWindow ? root.alignedX : (root.alignedX - contentWindow.actualMarginLeft)) + Theme.snap(animX, root.dpr) x: root.useSingleWindow ? root.alignedX : shadowBuffer
y: (root.useSingleWindow ? root.alignedY : (root.alignedY - contentWindow.actualMarginTop)) + Theme.snap(animY, root.dpr) y: root.useSingleWindow ? root.alignedY : shadowBuffer
width: root.alignedWidth width: root.alignedWidth
height: root.alignedHeight height: root.alignedHeight
@@ -382,117 +338,45 @@ Item {
} }
readonly property bool slide: root.animationType === "slide" readonly property bool slide: root.animationType === "slide"
readonly property bool directionalEffect: Theme.isDirectionalEffect readonly property real offsetX: slide ? 15 : 0
readonly property bool depthEffect: Theme.isDepthEffect readonly property real offsetY: slide ? -30 : root.animationOffset
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 offsetX: {
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
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 (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
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;
}
property real animX: root.shouldBeVisible ? 0 : root.frozenMotionOffsetX property real animX: 0
property real animY: root.shouldBeVisible ? 0 : root.frozenMotionOffsetY property real animY: 0
property real scaleValue: root.animationScaleCollapsed
readonly property real computedScaleCollapsed: (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) ? 0.0 : root.animationScaleCollapsed onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
property real scaleValue: root.shouldBeVisible ? 1.0 : computedScaleCollapsed onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
Connections {
target: root
function onShouldBeVisibleChanged() {
modalContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetX, root.dpr);
modalContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetY, root.dpr);
modalContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed;
}
}
Behavior on animX { Behavior on animX {
enabled: root.animationsEnabled enabled: root.animationsEnabled
NumberAnimation { DankAnim {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible) duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
Behavior on animY { Behavior on animY {
enabled: root.animationsEnabled enabled: root.animationsEnabled
NumberAnimation { DankAnim {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible) duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
Behavior on scaleValue { Behavior on scaleValue {
enabled: root.animationsEnabled enabled: root.animationsEnabled
NumberAnimation { DankAnim {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible) duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
@@ -508,14 +392,15 @@ Item {
id: animatedContent id: animatedContent
anchors.fill: parent anchors.fill: parent
clip: false clip: false
opacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (root.shouldBeVisible ? 1 : 0) opacity: root.shouldBeVisible ? 1 : 0
scale: modalContainer.scaleValue scale: modalContainer.scaleValue
transformOrigin: Item.Center x: Theme.snap(modalContainer.animX, root.dpr) + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5
y: Theme.snap(modalContainer.animY, root.dpr) + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5
Behavior on opacity { Behavior on opacity {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect) enabled: root.animationsEnabled
NumberAnimation { NumberAnimation {
duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale) duration: animationDuration
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
@@ -526,19 +411,19 @@ Item {
anchors.fill: parent anchors.fill: parent
level: root.shadowLevel level: root.shadowLevel
fallbackOffset: root.shadowFallbackOffset fallbackOffset: root.shadowFallbackOffset
targetRadius: root.effectiveCornerRadius targetRadius: root.cornerRadius
targetColor: root.effectiveBackgroundColor targetColor: root.backgroundColor
borderColor: root.effectiveBorderColor borderColor: root.borderColor
borderWidth: root.effectiveBorderWidth borderWidth: root.borderWidth
shadowEnabled: root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" shadowEnabled: root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
} }
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: root.effectiveCornerRadius radius: root.cornerRadius
color: "transparent" color: "transparent"
border.color: root.connectedSurfaceOverride ? "transparent" : BlurService.borderColor border.color: BlurService.borderColor
border.width: root.connectedSurfaceOverride ? 0 : BlurService.borderWidth border.width: BlurService.borderWidth
z: 100 z: 100
} }
+100 -144
View File
@@ -352,8 +352,7 @@ Item {
searchQuery = query; searchQuery = query;
searchDebounce.restart(); searchDebounce.restart();
var filesInAll = searchMode === "all" && (SettingsData.dankLauncherV2IncludeFilesInAll || SettingsData.dankLauncherV2IncludeFoldersInAll); if (searchMode !== "plugins" && (searchMode === "files" || query.startsWith("/")) && query.length > 0) {
if (searchMode !== "plugins" && (searchMode === "files" || query.startsWith("/") || filesInAll) && query.length > 0) {
fileSearchDebounce.restart(); fileSearchDebounce.restart();
} }
} }
@@ -370,8 +369,7 @@ Item {
searchMode = mode; searchMode = mode;
modeChanged(mode); modeChanged(mode);
performSearch(); performSearch();
var filesInAll = mode === "all" && (SettingsData.dankLauncherV2IncludeFilesInAll || SettingsData.dankLauncherV2IncludeFoldersInAll) && searchQuery.length > 0; if (mode === "files") {
if (mode === "files" || filesInAll) {
fileSearchDebounce.restart(); fileSearchDebounce.restart();
} }
} }
@@ -929,22 +927,10 @@ Item {
if (!DSearchService.dsearchAvailable) if (!DSearchService.dsearchAvailable)
return; return;
var fileQuery = ""; var fileQuery = "";
var effectiveType = fileSearchType || "all";
var includeFiles = SettingsData.dankLauncherV2IncludeFilesInAll;
var includeFolders = SettingsData.dankLauncherV2IncludeFoldersInAll;
if (searchQuery.startsWith("/")) { if (searchQuery.startsWith("/")) {
fileQuery = searchQuery.substring(1).trim(); fileQuery = searchQuery.substring(1).trim();
} else if (searchMode === "files") { } else if (searchMode === "files") {
fileQuery = searchQuery.trim(); fileQuery = searchQuery.trim();
} else if (searchMode === "all" && (includeFiles || includeFolders)) {
fileQuery = searchQuery.trim();
if (includeFiles && !includeFolders)
effectiveType = "file";
else if (!includeFiles && includeFolders)
effectiveType = "dir";
else
effectiveType = "all";
} else { } else {
return; return;
} }
@@ -955,129 +941,109 @@ Item {
} }
isFileSearching = true; isFileSearching = true;
var params = {
limit: 20,
fuzzy: true,
sort: fileSearchSort || "score",
desc: true
};
var splitBothTypes = searchMode === "all" && includeFiles && includeFolders && DSearchService.supportsTypeFilter; if (DSearchService.supportsTypeFilter) {
var queryTypes = splitBothTypes ? ["file", "dir"] : [effectiveType]; params.type = (fileSearchType && fileSearchType !== "all") ? fileSearchType : "all";
var pending = queryTypes.length; }
var aggregatedItems = []; if (fileSearchExt) {
params.ext = fileSearchExt;
}
if (fileSearchFolder) {
params.folder = fileSearchFolder;
}
for (var t = 0; t < queryTypes.length; t++) { DSearchService.search(fileQuery, params, function (response) {
var queryType = queryTypes[t]; isFileSearching = false;
var params = { if (response.error)
limit: 20, return;
fuzzy: true, var fileItems = [];
sort: fileSearchSort || "score", var hits = response.result?.hits || [];
desc: true
};
if (DSearchService.supportsTypeFilter) { for (var i = 0; i < hits.length; i++) {
params.type = (queryType && queryType !== "all") ? queryType : "all"; var hit = hits[i];
} var docTypes = hit.locations?.doc_type;
if (fileSearchExt) { var isDir = docTypes ? !!docTypes["dir"] : false;
params.ext = fileSearchExt; fileItems.push(transformFileResult({
} path: hit.id || "",
if (fileSearchFolder) { score: hit.score || 0,
params.folder = fileSearchFolder; is_dir: isDir
}));
} }
DSearchService.search(fileQuery, params, function (response) { var fileSections = [];
pending--; var showType = fileSearchType || "all";
if (!response.error) {
var hits = response.result?.hits || []; if (showType === "all" && DSearchService.supportsTypeFilter) {
for (var i = 0; i < hits.length; i++) { var onlyFiles = [];
var hit = hits[i]; var onlyDirs = [];
var docTypes = hit.locations?.doc_type; for (var j = 0; j < fileItems.length; j++) {
var isDir = docTypes ? !!docTypes["dir"] : false; if (fileItems[j].data?.is_dir)
aggregatedItems.push(transformFileResult({ onlyDirs.push(fileItems[j]);
path: hit.id || "", else
score: hit.score || 0, onlyFiles.push(fileItems[j]);
is_dir: isDir
}));
}
} }
if (pending > 0) if (onlyFiles.length > 0) {
return; fileSections.push({
id: "files",
title: I18n.tr("Files"),
icon: "insert_drive_file",
priority: 4,
items: onlyFiles,
collapsed: collapsedSections["files"] || false,
flatStartIndex: 0
});
}
if (onlyDirs.length > 0) {
fileSections.push({
id: "folders",
title: I18n.tr("Folders"),
icon: "folder",
priority: 4.1,
items: onlyDirs,
collapsed: collapsedSections["folders"] || false,
flatStartIndex: 0
});
}
} else {
var filesIcon = showType === "dir" ? "folder" : showType === "file" ? "insert_drive_file" : "folder";
var filesTitle = showType === "dir" ? I18n.tr("Folders") : I18n.tr("Files");
if (fileItems.length > 0) {
fileSections.push({
id: "files",
title: filesTitle,
icon: filesIcon,
priority: 4,
items: fileItems,
collapsed: collapsedSections["files"] || false,
flatStartIndex: 0
});
}
}
isFileSearching = false; var newSections;
_applyFileSearchResults(aggregatedItems, effectiveType); if (searchMode === "files") {
newSections = fileSections;
} else {
var existingNonFile = sections.filter(function (s) {
return s.id !== "files" && s.id !== "folders";
});
newSections = existingNonFile.concat(fileSections);
}
newSections.sort(function (a, b) {
return a.priority - b.priority;
}); });
} _applyHighlights(newSections, searchQuery);
} flatModel = Scorer.flattenSections(newSections);
sections = newSections;
function _applyFileSearchResults(fileItems, effectiveType) { selectedFlatIndex = getFirstItemIndex();
var fileSections = []; updateSelectedItem();
var showType = effectiveType;
var order = SettingsData.launcherPluginOrder || [];
var filesOrderIdx = order.indexOf("__files");
var foldersOrderIdx = order.indexOf("__folders");
var filesPriority = filesOrderIdx !== -1 ? 2.6 + filesOrderIdx * 0.01 : 4;
var foldersPriority = foldersOrderIdx !== -1 ? 2.6 + foldersOrderIdx * 0.01 : 4.1;
if (showType === "all" && DSearchService.supportsTypeFilter) {
var onlyFiles = [];
var onlyDirs = [];
for (var j = 0; j < fileItems.length; j++) {
if (fileItems[j].data?.is_dir)
onlyDirs.push(fileItems[j]);
else
onlyFiles.push(fileItems[j]);
}
if (onlyFiles.length > 0) {
fileSections.push({
id: "files",
title: I18n.tr("Files"),
icon: "insert_drive_file",
priority: filesPriority,
items: onlyFiles,
collapsed: collapsedSections["files"] || false,
flatStartIndex: 0
});
}
if (onlyDirs.length > 0) {
fileSections.push({
id: "folders",
title: I18n.tr("Folders"),
icon: "folder",
priority: foldersPriority,
items: onlyDirs,
collapsed: collapsedSections["folders"] || false,
flatStartIndex: 0
});
}
} else {
var filesIcon = showType === "dir" ? "folder" : showType === "file" ? "insert_drive_file" : "folder";
var filesTitle = showType === "dir" ? I18n.tr("Folders") : I18n.tr("Files");
var singlePriority = showType === "dir" ? foldersPriority : filesPriority;
if (fileItems.length > 0) {
fileSections.push({
id: "files",
title: filesTitle,
icon: filesIcon,
priority: singlePriority,
items: fileItems,
collapsed: collapsedSections["files"] || false,
flatStartIndex: 0
});
}
}
var newSections;
if (searchMode === "files") {
newSections = fileSections;
} else {
var existingNonFile = sections.filter(function (s) {
return s.id !== "files" && s.id !== "folders";
});
newSections = existingNonFile.concat(fileSections);
}
newSections.sort(function (a, b) {
return a.priority - b.priority;
}); });
_applyHighlights(newSections, searchQuery);
flatModel = Scorer.flattenSections(newSections);
sections = newSections;
selectedFlatIndex = getFirstItemIndex();
updateSelectedItem();
} }
function searchApps(query) { function searchApps(query) {
@@ -1310,11 +1276,7 @@ Item {
function buildDynamicSectionDefs(items) { function buildDynamicSectionDefs(items) {
var baseDefs = sectionDefinitions.slice(); var baseDefs = sectionDefinitions.slice();
var pluginSections = {}; var pluginSections = {};
var order = SettingsData.launcherPluginOrder || []; var basePriority = 2.6;
var orderMap = {};
for (var k = 0; k < order.length; k++)
orderMap[order[k]] = k;
var unorderedPriority = 2.6 + order.length * 0.01;
for (var i = 0; i < items.length; i++) { for (var i = 0; i < items.length; i++) {
var section = items[i].section; var section = items[i].section;
@@ -1325,25 +1287,19 @@ Item {
var pluginId = section.substring(7); var pluginId = section.substring(7);
var meta = getPluginMetadata(pluginId); var meta = getPluginMetadata(pluginId);
var viewPref = getPluginViewPref(pluginId); var viewPref = getPluginViewPref(pluginId);
var orderIdx = orderMap[pluginId];
var priority;
if (orderIdx !== undefined) {
priority = 2.6 + orderIdx * 0.01;
} else {
priority = unorderedPriority;
unorderedPriority += 0.01;
}
pluginSections[section] = { pluginSections[section] = {
id: section, id: section,
title: meta.name, title: meta.name,
icon: meta.icon, icon: meta.icon,
priority: priority, priority: basePriority,
defaultViewMode: viewPref.mode || "list" defaultViewMode: viewPref.mode || "list"
}; };
if (viewPref.mode) if (viewPref.mode)
setPluginViewPreference(section, viewPref.mode, viewPref.enforced); setPluginViewPreference(section, viewPref.mode, viewPref.enforced);
basePriority += 0.01;
} }
for (var sectionId in pluginSections) { for (var sectionId in pluginSections) {
@@ -1,5 +1,4 @@
import QtQuick import QtQuick
import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Hyprland import Quickshell.Hyprland
@@ -15,24 +14,16 @@ Item {
property bool spotlightOpen: false property bool spotlightOpen: false
property bool keyboardActive: false property bool keyboardActive: false
property bool contentVisible: false property bool contentVisible: false
readonly property bool launcherMotionVisible: Theme.isConnectedEffect ? _motionActive : (Theme.isDirectionalEffect ? spotlightOpen : _motionActive)
property var spotlightContent: launcherContentLoader.item property var spotlightContent: launcherContentLoader.item
property bool openedFromOverview: false property bool openedFromOverview: false
property bool isClosing: false property bool isClosing: false
property bool _windowEnabled: true
property bool _pendingInitialize: false property bool _pendingInitialize: false
property string _pendingQuery: "" property string _pendingQuery: ""
property string _pendingMode: "" property string _pendingMode: ""
readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose
// Animation state matches DankPopout/DankModal pattern
property bool animationsEnabled: true
property bool _motionActive: false
property real _frozenMotionX: 0
property real _frozenMotionY: 0
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
readonly property var effectiveScreen: contentWindow.screen readonly property var effectiveScreen: launcherWindow.screen
readonly property real screenWidth: effectiveScreen?.width ?? 1920 readonly property real screenWidth: effectiveScreen?.width ?? 1920
readonly property real screenHeight: effectiveScreen?.height ?? 1080 readonly property real screenHeight: effectiveScreen?.height ?? 1080
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1 readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
@@ -66,12 +57,8 @@ Item {
readonly property real modalX: (screenWidth - modalWidth) / 2 readonly property real modalX: (screenWidth - modalWidth) / 2
readonly property real modalY: (screenHeight - modalHeight) / 2 readonly property real modalY: (screenHeight - modalHeight) / 2
readonly property bool connectedSurfaceOverride: Theme.isConnectedEffect readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
readonly property int launcherAnimationDuration: Theme.isConnectedEffect ? Theme.popoutAnimationDuration : Theme.modalAnimationDuration readonly property real cornerRadius: Theme.cornerRadius
readonly property list<real> launcherEnterCurve: Theme.isConnectedEffect ? Theme.variantPopoutEnterCurve : Theme.variantModalEnterCurve
readonly property list<real> launcherExitCurve: Theme.isConnectedEffect ? Theme.variantPopoutExitCurve : Theme.variantModalExitCurve
readonly property color backgroundColor: connectedSurfaceOverride ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
readonly property real cornerRadius: connectedSurfaceOverride ? Theme.connectedSurfaceRadius : Theme.cornerRadius
readonly property color borderColor: { readonly property color borderColor: {
if (!SettingsData.dankLauncherV2BorderEnabled) if (!SettingsData.dankLauncherV2BorderEnabled)
return Theme.outlineMedium; return Theme.outlineMedium;
@@ -89,37 +76,6 @@ Item {
} }
} }
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0 readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
readonly property color effectiveBorderColor: connectedSurfaceOverride ? "transparent" : borderColor
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)
readonly property var shadowLevel: Theme.elevationLevel3
readonly property real shadowFallbackOffset: 6
readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
readonly property real shadowPad: Theme.snap(shadowRenderPadding, dpr)
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
readonly property real alignedX: Theme.snap(modalX, dpr)
readonly property real alignedY: Theme.snap(modalY, dpr)
// 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: _needsExtendedWindow ? 0 : Theme.snap(alignedY - shadowPad, dpr)
readonly property real _cwWidth: alignedWidth + shadowPad * 2
readonly property real _cwHeight: {
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: _needsExtendedWindow ? alignedY : shadowPad
signal dialogClosed signal dialogClosed
@@ -140,11 +96,18 @@ Item {
if (!spotlightContent) if (!spotlightContent)
return; return;
contentVisible = true; contentVisible = true;
// NOTE: forceActiveFocus() is deliberately NOT called here. spotlightContent.searchField.forceActiveFocus();
// It is deferred to after animation starts to avoid compositor IPC stalls.
var targetQuery = "";
if (query) {
targetQuery = query;
} else if (SettingsData.rememberLastQuery) {
targetQuery = SessionData.launcherLastQuery || "";
}
if (spotlightContent.searchField) { if (spotlightContent.searchField) {
spotlightContent.searchField.text = query; spotlightContent.searchField.text = targetQuery;
} }
if (spotlightContent.controller) { if (spotlightContent.controller) {
var targetMode = mode || SessionData.launcherLastMode || "all"; var targetMode = mode || SessionData.launcherLastMode || "all";
@@ -159,12 +122,10 @@ Item {
spotlightContent.controller.collapsedSections = {}; spotlightContent.controller.collapsedSections = {};
spotlightContent.controller.selectedFlatIndex = 0; spotlightContent.controller.selectedFlatIndex = 0;
spotlightContent.controller.selectedItem = null; spotlightContent.controller.selectedItem = null;
if (query) { spotlightContent.controller.historyIndex = -1;
spotlightContent.controller.setSearchQuery(query); spotlightContent.controller.searchQuery = targetQuery;
} else {
spotlightContent.controller.searchQuery = ""; spotlightContent.controller.performSearch();
spotlightContent.controller.performSearch();
}
} }
if (spotlightContent.resetScroll) { if (spotlightContent.resetScroll) {
spotlightContent.resetScroll(); spotlightContent.resetScroll();
@@ -174,59 +135,47 @@ Item {
} }
} }
function _openCommon(query, mode) { function _finishShow(query, mode) {
closeCleanupTimer.stop(); spotlightOpen = true;
isClosing = false; isClosing = false;
openedFromOverview = false; openedFromOverview = false;
// Disable animations so the snap is instant keyboardActive = true;
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(root); ModalManager.openModal(root);
spotlightOpen = true;
backgroundWindow.visible = true;
contentWindow.visible = true;
if (useHyprlandFocusGrab) if (useHyprlandFocusGrab)
focusGrab.active = true; focusGrab.active = true;
// Load content and initialize (but no forceActiveFocus that's deferred)
_ensureContentLoadedAndInitialize(query || "", mode || ""); _ensureContentLoadedAndInitialize(query || "", mode || "");
// Frame 1: enable animations and trigger enter motion
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)
root.spotlightContent.searchField.forceActiveFocus();
});
});
} }
function show() { function show() {
_openCommon("", ""); closeCleanupTimer.stop();
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
spotlightOpen = false;
isClosing = false;
launcherWindow.screen = focusedScreen;
Qt.callLater(() => root._finishShow("", ""));
return;
}
_finishShow("", "");
} }
function showWithQuery(query) { function showWithQuery(query) {
_openCommon(query, ""); closeCleanupTimer.stop();
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
spotlightOpen = false;
isClosing = false;
launcherWindow.screen = focusedScreen;
Qt.callLater(() => root._finishShow(query, ""));
return;
}
_finishShow(query, "");
} }
function hide() { function hide() {
@@ -234,17 +183,13 @@ Item {
return; return;
openedFromOverview = false; openedFromOverview = false;
isClosing = true; isClosing = true;
// For directional effects, defer contentVisible=false so content stays rendered during exit slide contentVisible = false;
if (!Theme.isDirectionalEffect)
contentVisible = false;
// Trigger exit animation Behaviors will animate motionX/Y to frozen collapsed position
_motionActive = false;
keyboardActive = false; keyboardActive = false;
spotlightOpen = false; spotlightOpen = false;
focusGrab.active = false; focusGrab.active = false;
ModalManager.closeModal(root); ModalManager.closeModal(root);
closeCleanupTimer.start(); closeCleanupTimer.start();
} }
@@ -253,7 +198,27 @@ Item {
} }
function showWithMode(mode) { function showWithMode(mode) {
_openCommon("", mode); closeCleanupTimer.stop();
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
spotlightOpen = false;
isClosing = false;
launcherWindow.screen = focusedScreen;
Qt.callLater(() => root._finishShow("", mode));
return;
}
spotlightOpen = true;
isClosing = false;
openedFromOverview = false;
keyboardActive = true;
ModalManager.openModal(root);
if (useHyprlandFocusGrab)
focusGrab.active = true;
_ensureContentLoadedAndInitialize("", mode);
} }
function toggleWithMode(mode) { function toggleWithMode(mode) {
@@ -274,13 +239,10 @@ Item {
Timer { Timer {
id: closeCleanupTimer id: closeCleanupTimer
interval: Theme.variantCloseInterval(root.launcherAnimationDuration) interval: Theme.modalAnimationDuration + 50
repeat: false repeat: false
onTriggered: { onTriggered: {
isClosing = false; isClosing = false;
contentVisible = false;
contentWindow.visible = false;
backgroundWindow.visible = false;
if (root.unloadContentOnClose) if (root.unloadContentOnClose)
launcherContentLoader.active = false; launcherContentLoader.active = false;
dialogClosed(); dialogClosed();
@@ -289,6 +251,7 @@ Item {
Connections { Connections {
target: spotlightContent?.controller ?? null target: spotlightContent?.controller ?? null
function onModeChanged(mode) { function onModeChanged(mode) {
if (spotlightContent.controller.autoSwitchedToFiles) if (spotlightContent.controller.autoSwitchedToFiles)
return; return;
@@ -298,7 +261,7 @@ Item {
HyprlandFocusGrab { HyprlandFocusGrab {
id: focusGrab id: focusGrab
windows: [contentWindow] windows: [launcherWindow]
active: false active: false
onCleared: { onCleared: {
@@ -323,54 +286,56 @@ Item {
if (Quickshell.screens.length === 0) if (Quickshell.screens.length === 0)
return; return;
const screen = contentWindow.screen; const screenName = launcherWindow.screen?.name;
const screenName = screen?.name; if (screenName) {
let needsReset = !screen || !screenName;
if (!needsReset) {
needsReset = true;
for (let i = 0; i < Quickshell.screens.length; i++) { for (let i = 0; i < Quickshell.screens.length; i++) {
if (Quickshell.screens[i].name === screenName) { if (Quickshell.screens[i].name === screenName)
needsReset = false; return;
break;
}
} }
} }
if (!needsReset) if (spotlightOpen)
return; hide();
const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0]; const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0];
if (!newScreen) if (newScreen)
return; launcherWindow.screen = newScreen;
root._windowEnabled = false;
backgroundWindow.screen = newScreen;
contentWindow.screen = newScreen;
Qt.callLater(() => {
root._windowEnabled = true;
});
} }
} }
// Background window: fullscreen, handles darkening + click-to-dismiss
PanelWindow { PanelWindow {
id: backgroundWindow id: launcherWindow
visible: false visible: spotlightOpen || isClosing
color: "transparent" color: "transparent"
exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "dms:spotlight:bg" WindowBlur {
WlrLayershell.layer: WlrLayershell.Top targetWindow: launcherWindow
WlrLayershell.exclusiveZone: -1 readonly property real s: Math.min(1, modalContainer.scale)
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None blurX: root.modalX + root.modalWidth * (1 - s) * 0.5
blurY: root.modalY + root.modalHeight * (1 - s) * 0.5
WlrLayershell.margins { blurWidth: (contentVisible && modalContainer.opacity > 0) ? root.modalWidth * s : 0
top: contentContainer.dockTop ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 0 ? Theme.px(42, root.dpr) : 0) blurHeight: (contentVisible && modalContainer.opacity > 0) ? root.modalHeight * s : 0
bottom: contentContainer.dockBottom ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 1 ? Theme.px(42, root.dpr) : 0) blurRadius: root.cornerRadius
left: contentContainer.dockLeft ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 2 ? Theme.px(42, root.dpr) : 0)
right: contentContainer.dockRight ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 3 ? Theme.px(42, root.dpr) : 0)
} }
WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
console.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "overlay":
return WlrLayershell.Overlay;
default:
return WlrLayershell.Top;
}
}
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
anchors { anchors {
top: true top: true
bottom: true bottom: true
@@ -379,11 +344,11 @@ Item {
} }
mask: Region { mask: Region {
item: (spotlightOpen || isClosing) ? bgFullScreenMask : null item: spotlightOpen ? fullScreenMask : null
} }
Item { Item {
id: bgFullScreenMask id: fullScreenMask
anchors.fill: parent anchors.fill: parent
} }
@@ -391,14 +356,13 @@ Item {
id: backgroundDarken id: backgroundDarken
anchors.fill: parent anchors.fill: parent
color: "black" color: "black"
opacity: launcherMotionVisible && SettingsData.modalDarkenBackground ? 0.5 : 0 opacity: contentVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
visible: launcherMotionVisible || opacity > 0 visible: contentVisible || opacity > 0
Behavior on opacity { Behavior on opacity {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
DankAnim { DankAnim {
duration: Math.round(Theme.variantDuration(root.launcherAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale) duration: Theme.modalAnimationDuration
easing.bezierCurve: launcherMotionVisible ? root.launcherEnterCurve : root.launcherExitCurve easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
} }
} }
} }
@@ -406,236 +370,96 @@ Item {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
enabled: spotlightOpen enabled: spotlightOpen
onClicked: root.hide() onClicked: mouse => {
} var contentX = modalContainer.x;
} var contentY = modalContainer.y;
var contentW = modalContainer.width;
var contentH = modalContainer.height;
// Content window: SMALL, positioned with margins only renders the modal area if (mouse.x < contentX || mouse.x > contentX + contentW || mouse.y < contentY || mouse.y > contentY + contentH) {
PanelWindow { root.hide();
id: contentWindow }
visible: false
color: "transparent"
WindowBlur {
targetWindow: contentWindow
blurEnabled: root.effectiveBlurEnabled
readonly property real s: Math.min(1, contentContainer.scaleValue)
blurX: root._ccX + root.alignedWidth * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr)
blurY: root._ccY + root.alignedHeight * (1 - s) * 0.5 + Theme.snap(contentContainer.animY, root.dpr)
blurWidth: (root.spotlightOpen || root.isClosing) && contentWrapper.opacity > 0 ? root.alignedWidth * s : 0
blurHeight: (root.spotlightOpen || root.isClosing) && contentWrapper.opacity > 0 ? root.alignedHeight * s : 0
blurRadius: root.cornerRadius
}
WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
console.error("DankLauncherV2Modal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
console.error("DankLauncherV2Modal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "overlay":
return WlrLayershell.Overlay;
default:
return WlrLayershell.Top;
} }
} }
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
anchors {
left: true
top: true
}
WlrLayershell.margins {
left: root._cwMarginLeft
top: root._cwMarginTop
}
implicitWidth: root._cwWidth
implicitHeight: root._cwHeight
mask: Region {
item: contentInputMask
}
Item { Item {
id: contentInputMask id: modalContainer
visible: false x: root.modalX
x: contentContainer.x + contentWrapper.x y: root.modalY
y: contentContainer.y + contentWrapper.y width: root.modalWidth
width: root.alignedWidth height: root.modalHeight
height: root.alignedHeight visible: contentVisible || opacity > 0
}
Item { opacity: contentVisible ? 1 : 0
id: contentContainer scale: contentVisible ? 1 : 0.96
transformOrigin: Item.Center
// For directional/depth: contentContainer is at alignedY from window top (window starts at screen top) Behavior on opacity {
// For standard: contentContainer is at shadowPad from window top (window starts near modal)
x: root._ccX
y: root._ccY
width: root.alignedWidth
height: root.alignedHeight
readonly property int dockEdge: typeof SettingsData !== "undefined" ? SettingsData.dockPosition : 1
readonly property bool dockTop: dockEdge === 0
readonly property bool dockBottom: dockEdge === 1
readonly property bool dockLeft: dockEdge === 2
readonly property bool dockRight: dockEdge === 3
readonly property real dockThickness: typeof SettingsData !== "undefined" && SettingsData.showDock ? Theme.px(SettingsData.dockIconSize + (SettingsData.dockMargin * 2) + SettingsData.dockSpacing + 8, root.dpr) : Theme.px(60, root.dpr)
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
readonly property real collapsedMotionX: {
if (directionalEffect) {
if (dockLeft)
return -(root._ccX + root.alignedWidth + Theme.effectAnimOffset);
if (dockRight)
return root.screenWidth - root._ccX + Theme.effectAnimOffset;
}
if (depthEffect)
return Theme.effectAnimOffset * 0.25;
return 0;
}
readonly property real collapsedMotionY: {
if (directionalEffect) {
if (dockTop)
return -(root._ccY + root.alignedHeight + Theme.effectAnimOffset);
if (dockBottom)
return root.screenHeight - root._ccY + root.shadowPad + Theme.effectAnimOffset;
return 0;
}
if (depthEffect)
return -Math.max(Theme.effectAnimOffset * 0.85, 34);
return -Math.max((root.shadowPad || 0) + Theme.effectAnimOffset, 40);
}
// Declarative bindings snap applied at render layer (contentWrapper x/y)
property real animX: root._motionActive ? 0 : root._frozenMotionX
property real animY: root._motionActive ? 0 : root._frozenMotionY
property real scaleValue: root._motionActive ? 1.0 : (Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed))
Behavior on animX {
enabled: root.animationsEnabled
DankAnim { DankAnim {
duration: Theme.variantDuration(root.launcherAnimationDuration, root._motionActive) duration: Theme.modalAnimationDuration
easing.bezierCurve: root._motionActive ? root.launcherEnterCurve : root.launcherExitCurve easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
} }
} }
Behavior on animY { Behavior on scale {
enabled: root.animationsEnabled
DankAnim { DankAnim {
duration: Theme.variantDuration(root.launcherAnimationDuration, root._motionActive) duration: Theme.modalAnimationDuration
easing.bezierCurve: root._motionActive ? root.launcherEnterCurve : root.launcherExitCurve easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
} }
} }
Behavior on scaleValue { ElevationShadow {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2)) id: launcherShadowLayer
DankAnim { anchors.fill: parent
duration: Theme.variantDuration(root.launcherAnimationDuration, root._motionActive) level: Theme.elevationLevel3
easing.bezierCurve: root._motionActive ? root.launcherEnterCurve : root.launcherExitCurve fallbackOffset: 6
} targetColor: root.backgroundColor
borderColor: root.borderColor
borderWidth: root.borderWidth
targetRadius: root.cornerRadius
shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
} }
Item { MouseArea {
id: directionalClipMask anchors.fill: parent
readonly property bool shouldClip: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0 onPressed: mouse => mouse.accepted = true
readonly property real clipOversize: 2000 }
clip: shouldClip FocusScope {
anchors.fill: parent
focus: keyboardActive
x: shouldClip ? (contentContainer.dockRight ? -clipOversize : (contentContainer.dockLeft ? contentContainer.dockThickness - root._ccX : -clipOversize)) : 0 Loader {
y: shouldClip ? (contentContainer.dockBottom ? -clipOversize : (contentContainer.dockTop ? contentContainer.dockThickness - root._ccY : -clipOversize)) : 0 id: launcherContentLoader
anchors.fill: parent
width: shouldClip ? parent.width + clipOversize + (contentContainer.dockRight ? (root.screenWidth - contentContainer.dockThickness - root._ccX - parent.width) : (contentContainer.dockLeft ? clipOversize : clipOversize)) : parent.width active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
height: shouldClip ? parent.height + clipOversize + (contentContainer.dockBottom ? (root.screenHeight - contentContainer.dockThickness - root._ccY - parent.height) : (contentContainer.dockTop ? clipOversize : clipOversize)) : parent.height asynchronous: false
sourceComponent: LauncherContent {
Item { focus: true
id: aligner parentModal: root
x: directionalClipMask.x !== 0 ? -directionalClipMask.x : 0
y: directionalClipMask.y !== 0 ? -directionalClipMask.y : 0
width: contentContainer.width
height: contentContainer.height
// Shadow mirrors contentWrapper position/scale/opacity
ElevationShadow {
id: launcherShadowLayer
width: parent.width
height: parent.height
opacity: contentWrapper.opacity
scale: contentWrapper.scale
x: contentWrapper.x
y: contentWrapper.y
level: root.shadowLevel
fallbackOffset: root.shadowFallbackOffset
targetColor: root.backgroundColor
borderColor: root.effectiveBorderColor
borderWidth: root.effectiveBorderWidth
targetRadius: root.cornerRadius
shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
} }
// contentWrapper moves inside static contentContainer DankPopout pattern onLoaded: {
Item { if (root._pendingInitialize) {
id: contentWrapper root._initializeAndShow(root._pendingQuery, root._pendingMode);
width: parent.width root._pendingInitialize = false;
height: parent.height
opacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (launcherMotionVisible ? 1 : 0)
visible: opacity > 0
scale: contentContainer.scaleValue
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
Behavior on opacity {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
DankAnim {
duration: Math.round(Theme.variantDuration(root.launcherAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
easing.bezierCurve: launcherMotionVisible ? root.launcherEnterCurve : root.launcherExitCurve
}
} }
}
}
MouseArea { Keys.onEscapePressed: event => {
anchors.fill: parent root.hide();
onPressed: mouse => mouse.accepted = true event.accepted = true;
} }
}
FocusScope { Rectangle {
anchors.fill: parent anchors.fill: parent
focus: keyboardActive radius: root.cornerRadius
color: "transparent"
Loader { border.color: BlurService.borderColor
id: launcherContentLoader border.width: BlurService.borderWidth
anchors.fill: parent }
active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize }
asynchronous: false }
sourceComponent: LauncherContent {
focus: true
parentModal: root
}
onLoaded: {
if (root._pendingInitialize) {
root._initializeAndShow(root._pendingQuery, root._pendingMode);
root._pendingInitialize = false;
}
}
}
Keys.onEscapePressed: event => {
root.hide();
event.accepted = true;
}
}
} // contentWrapper
} // aligner
} // directionalClipMask
} // contentContainer
} // PanelWindow
} }
@@ -41,6 +41,7 @@ FocusScope {
editCommentField.text = existing?.comment || ""; editCommentField.text = existing?.comment || "";
editEnvVarsField.text = existing?.envVars || ""; editEnvVarsField.text = existing?.envVars || "";
editExtraFlagsField.text = existing?.extraFlags || ""; editExtraFlagsField.text = existing?.extraFlags || "";
editDgpuToggle.checked = existing?.launchOnDgpu || false;
editMode = true; editMode = true;
Qt.callLater(() => editNameField.forceActiveFocus()); Qt.callLater(() => editNameField.forceActiveFocus());
} }
@@ -64,6 +65,8 @@ FocusScope {
override.envVars = editEnvVarsField.text.trim(); override.envVars = editEnvVarsField.text.trim();
if (editExtraFlagsField.text.trim()) if (editExtraFlagsField.text.trim())
override.extraFlags = editExtraFlagsField.text.trim(); override.extraFlags = editExtraFlagsField.text.trim();
if (editDgpuToggle.checked)
override.launchOnDgpu = true;
SessionData.setAppOverride(editAppId, override); SessionData.setAppOverride(editAppId, override);
closeEditMode(); closeEditMode();
} }
@@ -86,7 +89,7 @@ FocusScope {
Controller { Controller {
id: controller id: controller
active: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true active: root.parentModal?.spotlightOpen ?? true
viewModeContext: root.viewModeContext viewModeContext: root.viewModeContext
onItemExecuted: { onItemExecuted: {
@@ -146,10 +149,18 @@ FocusScope {
event.accepted = false; event.accepted = false;
return; return;
case Qt.Key_Down: case Qt.Key_Down:
controller.selectNext(); if (hasCtrl) {
controller.navigateHistory("down");
} else {
controller.selectNext();
}
return; return;
case Qt.Key_Up: case Qt.Key_Up:
controller.selectPrevious(); if (hasCtrl) {
controller.navigateHistory("up");
} else {
controller.selectPrevious();
}
return; return;
case Qt.Key_PageDown: case Qt.Key_PageDown:
controller.selectPageDown(8); controller.selectPageDown(8);
@@ -158,6 +169,10 @@ FocusScope {
controller.selectPageUp(8); controller.selectPageUp(8);
return; return;
case Qt.Key_Right: case Qt.Key_Right:
if (hasCtrl) {
controller.cycleMode();
return;
}
if (controller.getCurrentSectionViewMode() !== "list") { if (controller.getCurrentSectionViewMode() !== "list") {
controller.selectRight(); controller.selectRight();
return; return;
@@ -165,12 +180,25 @@ FocusScope {
event.accepted = false; event.accepted = false;
return; return;
case Qt.Key_Left: case Qt.Key_Left:
if (hasCtrl) {
const reverse = true;
controller.cycleMode(reverse);
return;
}
if (controller.getCurrentSectionViewMode() !== "list") { if (controller.getCurrentSectionViewMode() !== "list") {
controller.selectLeft(); controller.selectLeft();
return; return;
} }
event.accepted = false; event.accepted = false;
return; return;
case Qt.Key_H:
if (hasCtrl) {
const reverse = true;
controller.cycleMode(reverse);
return;
}
event.accepted = false;
return;
case Qt.Key_J: case Qt.Key_J:
if (hasCtrl) { if (hasCtrl) {
controller.selectNext(); controller.selectNext();
@@ -185,6 +213,13 @@ FocusScope {
} }
event.accepted = false; event.accepted = false;
return; return;
case Qt.Key_L:
if (hasCtrl) {
controller.cycleMode();
return;
}
event.accepted = false;
return;
case Qt.Key_N: case Qt.Key_N:
if (hasCtrl) { if (hasCtrl) {
controller.selectNextSection(); controller.selectNextSection();
@@ -200,13 +235,19 @@ FocusScope {
event.accepted = false; event.accepted = false;
return; return;
case Qt.Key_Tab: case Qt.Key_Tab:
if (actionPanel.hasActions) { if (hasCtrl && actionPanel.hasActions) {
actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show(); actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show();
return;
} }
controller.selectNext();
return; return;
case Qt.Key_Backtab: case Qt.Key_Backtab:
if (actionPanel.expanded) if (hasCtrl && actionPanel.expanded) {
actionPanel.hide(); const reverse = true;
actionPanel.expanded ? actionPanel.cycleAction(reverse) : actionPanel.show();
return;
}
controller.selectPrevious();
return; return;
case Qt.Key_Return: case Qt.Key_Return:
case Qt.Key_Enter: case Qt.Key_Enter:
@@ -270,7 +311,7 @@ FocusScope {
Item { Item {
anchors.fill: parent anchors.fill: parent
visible: !editMode visible: !editMode && !(root.parentModal?.isClosing ?? false)
Item { Item {
id: footerBar id: footerBar
@@ -388,7 +429,7 @@ FocusScope {
StyledText { StyledText {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "Tab " + I18n.tr("actions") text: "Ctrl-Tab " + I18n.tr("actions")
font.pixelSize: Theme.fontSizeSmall - 1 font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
visible: actionPanel.hasActions visible: actionPanel.hasActions
@@ -462,7 +503,7 @@ FocusScope {
showClearButton: true showClearButton: true
textColor: Theme.surfaceText textColor: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
enabled: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true enabled: root.parentModal ? root.parentModal.spotlightOpen : true
placeholderText: "" placeholderText: ""
ignoreUpDownKeys: true ignoreUpDownKeys: true
ignoreTabKeys: true ignoreTabKeys: true
@@ -696,14 +737,6 @@ FocusScope {
Item { Item {
width: parent.width width: parent.width
height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2) height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2)
opacity: {
if (!root.parentModal)
return 1;
if (Theme.isDirectionalEffect && root.parentModal.isClosing)
return 1;
return root.parentModal.isClosing ? 0 : 1;
}
ResultsList { ResultsList {
id: resultsList id: resultsList
anchors.fill: parent anchors.fill: parent
@@ -736,6 +769,7 @@ FocusScope {
} }
function onSearchQueryRequested(query) { function onSearchQueryRequested(query) {
searchField.text = query; searchField.text = query;
searchField.cursorPosition = query.length;
} }
function onModeChanged() { function onModeChanged() {
extFilterField.text = ""; extFilterField.text = "";
@@ -946,6 +980,15 @@ FocusScope {
keyNavigationBacktab: editEnvVarsField keyNavigationBacktab: editEnvVarsField
} }
} }
DankToggle {
id: editDgpuToggle
width: parent.width
text: I18n.tr("Launch on dGPU by default")
visible: SessionService.nvidiaCommand.length > 0
checked: false
onToggled: checked => editDgpuToggle.checked = checked
}
} }
} }
@@ -8,6 +8,9 @@ DankPopout {
layerNamespace: "dms:app-launcher" layerNamespace: "dms:app-launcher"
readonly property real screenWidth: screen?.width ?? 1920
readonly property real screenHeight: screen?.height ?? 1080
property string _pendingMode: "" property string _pendingMode: ""
property string _pendingQuery: "" property string _pendingQuery: ""
@@ -41,8 +44,35 @@ DankPopout {
openWithQuery(query); openWithQuery(query);
} }
popupWidth: 560 readonly property int _baseWidth: {
popupHeight: 640 switch (SettingsData.dankLauncherV2Size) {
case "micro":
return 500;
case "medium":
return 720;
case "large":
return 860;
default:
return 620;
}
}
readonly property int _baseHeight: {
switch (SettingsData.dankLauncherV2Size) {
case "micro":
return 480;
case "medium":
return 720;
case "large":
return 860;
default:
return 600;
}
}
popupWidth: Math.min(_baseWidth, screenWidth - 100)
popupHeight: Math.min(_baseHeight, screenHeight - 100)
triggerWidth: 40 triggerWidth: 40
positioning: "" positioning: ""
contentHandlesKeys: contentLoader.item?.launcherContent?.editMode ?? false contentHandlesKeys: contentLoader.item?.launcherContent?.editMode ?? false
@@ -60,7 +90,7 @@ DankPopout {
if (!lc) if (!lc)
return; return;
const query = _pendingQuery; const query = _pendingQuery || (SettingsData.rememberLastQuery ? SessionData.launcherLastQuery : "") || "";
const mode = _pendingMode || SessionData.appDrawerLastMode || "apps"; const mode = _pendingMode || SessionData.appDrawerLastMode || "apps";
_pendingMode = ""; _pendingMode = "";
_pendingQuery = ""; _pendingQuery = "";
@@ -72,12 +102,9 @@ DankPopout {
if (lc.controller) { if (lc.controller) {
lc.controller.searchMode = mode; lc.controller.searchMode = mode;
lc.controller.pluginFilter = ""; lc.controller.pluginFilter = "";
lc.controller.searchQuery = ""; lc.controller.searchQuery = query;
if (query) {
lc.controller.setSearchQuery(query); lc.controller.performSearch();
} else {
lc.controller.performSearch();
}
} }
lc.resetScroll?.(); lc.resetScroll?.();
lc.actionPanel?.hide(); lc.actionPanel?.hide();
@@ -106,7 +133,7 @@ DankPopout {
QtObject { QtObject {
id: modalAdapter id: modalAdapter
property bool spotlightOpen: appDrawerPopout.shouldBeVisible property bool spotlightOpen: appDrawerPopout.shouldBeVisible
property bool isClosing: appDrawerPopout.isClosing readonly property bool isClosing: !appDrawerPopout.shouldBeVisible
function hide() { function hide() {
appDrawerPopout.close(); appDrawerPopout.close();
@@ -136,11 +136,9 @@ DankPopout {
z: 5000 z: 5000
Behavior on opacity { Behavior on opacity {
enabled: !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: Theme.shortDuration duration: 200
easing.type: Easing.BezierSpline easing.type: Easing.OutCubic
easing.bezierCurve: root.shouldBeVisible ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
} }
+3 -14
View File
@@ -998,7 +998,6 @@ Item {
axis: barWindow.axis axis: barWindow.axis
barSpacing: barConfig?.spacing ?? 4 barSpacing: barConfig?.spacing ?? 4
barConfig: topBarContent.barConfig barConfig: topBarContent.barConfig
widgetData: parent.widgetData
isAutoHideBar: topBarContent.barConfig?.autoHide ?? false isAutoHideBar: topBarContent.barConfig?.autoHide ?? false
isAtBottom: barWindow.axis?.edge === "bottom" isAtBottom: barWindow.axis?.edge === "bottom"
visible: SettingsData.getFilteredScreens("systemTray").includes(barWindow.screen) && SystemTray.items.values.length > 0 visible: SettingsData.getFilteredScreens("systemTray").includes(barWindow.screen) && SystemTray.items.values.length > 0
@@ -1185,7 +1184,6 @@ Item {
if (!notificationCenterLoader.item) { if (!notificationCenterLoader.item) {
return; return;
} }
notificationCenterLoader.item.triggerScreen = barWindow.screen;
const effectiveBarConfig = topBarContent.barConfig; const effectiveBarConfig = topBarContent.barConfig;
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
if (notificationCenterLoader.item.setBarContext) { if (notificationCenterLoader.item.setBarContext) {
@@ -1468,21 +1466,12 @@ Item {
parentScreen: barWindow.screen parentScreen: barWindow.screen
onClicked: { onClicked: {
systemUpdateLoader.active = true; systemUpdateLoader.active = true;
if (!systemUpdateLoader.item)
return;
const popout = systemUpdateLoader.item;
const effectiveBarConfig = topBarContent.barConfig; const effectiveBarConfig = topBarContent.barConfig;
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
if (popout.setBarContext) { if (systemUpdateLoader.item && systemUpdateLoader.item.setBarContext) {
popout.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0); systemUpdateLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0);
} }
if (popout.setTriggerPosition) { systemUpdateLoader.item?.toggle();
const globalPos = visualContent.mapToItem(null, 0, 0);
const currentScreen = parentScreen || Screen;
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barWindow.effectiveBarThickness, visualWidth, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig);
popout.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig);
}
PopoutManager.requestPopout(popout, undefined, "systemUpdate");
} }
} }
} }
+1 -1
View File
@@ -661,7 +661,7 @@ PanelWindow {
anchors.left: !isVertical ? true : (barPos === SettingsData.Position.Left) anchors.left: !isVertical ? true : (barPos === SettingsData.Position.Left)
anchors.right: !isVertical ? true : (barPos === SettingsData.Position.Right) anchors.right: !isVertical ? true : (barPos === SettingsData.Position.Right)
exclusiveZone: (!(barConfig?.visible ?? true) || topBarCore.autoHide) ? -1 : (barWindow.effectiveBarThickness + effectiveSpacing + (Theme.isConnectedEffect ? 0 : (barConfig?.bottomGap ?? 0))) exclusiveZone: (!(barConfig?.visible ?? true) || topBarCore.autoHide) ? -1 : (barWindow.effectiveBarThickness + effectiveSpacing + (barConfig?.bottomGap ?? 0))
Item { Item {
id: inputMask id: inputMask
@@ -1,5 +1,4 @@
import QtQuick import QtQuick
import Quickshell.Services.UPower
import qs.Common import qs.Common
import qs.Modules.Plugins import qs.Modules.Plugins
import qs.Services import qs.Services
@@ -11,8 +10,6 @@ BasePill {
property bool batteryPopupVisible: false property bool batteryPopupVisible: false
property var popoutTarget: null property var popoutTarget: null
property real touchpadAccumulator: 0
readonly property int barPosition: { readonly property int barPosition: {
switch (axis?.edge) { switch (axis?.edge) {
case "top": case "top":
@@ -122,44 +119,5 @@ BasePill {
battery.triggerRipple(this, mouse.x, mouse.y); battery.triggerRipple(this, mouse.x, mouse.y);
toggleBatteryPopup(); toggleBatteryPopup();
} }
onWheel: wheel => {
var delta = wheel.angleDelta.y;
if (delta === 0)
return;
// Check if this is a touchpad
if (delta !== 120 && delta !== -120) {
touchpadAccumulator += delta;
console.info("Acc: "+touchpadAccumulator);
if (Math.abs(touchpadAccumulator) < 500)
return;
delta = touchpadAccumulator;
touchpadAccumulator = 0;
}
console.info("Trigger! Delta: "+delta)
// This is after the other delta checks so it only shows on valid Y scroll
if (typeof PowerProfiles === "undefined") {
ToastService.showError("power-profiles-daemon not available");
return;
}
// Get list of profiles, and current index
const profiles = [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []);
var index = profiles.findIndex(profile => PowerProfiles.profile === profile);
// Step once based on mouse wheel direction
if (delta > 0) index += 1;
else index -= 1;
// Already at end of list, can't go further
if (index < 0 || index >= profiles.length) return;
// Set new profile
PowerProfiles.profile = profiles[index];
if (PowerProfiles.profile !== profiles[index]) {
ToastService.showError("Failed to set power profile");
}
}
} }
} }
@@ -102,7 +102,7 @@ BasePill {
StyledTextMetrics { StyledTextMetrics {
id: cpuBaseline id: cpuBaseline
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
text: "100%" text: "88%"
} }
StyledTextMetrics { StyledTextMetrics {
@@ -17,7 +17,7 @@ BasePill {
property int availableWidth: 400 property int availableWidth: 400
readonly property int maxNormalWidth: 456 readonly property int maxNormalWidth: 456
readonly property int maxCompactWidth: 288 readonly property int maxCompactWidth: 288
property Toplevel activeWindow: null readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
property var activeDesktopEntry: null property var activeDesktopEntry: null
property bool isHovered: mouseArea.containsMouse property bool isHovered: mouseArea.containsMouse
property bool isAutoHideBar: false property bool isAutoHideBar: false
@@ -38,44 +38,10 @@ BasePill {
return 0; return 0;
} }
function updateActiveWindow() {
const active = ToplevelManager.activeToplevel;
if (!active) {
// Only clear if our tracked window is no longer alive
if (activeWindow) {
const alive = ToplevelManager.toplevels?.values;
if (alive && !Array.from(alive).some(t => t === activeWindow))
activeWindow = null;
}
return;
}
if (!parentScreen || CompositorService.filterCurrentDisplay([active], parentScreen?.name)?.length > 0) {
activeWindow = active;
}
// else: active window is on a different screen so keep the previous value
}
Component.onCompleted: { Component.onCompleted: {
updateActiveWindow();
updateDesktopEntry(); updateDesktopEntry();
} }
Connections {
target: ToplevelManager
function onActiveToplevelChanged() {
root.updateActiveWindow();
}
}
Connections {
target: CompositorService
function onToplevelsChanged() {
root.updateActiveWindow();
}
}
Connections { Connections {
target: DesktopEntries target: DesktopEntries
function onApplicationsChanged() { function onApplicationsChanged() {
+53 -114
View File
@@ -19,8 +19,7 @@ BasePill {
readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser
property bool compactMode: false property bool compactMode: false
property var widgetData: null property var widgetData: null
readonly property bool adaptiveWidthEnabled: SettingsData.mediaAdaptiveWidthEnabled readonly property int textWidth: {
readonly property int maxTextWidth: {
const size = widgetData?.mediaSize !== undefined ? widgetData.mediaSize : SettingsData.mediaSize; const size = widgetData?.mediaSize !== undefined ? widgetData.mediaSize : SettingsData.mediaSize;
switch (size) { switch (size) {
case 0: case 0:
@@ -37,7 +36,10 @@ BasePill {
if (isVerticalOrientation) { if (isVerticalOrientation) {
return widgetThickness - horizontalPadding * 2; return widgetThickness - horizontalPadding * 2;
} }
return 0; const controlsWidth = 20 + Theme.spacingXS + 24 + Theme.spacingXS + 20;
const audioVizWidth = 20;
const contentWidth = audioVizWidth + Theme.spacingXS + controlsWidth;
return contentWidth + (textWidth > 0 ? textWidth + Theme.spacingXS : 0);
} }
readonly property int currentContentHeight: { readonly property int currentContentHeight: {
if (!isVerticalOrientation) { if (!isVerticalOrientation) {
@@ -97,7 +99,7 @@ BasePill {
if (isMouseWheelY) { if (isMouseWheelY) {
if (deltaY > 0) { if (deltaY > 0) {
MprisController.previousOrRewind(); activePlayer.previous();
} else { } else {
activePlayer.next(); activePlayer.next();
} }
@@ -105,7 +107,7 @@ BasePill {
scrollAccumulatorY += deltaY; scrollAccumulatorY += deltaY;
if (Math.abs(scrollAccumulatorY) >= touchpadThreshold) { if (Math.abs(scrollAccumulatorY) >= touchpadThreshold) {
if (scrollAccumulatorY > 0) { if (scrollAccumulatorY > 0) {
MprisController.previousOrRewind(); activePlayer.previous();
} else { } else {
activePlayer.next(); activePlayer.next();
} }
@@ -117,28 +119,7 @@ BasePill {
content: Component { content: Component {
Item { Item {
id: contentRoot implicitWidth: root.playerAvailable ? root.currentContentWidth : 0
readonly property real measuredTextWidth: {
if (!root.playerAvailable || root.maxTextWidth <= 0 || !textContainer.visible)
return 0;
// Preserve the fixed-width text slot even if metadata is briefly empty.
if (!root.adaptiveWidthEnabled)
return root.maxTextWidth;
if (textContainer.displayText.length === 0)
return 0;
const rawWidth = mediaText.contentWidth;
if (!isFinite(rawWidth) || rawWidth <= 0)
return 0;
return Math.min(root.maxTextWidth, Math.ceil(rawWidth));
}
readonly property int horizontalContentWidth: {
const controlsWidth = 20 + Theme.spacingXS + 24 + Theme.spacingXS + 20;
const audioVizWidth = 20;
const baseWidth = audioVizWidth + Theme.spacingXS + controlsWidth;
return baseWidth + (measuredTextWidth > 0 ? measuredTextWidth + Theme.spacingXS : 0);
}
implicitWidth: root.playerAvailable ? (root.isVerticalOrientation ? root.currentContentWidth : horizontalContentWidth) : 0
implicitHeight: root.playerAvailable ? root.currentContentHeight : 0 implicitHeight: root.playerAvailable ? root.currentContentHeight : 0
opacity: root.playerAvailable ? 1 : 0 opacity: root.playerAvailable ? 1 : 0
@@ -151,9 +132,8 @@ BasePill {
Behavior on implicitWidth { Behavior on implicitWidth {
NumberAnimation { NumberAnimation {
duration: Theme.mediumDuration duration: Theme.shortDuration
easing.type: Easing.BezierSpline easing.type: Theme.standardEasing
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
} }
} }
@@ -234,7 +214,7 @@ BasePill {
if (mouse.button === Qt.LeftButton) { if (mouse.button === Qt.LeftButton) {
activePlayer.togglePlaying(); activePlayer.togglePlaying();
} else if (mouse.button === Qt.MiddleButton) { } else if (mouse.button === Qt.MiddleButton) {
MprisController.previousOrRewind(); activePlayer.previous();
} else if (mouse.button === Qt.RightButton) { } else if (mouse.button === Qt.RightButton) {
activePlayer.next(); activePlayer.next();
} }
@@ -289,7 +269,7 @@ BasePill {
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: contentRoot.measuredTextWidth width: textWidth
height: root.widgetThickness height: root.widgetThickness
visible: { visible: {
const size = widgetData?.mediaSize !== undefined ? widgetData.mediaSize : SettingsData.mediaSize; const size = widgetData?.mediaSize !== undefined ? widgetData.mediaSize : SettingsData.mediaSize;
@@ -298,95 +278,50 @@ BasePill {
clip: true clip: true
color: "transparent" color: "transparent"
Behavior on width { StyledText {
NumberAnimation { id: mediaText
duration: Theme.mediumDuration property bool needsScrolling: implicitWidth > textContainer.width && SettingsData.scrollTitleEnabled
easing.type: Easing.BezierSpline property real scrollOffset: 0
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
anchors.verticalCenter: parent.verticalCenter
text: textContainer.displayText
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
wrapMode: Text.NoWrap
x: needsScrolling ? -scrollOffset : 0
onTextChanged: {
scrollOffset = 0;
scrollAnimation.restart();
} }
}
Item { SequentialAnimation {
id: textClip id: scrollAnimation
anchors.fill: parent running: mediaText.needsScrolling && textContainer.visible
clip: true loops: Animation.Infinite
StyledText { PauseAnimation {
id: mediaText duration: 2000
property bool needsScrolling: implicitWidth > textContainer.width && SettingsData.scrollTitleEnabled
property real scrollOffset: 0
property real textShift: 0
anchors.verticalCenter: parent.verticalCenter
text: textContainer.displayText
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
wrapMode: Text.NoWrap
x: (needsScrolling ? -scrollOffset : 0) + textShift
opacity: 1
onTextChanged: {
scrollOffset = 0;
textShift = 0;
scrollAnimation.restart();
textChangeAnimation.restart();
} }
SequentialAnimation { NumberAnimation {
id: scrollAnimation target: mediaText
running: mediaText.needsScrolling && textContainer.visible property: "scrollOffset"
loops: Animation.Infinite from: 0
to: mediaText.implicitWidth - textContainer.width + 5
PauseAnimation { duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60)
duration: 2000 easing.type: Easing.Linear
}
NumberAnimation {
target: mediaText
property: "scrollOffset"
from: 0
to: mediaText.implicitWidth - textContainer.width + 5
duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60)
easing.type: Easing.Linear
}
PauseAnimation {
duration: 2000
}
NumberAnimation {
target: mediaText
property: "scrollOffset"
to: 0
duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60)
easing.type: Easing.Linear
}
} }
SequentialAnimation { PauseAnimation {
id: textChangeAnimation duration: 2000
}
ParallelAnimation { NumberAnimation {
NumberAnimation { target: mediaText
target: mediaText property: "scrollOffset"
property: "opacity" to: 0
from: 0.7 duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60)
to: 1 easing.type: Easing.Linear
duration: Theme.shortDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
NumberAnimation {
target: mediaText
property: "textShift"
from: 4
to: 0
duration: Theme.shortDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
}
} }
} }
} }
@@ -435,7 +370,11 @@ BasePill {
anchors.fill: parent anchors.fill: parent
enabled: root.playerAvailable enabled: root.playerAvailable
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: MprisController.previousOrRewind() onClicked: {
if (activePlayer) {
activePlayer.previous();
}
}
} }
} }
@@ -271,7 +271,7 @@ BasePill {
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
if (isFocused) { if (isFocused) {
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.45); return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.2);
} }
return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"; return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
} }
@@ -526,7 +526,7 @@ BasePill {
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
if (isFocused) { if (isFocused) {
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.45); return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.2);
} }
return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"; return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
} }
@@ -16,11 +16,8 @@ BasePill {
enableCursor: false enableCursor: false
property var parentWindow: null property var parentWindow: null
property var widgetData: null
property string section: "right"
property bool isAtBottom: false property bool isAtBottom: false
property bool isAutoHideBar: false property bool isAutoHideBar: false
property bool useOverflowPopup: !widgetData?.trayUseInlineExpansion
readonly property var hiddenTrayIds: { readonly property var hiddenTrayIds: {
const envValue = Quickshell.env("DMS_HIDE_TRAYIDS") || ""; const envValue = Quickshell.env("DMS_HIDE_TRAYIDS") || "";
return envValue ? envValue.split(",").map(id => id.trim().toLowerCase()) : []; return envValue ? envValue.split(",").map(id => id.trim().toLowerCase()) : [];
@@ -43,76 +40,6 @@ BasePill {
return `${id}::${tooltipTitle}`; return `${id}::${tooltipTitle}`;
} }
function trayIconSourceFor(trayItem) {
let icon = trayItem && trayItem.icon;
if (typeof icon === 'string' || icon instanceof String) {
if (icon === "")
return "";
if (icon.includes("?path=")) {
const split = icon.split("?path=");
if (split.length !== 2)
return icon;
const name = split[0];
const path = split[1];
let fileName = name.substring(name.lastIndexOf("/") + 1);
if (fileName.startsWith("dropboxstatus")) {
fileName = `hicolor/16x16/status/${fileName}`;
}
return `file://${path}/${fileName}`;
}
if (icon.startsWith("/") && !icon.startsWith("file://"))
return `file://${icon}`;
return icon;
}
return "";
}
function activateInlineTrayItem(trayItem, anchorItem) {
if (!trayItem)
return;
if (!trayItem.onlyMenu) {
trayItem.activate();
return;
}
if (!trayItem.hasMenu)
return;
root.showForTrayItem(trayItem, anchorItem, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
}
function openInlineTrayContextMenu(trayItem, areaItem, mouse, anchorItem) {
if (!trayItem) {
return;
}
if (!trayItem.hasMenu) {
const gp = areaItem.mapToGlobal(mouse.x, mouse.y);
root.callContextMenuFallback(trayItem.id, Math.round(gp.x), Math.round(gp.y));
return;
}
root.showForTrayItem(trayItem, anchorItem, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
}
function toggleIconName() {
const edge = root.axis?.edge;
if (root.useOverflowPopup) {
switch (edge) {
case "left":
return root.menuOpen ? "keyboard_arrow_left" : "keyboard_arrow_right";
case "right":
return root.menuOpen ? "keyboard_arrow_right" : "keyboard_arrow_left";
case "bottom":
return root.menuOpen ? "keyboard_arrow_down" : "keyboard_arrow_up";
case "top":
return root.menuOpen ? "keyboard_arrow_up" : "keyboard_arrow_down";
}
}
if (edge === "left" || edge === "right") {
return root.menuOpen == (root.section !== "right") ? "keyboard_arrow_up" : "keyboard_arrow_down";
}
return root.menuOpen != (root.section === "right") ? "keyboard_arrow_left" : "keyboard_arrow_right";
}
// ! TODO - replace with either native dbus client (like plugins use) or just a DMS cli or something // ! TODO - replace with either native dbus client (like plugins use) or just a DMS cli or something
function callContextMenuFallback(trayItemId, globalX, globalY) { function callContextMenuFallback(trayItemId, globalX, globalY) {
const script = ['ITEMS=$(dbus-send --session --print-reply --dest=org.kde.StatusNotifierWatcher /StatusNotifierWatcher org.freedesktop.DBus.Properties.Get string:org.kde.StatusNotifierWatcher string:RegisteredStatusNotifierItems 2>/dev/null)', 'while IFS= read -r line; do', ' line="${line#*\\\"}"', ' line="${line%\\\"*}"', ' [ -z "$line" ] && continue', ' BUS="${line%%/*}"', ' OBJ="/${line#*/}"', ' ID=$(dbus-send --session --print-reply --dest="$BUS" "$OBJ" org.freedesktop.DBus.Properties.Get string:org.kde.StatusNotifierItem string:Id 2>/dev/null | grep -oP "(?<=\\\")(.*?)(?=\\\")" | tail -1)', ' if [ "$ID" = "$1" ]; then', ' dbus-send --session --type=method_call --dest="$BUS" "$OBJ" org.kde.StatusNotifierItem.ContextMenu int32:"$2" int32:"$3"', ' exit 0', ' fi', 'done <<< "$ITEMS"',].join("\n"); const script = ['ITEMS=$(dbus-send --session --print-reply --dest=org.kde.StatusNotifierWatcher /StatusNotifierWatcher org.freedesktop.DBus.Properties.Get string:org.kde.StatusNotifierWatcher string:RegisteredStatusNotifierItems 2>/dev/null)', 'while IFS= read -r line; do', ' line="${line#*\\\"}"', ' line="${line%\\\"*}"', ' [ -z "$line" ] && continue', ' BUS="${line%%/*}"', ' OBJ="/${line#*/}"', ' ID=$(dbus-send --session --print-reply --dest="$BUS" "$OBJ" org.freedesktop.DBus.Properties.Get string:org.kde.StatusNotifierItem string:Id 2>/dev/null | grep -oP "(?<=\\\")(.*?)(?=\\\")" | tail -1)', ' if [ "$ID" = "$1" ]; then', ' dbus-send --session --type=method_call --dest="$BUS" "$OBJ" org.kde.StatusNotifierItem.ContextMenu int32:"$2" int32:"$3"', ' exit 0', ' fi', 'done <<< "$ITEMS"',].join("\n");
@@ -151,13 +78,6 @@ BasePill {
item: item item: item
})) }))
readonly property var hiddenBarItems: allSortedTrayItems.filter(item => SessionData.isHiddenTrayId(root.getTrayItemKey(item))) readonly property var hiddenBarItems: allSortedTrayItems.filter(item => SessionData.isHiddenTrayId(root.getTrayItemKey(item)))
readonly property bool reverseInlineHorizontal: !useOverflowPopup && !isVerticalOrientation && section === "right"
readonly property bool reverseInlineVertical: !useOverflowPopup && isVerticalOrientation && section === "right"
readonly property var displayedMainBarItems: reverseInlineHorizontal ? [...mainBarItems].reverse() : mainBarItems
readonly property var displayedInlineExpandedItems: (reverseInlineHorizontal ? [...hiddenBarItems].reverse() : hiddenBarItems).map(item => ({
key: getTrayItemKey(item),
item: item
}))
function moveTrayItemInFullOrder(visibleFromIndex, visibleToIndex) { function moveTrayItemInFullOrder(visibleFromIndex, visibleToIndex) {
if (visibleFromIndex === visibleToIndex || visibleFromIndex < 0 || visibleToIndex < 0) if (visibleFromIndex === visibleToIndex || visibleFromIndex < 0 || visibleToIndex < 0)
@@ -183,7 +103,6 @@ BasePill {
property int dropTargetIndex: -1 property int dropTargetIndex: -1
property bool suppressShiftAnimation: false property bool suppressShiftAnimation: false
readonly property bool hasHiddenItems: allTrayItems.length > mainBarItems.length readonly property bool hasHiddenItems: allTrayItems.length > mainBarItems.length
readonly property bool inlineExpanded: hasHiddenItems && !useOverflowPopup && menuOpen
visible: allTrayItems.length > 0 visible: allTrayItems.length > 0
opacity: allTrayItems.length > 0 ? 1 : 0 opacity: allTrayItems.length > 0 ? 1 : 0
@@ -279,11 +198,10 @@ BasePill {
id: rowComp id: rowComp
Row { Row {
spacing: 0 spacing: 0
layoutDirection: root.reverseInlineHorizontal ? Qt.RightToLeft : Qt.LeftToRight
Repeater { Repeater {
model: ScriptModel { model: ScriptModel {
values: root.displayedMainBarItems values: root.mainBarItems
objectProp: "key" objectProp: "key"
} }
@@ -291,7 +209,29 @@ BasePill {
id: delegateRoot id: delegateRoot
property var trayItem: modelData.item property var trayItem: modelData.item
property string itemKey: modelData.key property string itemKey: modelData.key
property string iconSource: root.trayIconSourceFor(trayItem) property string iconSource: {
let icon = trayItem && trayItem.icon;
if (typeof icon === 'string' || icon instanceof String) {
if (icon === "")
return "";
if (icon.includes("?path=")) {
const split = icon.split("?path=");
if (split.length !== 2)
return icon;
const name = split[0];
const path = split[1];
let fileName = name.substring(name.lastIndexOf("/") + 1);
if (fileName.startsWith("dropboxstatus")) {
fileName = `hicolor/16x16/status/${fileName}`;
}
return `file://${path}/${fileName}`;
}
if (icon.startsWith("/") && !icon.startsWith("file://"))
return `file://${icon}`;
return icon;
}
return "";
}
width: root.trayItemSize width: root.trayItemSize
height: root.barThickness height: root.barThickness
@@ -431,8 +371,7 @@ BasePill {
} }
if (!delegateRoot.trayItem.hasMenu) if (!delegateRoot.trayItem.hasMenu)
return; return;
if (root.useOverflowPopup) root.menuOpen = false;
root.menuOpen = false;
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis); root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
} }
@@ -441,8 +380,8 @@ BasePill {
const distance = Math.abs(mouse.x - dragHandler.dragStartPos.x); const distance = Math.abs(mouse.x - dragHandler.dragStartPos.x);
if (distance > 5) { if (distance > 5) {
dragHandler.dragging = true; dragHandler.dragging = true;
root.draggedIndex = root.reverseInlineHorizontal ? (root.mainBarItems.length - 1 - index) : index; root.draggedIndex = index;
root.dropTargetIndex = root.draggedIndex; root.dropTargetIndex = index;
} }
} }
if (!dragHandler.dragging) if (!dragHandler.dragging)
@@ -452,8 +391,7 @@ BasePill {
dragHandler.dragAxisOffset = axisOffset; dragHandler.dragAxisOffset = axisOffset;
const itemSize = root.trayItemSize; const itemSize = root.trayItemSize;
const slotOffset = Math.round(axisOffset / itemSize); const slotOffset = Math.round(axisOffset / itemSize);
const visualTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset)); const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
const newTargetIndex = root.reverseInlineHorizontal ? (root.mainBarItems.length - 1 - visualTargetIndex) : visualTargetIndex;
if (newTargetIndex !== root.dropTargetIndex) { if (newTargetIndex !== root.dropTargetIndex) {
root.dropTargetIndex = newTargetIndex; root.dropTargetIndex = newTargetIndex;
} }
@@ -469,8 +407,7 @@ BasePill {
root.callContextMenuFallback(delegateRoot.trayItem.id, Math.round(gp.x), Math.round(gp.y)); root.callContextMenuFallback(delegateRoot.trayItem.id, Math.round(gp.x), Math.round(gp.y));
return; return;
} }
if (root.useOverflowPopup) root.menuOpen = false;
root.menuOpen = false;
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis); root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
} }
} }
@@ -492,7 +429,7 @@ BasePill {
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: root.toggleIconName() name: root.menuOpen ? "expand_less" : "expand_more"
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: Theme.widgetTextColor color: Theme.widgetTextColor
} }
@@ -514,301 +451,6 @@ BasePill {
} }
} }
} }
Repeater {
model: ScriptModel {
values: root.displayedInlineExpandedItems
objectProp: "key"
}
delegate: inlineExpandedTrayItemDelegate
}
}
}
Component {
id: inlineExpandedTrayItemDelegate
Item {
property var trayItem: modelData.item
property string itemKey: modelData.key
property string iconSource: root.trayIconSourceFor(trayItem)
width: root.isVerticalOrientation ? root.barThickness : (root.inlineExpanded ? root.trayItemSize : 0)
height: root.isVerticalOrientation ? (root.inlineExpanded ? root.trayItemSize : 0) : root.barThickness
visible: width > 0 || height > 0
Behavior on width {
enabled: !root.isVerticalOrientation
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on height {
enabled: root.isVerticalOrientation
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Rectangle {
id: inlineVisualContent
width: root.trayItemSize
height: root.trayItemSize
x: root.isVerticalOrientation ? Math.round((parent.width - width) / 2) : (root.reverseInlineHorizontal ? parent.width - width : 0)
y: root.isVerticalOrientation ? (root.reverseInlineVertical ? parent.height - height : 0) : Math.round((parent.height - height) / 2)
radius: Theme.cornerRadius
color: inlineTrayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
opacity: root.inlineExpanded ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
IconImage {
id: inlineIconImg
anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
source: iconSource
asynchronous: true
smooth: true
mipmap: true
visible: status === Image.Ready
}
Text {
anchors.centerIn: parent
visible: !inlineIconImg.visible
text: {
const itemId = trayItem?.id || "";
if (!itemId)
return "?";
return itemId.charAt(0).toUpperCase();
}
font.pixelSize: 10
color: Theme.widgetTextColor
}
DankRipple {
id: inlineItemRipple
cornerRadius: Theme.cornerRadius
}
}
MouseArea {
id: inlineTrayItemArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
enabled: root.inlineExpanded
onPressed: mouse => {
const pos = mapToItem(inlineVisualContent, mouse.x, mouse.y);
inlineItemRipple.trigger(pos.x, pos.y);
}
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
root.activateInlineTrayItem(trayItem, inlineVisualContent);
return;
}
if (mouse.button !== Qt.RightButton)
return;
root.openInlineTrayContextMenu(trayItem, inlineTrayItemArea, mouse, inlineVisualContent);
}
}
}
}
Component {
id: verticalMainTrayItemDelegate
Item {
property var trayItem: modelData.item
property string itemKey: modelData.key
property string iconSource: root.trayIconSourceFor(trayItem)
width: root.barThickness
height: root.trayItemSize
z: dragHandler.dragging ? 100 : 0
property real shiftOffset: {
if (root.draggedIndex < 0)
return 0;
if (index === root.draggedIndex)
return 0;
const dragIdx = root.draggedIndex;
const dropIdx = root.dropTargetIndex;
const shiftAmount = root.trayItemSize;
if (dropIdx < 0)
return 0;
if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
return -shiftAmount;
if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx)
return shiftAmount;
return 0;
}
transform: Translate {
y: shiftOffset
Behavior on y {
enabled: !root.suppressShiftAnimation
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
}
Item {
id: dragHandler
anchors.fill: parent
property bool dragging: false
property point dragStartPos: Qt.point(0, 0)
property real dragAxisOffset: 0
property bool longPressing: false
Timer {
id: longPressTimer
interval: 400
repeat: false
onTriggered: dragHandler.longPressing = true
}
}
Rectangle {
id: visualContent
width: root.trayItemSize
height: root.trayItemSize
anchors.centerIn: parent
radius: Theme.cornerRadius
color: trayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
border.width: dragHandler.dragging ? 2 : 0
border.color: Theme.primary
opacity: dragHandler.dragging ? 0.8 : 1.0
transform: Translate {
y: dragHandler.dragging ? dragHandler.dragAxisOffset : 0
}
IconImage {
id: iconImg
anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
source: iconSource
asynchronous: true
smooth: true
mipmap: true
visible: status === Image.Ready
}
Text {
anchors.centerIn: parent
visible: !iconImg.visible
text: {
const itemId = trayItem?.id || "";
if (!itemId)
return "?";
return itemId.charAt(0).toUpperCase();
}
font.pixelSize: 10
color: Theme.widgetTextColor
}
DankRipple {
id: itemRipple
cornerRadius: Theme.cornerRadius
}
}
MouseArea {
id: trayItemArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: dragHandler.longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
onPressed: mouse => {
const pos = mapToItem(visualContent, mouse.x, mouse.y);
itemRipple.trigger(pos.x, pos.y);
if (mouse.button === Qt.LeftButton) {
dragHandler.dragStartPos = Qt.point(mouse.x, mouse.y);
longPressTimer.start();
}
}
onReleased: mouse => {
longPressTimer.stop();
const wasDragging = dragHandler.dragging;
const didReorder = wasDragging && root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex;
if (didReorder) {
root.suppressShiftAnimation = true;
root.moveTrayItemInFullOrder(root.draggedIndex, root.dropTargetIndex);
Qt.callLater(() => root.suppressShiftAnimation = false);
}
dragHandler.longPressing = false;
dragHandler.dragging = false;
dragHandler.dragAxisOffset = 0;
root.draggedIndex = -1;
root.dropTargetIndex = -1;
if (wasDragging || mouse.button !== Qt.LeftButton)
return;
if (!trayItem)
return;
if (!trayItem.onlyMenu) {
trayItem.activate();
return;
}
if (!trayItem.hasMenu)
return;
if (root.useOverflowPopup)
root.menuOpen = false;
root.showForTrayItem(trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
}
onPositionChanged: mouse => {
if (dragHandler.longPressing && !dragHandler.dragging) {
const distance = Math.abs(mouse.y - dragHandler.dragStartPos.y);
if (distance > 5) {
dragHandler.dragging = true;
root.draggedIndex = index;
root.dropTargetIndex = root.draggedIndex;
}
}
if (!dragHandler.dragging)
return;
const axisOffset = mouse.y - dragHandler.dragStartPos.y;
dragHandler.dragAxisOffset = axisOffset;
const itemSize = root.trayItemSize;
const slotOffset = Math.round(axisOffset / itemSize);
const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
if (newTargetIndex !== root.dropTargetIndex) {
root.dropTargetIndex = newTargetIndex;
}
}
onClicked: mouse => {
if (dragHandler.dragging)
return;
if (mouse.button !== Qt.RightButton)
return;
root.openInlineTrayContextMenu(trayItem, trayItemArea, mouse, visualContent);
}
}
} }
} }
@@ -817,23 +459,219 @@ BasePill {
Column { Column {
spacing: 0 spacing: 0
// Column lacks layoutDirection, so we use four repeaters with mutually exclusive models to control whether main items or expanded items appear above/ below the toggle button.
// When reverseInlineVertical is true the first and third repeaters are empty and the second and fourth are active, and vice-versa.
// Because items are swapped between repeaters rather than reversed within a single list, vertical drag-and-drop indices don't need remapping (unlike the horizontal RightToLeft case).
Repeater { Repeater {
model: ScriptModel { model: ScriptModel {
values: root.reverseInlineVertical ? [] : root.displayedMainBarItems values: root.mainBarItems
objectProp: "key" objectProp: "key"
} }
delegate: verticalMainTrayItemDelegate
}
Repeater { delegate: Item {
model: ScriptModel { id: delegateRoot
values: root.reverseInlineVertical ? root.displayedInlineExpandedItems : [] property var trayItem: modelData.item
objectProp: "key" property string itemKey: modelData.key
property string iconSource: {
let icon = trayItem && trayItem.icon;
if (typeof icon === 'string' || icon instanceof String) {
if (icon === "")
return "";
if (icon.includes("?path=")) {
const split = icon.split("?path=");
if (split.length !== 2)
return icon;
const name = split[0];
const path = split[1];
let fileName = name.substring(name.lastIndexOf("/") + 1);
if (fileName.startsWith("dropboxstatus")) {
fileName = `hicolor/16x16/status/${fileName}`;
}
return `file://${path}/${fileName}`;
}
if (icon.startsWith("/") && !icon.startsWith("file://"))
return `file://${icon}`;
return icon;
}
return "";
}
width: root.barThickness
height: root.trayItemSize
z: dragHandler.dragging ? 100 : 0
property real shiftOffset: {
if (root.draggedIndex < 0)
return 0;
if (index === root.draggedIndex)
return 0;
const dragIdx = root.draggedIndex;
const dropIdx = root.dropTargetIndex;
const shiftAmount = root.trayItemSize;
if (dropIdx < 0)
return 0;
if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
return -shiftAmount;
if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx)
return shiftAmount;
return 0;
}
transform: Translate {
y: delegateRoot.shiftOffset
Behavior on y {
enabled: !root.suppressShiftAnimation
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
}
Item {
id: dragHandler
anchors.fill: parent
property bool dragging: false
property point dragStartPos: Qt.point(0, 0)
property real dragAxisOffset: 0
property bool longPressing: false
Timer {
id: longPressTimer
interval: 400
repeat: false
onTriggered: dragHandler.longPressing = true
}
}
Rectangle {
id: visualContent
width: root.trayItemSize
height: root.trayItemSize
anchors.centerIn: parent
radius: Theme.cornerRadius
color: trayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
border.width: dragHandler.dragging ? 2 : 0
border.color: Theme.primary
opacity: dragHandler.dragging ? 0.8 : 1.0
transform: Translate {
y: dragHandler.dragging ? dragHandler.dragAxisOffset : 0
}
IconImage {
id: iconImg
anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
source: delegateRoot.iconSource
asynchronous: true
smooth: true
mipmap: true
visible: status === Image.Ready
}
Text {
anchors.centerIn: parent
visible: !iconImg.visible
text: {
const itemId = trayItem?.id || "";
if (!itemId)
return "?";
return itemId.charAt(0).toUpperCase();
}
font.pixelSize: 10
color: Theme.widgetTextColor
}
DankRipple {
id: itemRipple
cornerRadius: Theme.cornerRadius
}
}
MouseArea {
id: trayItemArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: dragHandler.longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
onPressed: mouse => {
const pos = mapToItem(visualContent, mouse.x, mouse.y);
itemRipple.trigger(pos.x, pos.y);
if (mouse.button === Qt.LeftButton) {
dragHandler.dragStartPos = Qt.point(mouse.x, mouse.y);
longPressTimer.start();
}
}
onReleased: mouse => {
longPressTimer.stop();
const wasDragging = dragHandler.dragging;
const didReorder = wasDragging && root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex;
if (didReorder) {
root.suppressShiftAnimation = true;
root.moveTrayItemInFullOrder(root.draggedIndex, root.dropTargetIndex);
Qt.callLater(() => root.suppressShiftAnimation = false);
}
dragHandler.longPressing = false;
dragHandler.dragging = false;
dragHandler.dragAxisOffset = 0;
root.draggedIndex = -1;
root.dropTargetIndex = -1;
if (wasDragging || mouse.button !== Qt.LeftButton)
return;
if (!delegateRoot.trayItem)
return;
if (!delegateRoot.trayItem.onlyMenu) {
delegateRoot.trayItem.activate();
return;
}
if (!delegateRoot.trayItem.hasMenu)
return;
root.menuOpen = false;
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
}
onPositionChanged: mouse => {
if (dragHandler.longPressing && !dragHandler.dragging) {
const distance = Math.abs(mouse.y - dragHandler.dragStartPos.y);
if (distance > 5) {
dragHandler.dragging = true;
root.draggedIndex = index;
root.dropTargetIndex = index;
}
}
if (!dragHandler.dragging)
return;
const axisOffset = mouse.y - dragHandler.dragStartPos.y;
dragHandler.dragAxisOffset = axisOffset;
const itemSize = root.trayItemSize;
const slotOffset = Math.round(axisOffset / itemSize);
const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
if (newTargetIndex !== root.dropTargetIndex) {
root.dropTargetIndex = newTargetIndex;
}
}
onClicked: mouse => {
if (dragHandler.dragging)
return;
if (mouse.button !== Qt.RightButton)
return;
if (!delegateRoot.trayItem?.hasMenu) {
const gp = trayItemArea.mapToGlobal(mouse.x, mouse.y);
root.callContextMenuFallback(delegateRoot.trayItem.id, Math.round(gp.x), Math.round(gp.y));
return;
}
root.menuOpen = false;
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
}
}
} }
delegate: inlineExpandedTrayItemDelegate
} }
Item { Item {
@@ -851,7 +689,14 @@ BasePill {
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: root.toggleIconName() name: {
const edge = root.axis?.edge;
if (edge === "left") {
return root.menuOpen ? "chevron_left" : "chevron_right";
} else {
return root.menuOpen ? "chevron_right" : "chevron_left";
}
}
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: Theme.widgetTextColor color: Theme.widgetTextColor
} }
@@ -873,22 +718,6 @@ BasePill {
} }
} }
} }
Repeater {
model: ScriptModel {
values: root.reverseInlineVertical ? [] : root.displayedInlineExpandedItems
objectProp: "key"
}
delegate: inlineExpandedTrayItemDelegate
}
Repeater {
model: ScriptModel {
values: root.reverseInlineVertical ? root.displayedMainBarItems : []
objectProp: "key"
}
delegate: verticalMainTrayItemDelegate
}
} }
} }
@@ -904,7 +733,7 @@ BasePill {
blurRadius: Theme.cornerRadius blurRadius: Theme.cornerRadius
} }
visible: root.useOverflowPopup && root.menuOpen visible: root.menuOpen
screen: root.parentScreen screen: root.parentScreen
WlrLayershell.layer: WlrLayershell.Top WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.exclusiveZone: -1 WlrLayershell.exclusiveZone: -1
@@ -920,14 +749,13 @@ BasePill {
HyprlandFocusGrab { HyprlandFocusGrab {
windows: [overflowMenu] windows: [overflowMenu]
active: CompositorService.useHyprlandFocusGrab && root.useOverflowPopup && root.menuOpen active: CompositorService.useHyprlandFocusGrab && root.menuOpen
} }
Connections { Connections {
target: PopoutManager target: PopoutManager
function onPopoutOpening() { function onPopoutOpening() {
if (root.useOverflowPopup) root.menuOpen = false;
root.menuOpen = false;
} }
} }
@@ -1193,7 +1021,30 @@ BasePill {
delegate: Rectangle { delegate: Rectangle {
property var trayItem: modelData property var trayItem: modelData
property string iconSource: root.trayIconSourceFor(trayItem) property string iconSource: {
let icon = trayItem?.icon;
if (typeof icon === 'string' || icon instanceof String) {
if (icon === "")
return "";
if (icon.includes("?path=")) {
const split = icon.split("?path=");
if (split.length !== 2)
return icon;
const name = split[0];
const path = split[1];
let fileName = name.substring(name.lastIndexOf("/") + 1);
if (fileName.startsWith("dropboxstatus")) {
fileName = `hicolor/16x16/status/${fileName}`;
}
return `file://${path}/${fileName}`;
}
if (icon.startsWith("/") && !icon.startsWith("file://")) {
return `file://${icon}`;
}
return icon;
}
return "";
}
width: root.trayItemSize + 4 width: root.trayItemSize + 4
height: root.trayItemSize + 4 height: root.trayItemSize + 4
@@ -1462,8 +1313,7 @@ BasePill {
onVisibleChanged: { onVisibleChanged: {
if (visible) { if (visible) {
updatePosition(); updatePosition();
if (root.useOverflowPopup) root.menuOpen = false;
root.menuOpen = false;
PopoutManager.closeAllPopouts(); PopoutManager.closeAllPopouts();
ModalManager.closeAllModalsExcept(null); ModalManager.closeAllModalsExcept(null);
} }
@@ -16,6 +16,7 @@ DankPopout {
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500 popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
triggerWidth: 80 triggerWidth: 80
screen: triggerScreen screen: triggerScreen
shouldBeVisible: dashVisible
property bool __focusArmed: false property bool __focusArmed: false
property bool __contentReady: false property bool __contentReady: false
@@ -99,7 +100,7 @@ DankPopout {
if (currentPlayer && currentPlayer !== player && currentPlayer.canPause) { if (currentPlayer && currentPlayer !== player && currentPlayer.canPause) {
currentPlayer.pause(); currentPlayer.pause();
} }
MprisController.setActivePlayer(player); MprisController.activePlayer = player;
root.__hideDropdowns(); root.__hideDropdowns();
} }
onDeviceSelected: device => { onDeviceSelected: device => {
@@ -44,43 +44,6 @@ Item {
property int __volumeHoverCount: 0 property int __volumeHoverCount: 0
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
function panelMotionX(panelWidth, active) {
if (active)
return 0;
if (directionalEffect) {
const travel = Math.max(Theme.effectAnimOffset, panelWidth * 0.85);
return isRightEdge ? -travel : travel;
}
if (depthEffect) {
const travel = Math.max(Theme.effectAnimOffset * 0.7, panelWidth * 0.32);
return isRightEdge ? -travel : travel;
}
return 0;
}
function panelMotionY(panelType, panelHeight, active) {
if (active)
return 0;
if (directionalEffect) {
if (panelType === 2)
return panelHeight * 0.08;
if (panelType === 3)
return -panelHeight * 0.08;
return 0;
}
if (depthEffect) {
if (panelType === 2)
return panelHeight * 0.04;
if (panelType === 3)
return -panelHeight * 0.04;
return 0;
}
return 0;
}
function volumeAreaEntered() { function volumeAreaEntered() {
__volumeHoverCount++; __volumeHoverCount++;
panelEntered(); panelEntered();
@@ -99,47 +62,30 @@ Item {
visible: dropdownType === 1 && volumeAvailable visible: dropdownType === 1 && volumeAvailable
width: 60 width: 60
height: 180 height: 180
x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 1) x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2 + panelMotionY(1, height, dropdownType === 1) y: anchorPos.y - height / 2
radius: Theme.cornerRadius * 2 radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
border.width: 1 border.width: 1
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : 0) opacity: dropdownType === 1 ? 1 : 0
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : Theme.effectScaleCollapsed) scale: dropdownType === 1 ? 1 : 0.96
transformOrigin: isRightEdge ? Item.Left : Item.Right transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity { Behavior on opacity {
enabled: !Theme.isDirectionalEffect NumberAnimation {
DankAnim { duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1) * Theme.variantOpacityDurationScale) easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
} }
} }
Behavior on scale { Behavior on scale {
enabled: !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1) duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on y {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
@@ -251,50 +197,33 @@ Item {
Rectangle { Rectangle {
id: audioDevicesPanel id: audioDevicesPanel
visible: dropdownType === 2 && activePlayer !== null visible: dropdownType === 2
width: 280 width: 280
height: Math.max(200, Math.min(280, availableDevices.length * 50 + 100)) height: Math.max(200, Math.min(280, availableDevices.length * 50 + 100))
x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 2) x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2 + panelMotionY(2, height, dropdownType === 2) y: anchorPos.y - height / 2
radius: Theme.cornerRadius * 2 radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98) color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
border.width: 2 border.width: 2
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : 0) opacity: dropdownType === 2 ? 1 : 0
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : Theme.effectScaleCollapsed) scale: dropdownType === 2 ? 1 : 0.96
transformOrigin: isRightEdge ? Item.Left : Item.Right transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity { Behavior on opacity {
enabled: !Theme.isDirectionalEffect NumberAnimation {
DankAnim { duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2) * Theme.variantOpacityDurationScale) easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
} }
} }
Behavior on scale { Behavior on scale {
enabled: !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2) duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on y {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
@@ -425,47 +354,30 @@ Item {
visible: dropdownType === 3 visible: dropdownType === 3
width: 240 width: 240
height: Math.max(180, Math.min(240, (allPlayers?.length || 0) * 50 + 80)) height: Math.max(180, Math.min(240, (allPlayers?.length || 0) * 50 + 80))
x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 3) x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2 + panelMotionY(3, height, dropdownType === 3) y: anchorPos.y - height / 2
radius: Theme.cornerRadius * 2 radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98) color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
border.width: 2 border.width: 2
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : 0) opacity: dropdownType === 3 ? 1 : 0
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : Theme.effectScaleCollapsed) scale: dropdownType === 3 ? 1 : 0.96
transformOrigin: isRightEdge ? Item.Left : Item.Right transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity { Behavior on opacity {
enabled: !Theme.isDirectionalEffect NumberAnimation {
DankAnim { duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3) * Theme.variantOpacityDurationScale) easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
} }
} }
Behavior on scale { Behavior on scale {
enabled: !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3) duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on y {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
+11 -1
View File
@@ -487,7 +487,17 @@ Item {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: MprisController.previousOrRewind() onClicked: {
if (!activePlayer) {
return;
}
if (activePlayer.position > 8 && activePlayer.canSeek) {
activePlayer.position = 0;
} else {
activePlayer.previous();
}
}
} }
} }
} }
@@ -145,7 +145,14 @@ Card {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: MprisController.previousOrRewind() onClicked: {
if (!activePlayer) return
if (activePlayer.position > 8 && activePlayer.canSeek) {
activePlayer.position = 0
} else {
activePlayer.previous()
}
}
} }
} }
+34 -214
View File
@@ -19,12 +19,11 @@ Variants {
WindowBlur { WindowBlur {
targetWindow: dock targetWindow: dock
blurEnabled: dock.effectiveBlurEnabled && !SettingsData.connectedFrameModeActive
blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x
blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y
blurWidth: dock.hasApps && dock.reveal ? dockBackground.width : 0 blurWidth: dock.hasApps && dock.reveal ? dockBackground.width : 0
blurHeight: dock.hasApps && dock.reveal ? dockBackground.height : 0 blurHeight: dock.hasApps && dock.reveal ? dockBackground.height : 0
blurRadius: Theme.isConnectedEffect ? Theme.connectedCornerRadius : dock.surfaceRadius blurRadius: Theme.cornerRadius
} }
WlrLayershell.namespace: "dms:dock" WlrLayershell.namespace: "dms:dock"
@@ -43,23 +42,6 @@ Variants {
property real backgroundTransparency: SettingsData.dockTransparency property real backgroundTransparency: SettingsData.dockTransparency
property bool groupByApp: SettingsData.dockGroupByApp property bool groupByApp: SettingsData.dockGroupByApp
readonly property int borderThickness: SettingsData.dockBorderEnabled ? SettingsData.dockBorderThickness : 0 readonly property int borderThickness: SettingsData.dockBorderEnabled ? SettingsData.dockBorderThickness : 0
readonly property string connectedBarSide: SettingsData.dockPosition === SettingsData.Position.Top ? "top" : SettingsData.dockPosition === SettingsData.Position.Bottom ? "bottom" : SettingsData.dockPosition === SettingsData.Position.Left ? "left" : "right"
readonly property bool connectedBarActiveOnEdge: Theme.isConnectedEffect && !!(dock.screen || modelData) && SettingsData.getActiveBarEdgesForScreen(dock.screen || modelData).includes(connectedBarSide)
readonly property real connectedJoinInset: {
if (!Theme.isConnectedEffect)
return 0;
return connectedBarActiveOnEdge ? SettingsData.frameBarSize : SettingsData.frameThickness;
}
readonly property real surfaceRadius: Theme.connectedSurfaceRadius
readonly property color surfaceColor: Theme.isConnectedEffect ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency)
readonly property color surfaceBorderColor: Theme.isConnectedEffect ? "transparent" : BlurService.borderColor
readonly property real surfaceBorderWidth: Theme.isConnectedEffect ? 0 : BlurService.borderWidth
readonly property real surfaceTopLeftRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius
readonly property real surfaceTopRightRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius
readonly property real surfaceBottomLeftRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius
readonly property real surfaceBottomRightRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius
readonly property real horizontalConnectorExtent: Theme.isConnectedEffect && !isVertical ? Theme.connectedCornerRadius : 0
readonly property real verticalConnectorExtent: Theme.isConnectedEffect && isVertical ? Theme.connectedCornerRadius : 0
readonly property int hasApps: dockApps.implicitWidth > 0 || dockApps.implicitHeight > 0 readonly property int hasApps: dockApps.implicitWidth > 0 || dockApps.implicitHeight > 0
@@ -131,116 +113,13 @@ Variants {
return getBarHeight(leftBar); return getBarHeight(leftBar);
} }
readonly property real dockMargin: SettingsData.dockMargin readonly property real dockMargin: SettingsData.dockSpacing
readonly property bool effectiveBlurEnabled: Theme.connectedSurfaceBlurEnabled readonly property real positionSpacing: barSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin
readonly property real effectiveDockBottomGap: Theme.isConnectedEffect ? 0 : SettingsData.dockBottomGap
readonly property real effectiveDockMargin: Theme.isConnectedEffect ? 0 : SettingsData.dockMargin
readonly property real positionSpacing: barSpacing + effectiveDockBottomGap + effectiveDockMargin
readonly property real joinedEdgeMargin: Theme.isConnectedEffect ? 0 : (barSpacing + effectiveDockMargin + 1 + dock.borderThickness)
readonly property real _dpr: (dock.screen && dock.screen.devicePixelRatio) ? dock.screen.devicePixelRatio : 1 readonly property real _dpr: (dock.screen && dock.screen.devicePixelRatio) ? dock.screen.devicePixelRatio : 1
function px(v) { function px(v) {
return Math.round(v * _dpr) / _dpr; return Math.round(v * _dpr) / _dpr;
} }
function connectorWidth(spacing) {
return dock.isVertical ? (spacing + Theme.connectedCornerRadius) : Theme.connectedCornerRadius;
}
function connectorHeight(spacing) {
return dock.isVertical ? Theme.connectedCornerRadius : (spacing + Theme.connectedCornerRadius);
}
function connectorSeamX(baseX, bodyWidth, placement) {
if (!dock.isVertical)
return placement === "left" ? baseX : baseX + bodyWidth;
return SettingsData.dockPosition === SettingsData.Position.Left ? baseX : baseX + bodyWidth;
}
function connectorSeamY(baseY, bodyHeight, placement) {
if (SettingsData.dockPosition === SettingsData.Position.Top)
return baseY;
if (SettingsData.dockPosition === SettingsData.Position.Bottom)
return baseY + bodyHeight;
return placement === "left" ? baseY : baseY + bodyHeight;
}
function connectorX(baseX, bodyWidth, placement, spacing) {
const seamX = connectorSeamX(baseX, bodyWidth, placement);
const width = connectorWidth(spacing);
if (!dock.isVertical)
return placement === "left" ? seamX - width : seamX;
return SettingsData.dockPosition === SettingsData.Position.Left ? seamX : seamX - width;
}
function connectorY(baseY, bodyHeight, placement, spacing) {
const seamY = connectorSeamY(baseY, bodyHeight, placement);
const height = connectorHeight(spacing);
if (SettingsData.dockPosition === SettingsData.Position.Top)
return seamY;
if (SettingsData.dockPosition === SettingsData.Position.Bottom)
return seamY - height;
return placement === "left" ? seamY - height : seamY;
}
// ConnectedModeState sync
// Dock window origin in screen-relative coordinates (FrameWindow space).
function _dockWindowOriginX() {
if (!dock.isVertical)
return 0;
if (SettingsData.dockPosition === SettingsData.Position.Right)
return (dock.screen ? dock.screen.width : 0) - dock.width;
return 0;
}
function _dockWindowOriginY() {
if (dock.isVertical)
return 0;
if (SettingsData.dockPosition === SettingsData.Position.Bottom)
return (dock.screen ? dock.screen.height : 0) - dock.height;
return 0;
}
readonly property string _dockScreenName: dock.modelData ? dock.modelData.name : (dock.screen ? dock.screen.name : "")
function _syncDockChromeState() {
if (!dock._dockScreenName)
return;
if (!SettingsData.connectedFrameModeActive) {
ConnectedModeState.clearDockState(dock._dockScreenName);
return;
}
ConnectedModeState.setDockState(dock._dockScreenName, {
"reveal": dock.visible && (dock.reveal || slideXAnimation.running || slideYAnimation.running) && dock.hasApps,
"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,
"slideX": dockSlide.x,
"slideY": dockSlide.y
});
}
function _syncDockSlide() {
if (!dock._dockScreenName || !SettingsData.connectedFrameModeActive)
return;
ConnectedModeState.setDockSlide(dock._dockScreenName, dockSlide.x, dockSlide.y);
}
property bool _slideSyncPending: false
function _queueSlideSync() {
if (!SettingsData.connectedFrameModeActive)
return;
if (_slideSyncPending)
return;
_slideSyncPending = true;
Qt.callLater(dock._flushSlideSync);
}
function _flushSlideSync() {
_slideSyncPending = false;
dock._syncDockSlide();
}
property bool contextMenuOpen: (dockVariants.contextMenu && dockVariants.contextMenu.visible && dockVariants.contextMenu.screen === modelData) property bool contextMenuOpen: (dockVariants.contextMenu && dockVariants.contextMenu.visible && dockVariants.contextMenu.screen === modelData)
property bool revealSticky: false property bool revealSticky: false
@@ -251,7 +130,7 @@ Variants {
return false; return false;
const screenName = dock.modelData?.name ?? ""; const screenName = dock.modelData?.name ?? "";
const dockThickness = dock.connectedJoinInset + effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin; const dockThickness = effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin;
const screenWidth = dock.screen?.width ?? 0; const screenWidth = dock.screen?.width ?? 0;
const screenHeight = dock.screen?.height ?? 0; const screenHeight = dock.screen?.height ?? 0;
@@ -402,23 +281,6 @@ Variants {
} }
} }
Component.onCompleted: Qt.callLater(() => dock._syncDockChromeState())
Component.onDestruction: ConnectedModeState.clearDockState(dock._dockScreenName)
onRevealChanged: dock._syncDockChromeState()
onWidthChanged: dock._syncDockChromeState()
onHeightChanged: dock._syncDockChromeState()
onVisibleChanged: dock._syncDockChromeState()
onHasAppsChanged: dock._syncDockChromeState()
onConnectedBarSideChanged: dock._syncDockChromeState()
Connections {
target: SettingsData
function onConnectedFrameModeActiveChanged() {
dock._syncDockChromeState();
}
}
Connections { Connections {
target: SettingsData target: SettingsData
function onDockTransparencyChanged() { function onDockTransparencyChanged() {
@@ -440,13 +302,13 @@ Variants {
return -1; return -1;
if (barSpacing > 0) if (barSpacing > 0)
return -1; return -1;
return px(connectedJoinInset + effectiveBarHeight + SettingsData.dockSpacing + effectiveDockBottomGap + effectiveDockMargin); return px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin);
} }
property real animationHeadroom: Math.ceil(SettingsData.dockIconSize * 0.35) property real animationHeadroom: Math.ceil(SettingsData.dockIconSize * 0.35)
implicitWidth: isVertical ? (px(connectedJoinInset + effectiveBarHeight + SettingsData.dockSpacing + effectiveDockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0 implicitWidth: isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
implicitHeight: !isVertical ? (px(connectedJoinInset + effectiveBarHeight + SettingsData.dockSpacing + effectiveDockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0 implicitHeight: !isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
Item { Item {
id: maskItem id: maskItem
@@ -456,17 +318,17 @@ Variants {
x: { x: {
const baseX = dockCore.x + dockMouseArea.x; const baseX = dockCore.x + dockMouseArea.x;
if (isVertical && SettingsData.dockPosition === SettingsData.Position.Right) if (isVertical && SettingsData.dockPosition === SettingsData.Position.Right)
return baseX - (expanded ? animationHeadroom + borderThickness + dock.horizontalConnectorExtent : 0); return baseX - (expanded ? animationHeadroom + borderThickness : 0);
return baseX - (expanded ? borderThickness + dock.horizontalConnectorExtent : 0); return baseX - (expanded ? borderThickness : 0);
} }
y: { y: {
const baseY = dockCore.y + dockMouseArea.y; const baseY = dockCore.y + dockMouseArea.y;
if (!isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom) if (!isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom)
return baseY - (expanded ? animationHeadroom + borderThickness + dock.verticalConnectorExtent : 0); return baseY - (expanded ? animationHeadroom + borderThickness : 0);
return baseY - (expanded ? borderThickness + dock.verticalConnectorExtent : 0); return baseY - (expanded ? borderThickness : 0);
} }
width: dockMouseArea.width + (isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.horizontalConnectorExtent * 2 : 0) width: dockMouseArea.width + (isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 : 0)
height: dockMouseArea.height + (!isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.verticalConnectorExtent * 2 : 0) height: dockMouseArea.height + (!isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 : 0)
} }
mask: Region { mask: Region {
@@ -526,7 +388,7 @@ Variants {
const screenHeight = dock.screen ? dock.screen.height : 0; const screenHeight = dock.screen ? dock.screen.height : 0;
const gap = Theme.spacingS; const gap = Theme.spacingS;
const bgMargin = dock.joinedEdgeMargin + dock.connectedJoinInset; const bgMargin = barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness;
const btnW = dock.hoveredButton.width; const btnW = dock.hoveredButton.width;
const btnH = dock.hoveredButton.height; const btnH = dock.hoveredButton.height;
@@ -597,11 +459,11 @@ Variants {
// Keep the taller hit area regardless of the reveal state to prevent shrinking loop // Keep the taller hit area regardless of the reveal state to prevent shrinking loop
return Math.min(Math.max(dockBackground.height + 64, 200), maxDockHeight); return Math.min(Math.max(dockBackground.height + 64, 200), maxDockHeight);
} }
return dock.reveal ? px(dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin) : 1; return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1;
} }
width: { width: {
if (dock.isVertical) { if (dock.isVertical) {
return dock.reveal ? px(dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin) : 1; return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1;
} }
// Keep the wider hit area regardless of the reveal state to prevent shrinking loop // Keep the wider hit area regardless of the reveal state to prevent shrinking loop
return Math.min(dockBackground.width + 8 + dock.borderThickness, maxDockWidth); return Math.min(dockBackground.width + 8 + dock.borderThickness, maxDockWidth);
@@ -643,11 +505,7 @@ Variants {
return 0; return 0;
if (dock.reveal) if (dock.reveal)
return 0; return 0;
if (Theme.isConnectedEffect) { const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + 10;
const retractDist = dockBackground.width + SettingsData.dockSpacing + 10;
return SettingsData.dockPosition === SettingsData.Position.Right ? retractDist : -retractDist;
}
const hideDistance = dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin + 10;
if (SettingsData.dockPosition === SettingsData.Position.Right) { if (SettingsData.dockPosition === SettingsData.Position.Right) {
return hideDistance; return hideDistance;
} else { } else {
@@ -659,11 +517,7 @@ Variants {
return 0; return 0;
if (dock.reveal) if (dock.reveal)
return 0; return 0;
if (Theme.isConnectedEffect) { const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + 10;
const retractDist = dockBackground.height + SettingsData.dockSpacing + 10;
return SettingsData.dockPosition === SettingsData.Position.Bottom ? retractDist : -retractDist;
}
const hideDistance = dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin + 10;
if (SettingsData.dockPosition === SettingsData.Position.Bottom) { if (SettingsData.dockPosition === SettingsData.Position.Bottom) {
return hideDistance; return hideDistance;
} else { } else {
@@ -674,25 +528,18 @@ Variants {
Behavior on x { Behavior on x {
NumberAnimation { NumberAnimation {
id: slideXAnimation id: slideXAnimation
duration: Theme.isConnectedEffect ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.isConnectedEffect ? Easing.BezierSpline : Easing.OutCubic easing.type: Easing.OutCubic
easing.bezierCurve: Theme.isConnectedEffect ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : []
onRunningChanged: if (!running) dock._syncDockChromeState()
} }
} }
Behavior on y { Behavior on y {
NumberAnimation { NumberAnimation {
id: slideYAnimation id: slideYAnimation
duration: Theme.isConnectedEffect ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.isConnectedEffect ? Easing.BezierSpline : Easing.OutCubic easing.type: Easing.OutCubic
easing.bezierCurve: Theme.isConnectedEffect ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : []
onRunningChanged: if (!running) dock._syncDockChromeState()
} }
} }
onXChanged: dock._queueSlideSync()
onYChanged: dock._queueSlideSync()
} }
Item { Item {
@@ -706,60 +553,33 @@ Variants {
right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined
verticalCenter: dock.isVertical ? parent.verticalCenter : undefined verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
} }
anchors.topMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Top ? (dock.connectedJoinInset + dock.joinedEdgeMargin) : 0 anchors.topMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Top ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0
anchors.bottomMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? (dock.connectedJoinInset + dock.joinedEdgeMargin) : 0 anchors.bottomMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0
anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? (dock.connectedJoinInset + dock.joinedEdgeMargin) : 0 anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0
anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? (dock.connectedJoinInset + dock.joinedEdgeMargin) : 0 anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0
implicitWidth: dock.isVertical ? (dockApps.implicitHeight + SettingsData.dockSpacing * 2) : (dockApps.implicitWidth + SettingsData.dockSpacing * 2) implicitWidth: dock.isVertical ? (dockApps.implicitHeight + SettingsData.dockSpacing * 2) : (dockApps.implicitWidth + SettingsData.dockSpacing * 2)
implicitHeight: dock.isVertical ? (dockApps.implicitWidth + SettingsData.dockSpacing * 2) : (dockApps.implicitHeight + SettingsData.dockSpacing * 2) implicitHeight: dock.isVertical ? (dockApps.implicitWidth + SettingsData.dockSpacing * 2) : (dockApps.implicitHeight + SettingsData.dockSpacing * 2)
width: implicitWidth width: implicitWidth
height: implicitHeight height: implicitHeight
// Avoid an offscreen texture seam where the connected dock meets the frame. layer.enabled: true
layer.enabled: !Theme.isConnectedEffect
clip: false clip: false
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
visible: !SettingsData.connectedFrameModeActive && !(Theme.isConnectedEffect && dock.reveal) color: Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency)
color: dock.surfaceColor radius: Theme.cornerRadius
topLeftRadius: dock.surfaceTopLeftRadius
topRightRadius: dock.surfaceTopRightRadius
bottomLeftRadius: dock.surfaceBottomLeftRadius
bottomRightRadius: dock.surfaceBottomRightRadius
} }
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
visible: !SettingsData.connectedFrameModeActive && !(Theme.isConnectedEffect && dock.reveal)
color: "transparent" color: "transparent"
topLeftRadius: dock.surfaceTopLeftRadius radius: Theme.cornerRadius
topRightRadius: dock.surfaceTopRightRadius border.color: BlurService.borderColor
bottomLeftRadius: dock.surfaceBottomLeftRadius border.width: BlurService.borderWidth
bottomRightRadius: dock.surfaceBottomRightRadius
border.color: dock.surfaceBorderColor
border.width: dock.surfaceBorderWidth
z: 100 z: 100
} }
// Sync dockBackground geometry to ConnectedModeState
onXChanged: dock._syncDockChromeState()
onYChanged: dock._syncDockChromeState()
onWidthChanged: dock._syncDockChromeState()
onHeightChanged: dock._syncDockChromeState()
}
ConnectedShape {
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
} }
Shape { Shape {
@@ -768,12 +588,12 @@ Variants {
y: dockBackground.y - borderThickness y: dockBackground.y - borderThickness
width: dockBackground.width + borderThickness * 2 width: dockBackground.width + borderThickness * 2
height: dockBackground.height + borderThickness * 2 height: dockBackground.height + borderThickness * 2
visible: SettingsData.dockBorderEnabled && dock.hasApps && !Theme.isConnectedEffect visible: SettingsData.dockBorderEnabled && dock.hasApps
preferredRendererType: Shape.CurveRenderer preferredRendererType: Shape.CurveRenderer
readonly property real borderThickness: Math.max(1, dock.borderThickness) readonly property real borderThickness: Math.max(1, dock.borderThickness)
readonly property real i: borderThickness / 2 readonly property real i: borderThickness / 2
readonly property real cr: dock.surfaceRadius readonly property real cr: Theme.cornerRadius
readonly property real w: dockBackground.width readonly property real w: dockBackground.width
readonly property real h: dockBackground.height readonly property real h: dockBackground.height
+17 -12
View File
@@ -14,22 +14,27 @@ Item {
required property real cutoutLeftInset required property real cutoutLeftInset
required property real cutoutRightInset required property real cutoutRightInset
required property real cutoutRadius required property real cutoutRadius
property color borderColor: Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity)
Rectangle { Rectangle {
id: borderRect id: borderRect
anchors.fill: parent anchors.fill: parent
// Bake frameOpacity into the color alpha rather than using the `opacity` property // Bake frameOpacity into the color alpha rather than using the `opacity` property.
color: root.borderColor // Qt Quick can skip layer.effect processing on items with opacity < 1 as an
// optimization, causing the MultiEffect inverted mask to stop working and the
// Rectangle to render as a plain square at low opacity values.
color: Qt.rgba(SettingsData.effectiveFrameColor.r,
SettingsData.effectiveFrameColor.g,
SettingsData.effectiveFrameColor.b,
SettingsData.frameOpacity)
layer.enabled: true layer.enabled: true
layer.effect: MultiEffect { layer.effect: MultiEffect {
maskSource: cutoutMask maskSource: cutoutMask
maskEnabled: true maskEnabled: true
maskInverted: true maskInverted: true
maskThresholdMin: 0.5 maskThresholdMin: 0.5
maskSpreadAtMin: 1 maskSpreadAtMin: 1
} }
} }
@@ -42,11 +47,11 @@ Item {
Rectangle { Rectangle {
anchors { anchors {
fill: parent fill: parent
topMargin: root.cutoutTopInset topMargin: root.cutoutTopInset
bottomMargin: root.cutoutBottomInset bottomMargin: root.cutoutBottomInset
leftMargin: root.cutoutLeftInset leftMargin: root.cutoutLeftInset
rightMargin: root.cutoutRightInset rightMargin: root.cutoutRightInset
} }
radius: root.cutoutRadius radius: root.cutoutRadius
} }
+53 -835
View File
@@ -1,12 +1,10 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets
PanelWindow { PanelWindow {
id: win id: win
@@ -28,6 +26,8 @@ PanelWindow {
} }
color: "transparent" color: "transparent"
// No input pass everything through to apps and bar
mask: Region {} mask: Region {}
readonly property var barEdges: { readonly property var barEdges: {
@@ -36,58 +36,20 @@ PanelWindow {
} }
readonly property real _dpr: CompositorService.getScreenScale(win.screen) readonly property real _dpr: CompositorService.getScreenScale(win.screen)
readonly property bool _frameActive: SettingsData.frameEnabled && SettingsData.isScreenInPreferences(win.screen, SettingsData.frameScreenPreferences) readonly property bool _frameActive: SettingsData.frameEnabled
readonly property int _windowRegionWidth: win._regionInt(win.width) && SettingsData.isScreenInPreferences(win.screen, SettingsData.frameScreenPreferences)
readonly property int _windowRegionWidth: win._regionInt(win.width)
readonly property int _windowRegionHeight: win._regionInt(win.height) readonly property int _windowRegionHeight: win._regionInt(win.height)
readonly property string _screenName: win.screen ? win.screen.name : ""
readonly property var _dockState: ConnectedModeState.dockStates[win._screenName] || ConnectedModeState.emptyDockState
readonly property var _dockSlide: ConnectedModeState.dockSlides[win._screenName] || ({
"x": 0,
"y": 0
})
readonly property var _notifState: ConnectedModeState.notificationStates[win._screenName] || ConnectedModeState.emptyNotificationState
// Connected chrome convenience properties
readonly property bool _connectedActive: win._frameActive && SettingsData.connectedFrameModeActive
readonly property string _barSide: {
const edges = win.barEdges;
if (edges.includes("top"))
return "top";
if (edges.includes("bottom"))
return "bottom";
if (edges.includes("left"))
return "left";
return "right";
}
readonly property real _ccr: Theme.connectedCornerRadius
readonly property real _effectivePopoutCcr: {
const extent = win._popoutArcExtent();
const isHoriz = ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom";
const crossSize = isHoriz ? _popoutBodyBlurAnchor.width : _popoutBodyBlurAnchor.height;
return Math.max(0, Math.min(win._ccr, extent, crossSize / 2));
}
readonly property real _effectiveNotifCcr: {
const isHoriz = win._notifState.barSide === "top" || win._notifState.barSide === "bottom";
const crossSize = isHoriz ? _notifBodyBlurAnchor.width : _notifBodyBlurAnchor.height;
const extent = isHoriz ? _notifBodyBlurAnchor.height : _notifBodyBlurAnchor.width;
return Theme.snap(Math.max(0, Math.min(win._ccr, win._surfaceRadius, extent, crossSize / 2)), win._dpr);
}
readonly property color _surfaceColor: Theme.connectedSurfaceColor
readonly property real _surfaceOpacity: _surfaceColor.a
readonly property color _opaqueSurfaceColor: Qt.rgba(_surfaceColor.r, _surfaceColor.g, _surfaceColor.b, 1)
readonly property real _surfaceRadius: Theme.connectedSurfaceRadius
readonly property real _seamOverlap: Theme.hairline(win._dpr)
readonly property bool _disableLayer: Quickshell.env("DMS_DISABLE_LAYER") === "true" || Quickshell.env("DMS_DISABLE_LAYER") === "1"
function _regionInt(value) { function _regionInt(value) {
return Math.max(0, Math.round(Theme.px(value, win._dpr))); return Math.max(0, Math.round(Theme.px(value, win._dpr)));
} }
readonly property int cutoutTopInset: win._regionInt(barEdges.includes("top") ? SettingsData.frameBarSize : SettingsData.frameThickness) readonly property int cutoutTopInset: win._regionInt(barEdges.includes("top") ? SettingsData.frameBarSize : SettingsData.frameThickness)
readonly property int cutoutBottomInset: win._regionInt(barEdges.includes("bottom") ? SettingsData.frameBarSize : SettingsData.frameThickness) readonly property int cutoutBottomInset: win._regionInt(barEdges.includes("bottom") ? SettingsData.frameBarSize : SettingsData.frameThickness)
readonly property int cutoutLeftInset: win._regionInt(barEdges.includes("left") ? SettingsData.frameBarSize : SettingsData.frameThickness) readonly property int cutoutLeftInset: win._regionInt(barEdges.includes("left") ? SettingsData.frameBarSize : SettingsData.frameThickness)
readonly property int cutoutRightInset: win._regionInt(barEdges.includes("right") ? SettingsData.frameBarSize : SettingsData.frameThickness) readonly property int cutoutRightInset: win._regionInt(barEdges.includes("right") ? SettingsData.frameBarSize : SettingsData.frameThickness)
readonly property int cutoutWidth: Math.max(0, win._windowRegionWidth - win.cutoutLeftInset - win.cutoutRightInset) readonly property int cutoutWidth: Math.max(0, win._windowRegionWidth - win.cutoutLeftInset - win.cutoutRightInset)
readonly property int cutoutHeight: Math.max(0, win._windowRegionHeight - win.cutoutTopInset - win.cutoutBottomInset) readonly property int cutoutHeight: Math.max(0, win._windowRegionHeight - win.cutoutTopInset - win.cutoutBottomInset)
readonly property int cutoutRadius: { readonly property int cutoutRadius: {
const requested = win._regionInt(SettingsData.frameRounding); const requested = win._regionInt(SettingsData.frameRounding);
@@ -95,6 +57,7 @@ PanelWindow {
return Math.max(0, Math.min(requested, maxRadius)); return Math.max(0, Math.min(requested, maxRadius));
} }
// Slightly expand the subtractive blur cutout at very low opacity levels
readonly property int _blurCutoutCompensation: SettingsData.frameOpacity <= 0.2 ? 1 : 0 readonly property int _blurCutoutCompensation: SettingsData.frameOpacity <= 0.2 ? 1 : 0
readonly property int _blurCutoutLeft: Math.max(0, win.cutoutLeftInset - win._blurCutoutCompensation) readonly property int _blurCutoutLeft: Math.max(0, win.cutoutLeftInset - win._blurCutoutCompensation)
readonly property int _blurCutoutTop: Math.max(0, win.cutoutTopInset - win._blurCutoutCompensation) readonly property int _blurCutoutTop: Math.max(0, win.cutoutTopInset - win._blurCutoutCompensation)
@@ -106,7 +69,7 @@ PanelWindow {
return Math.max(0, Math.min(requested, maxRadius)); return Math.max(0, Math.min(requested, maxRadius));
} }
// Invisible items providing scene coordinates for blur Region anchors // Must stay visible so Region.item can resolve scene coordinates.
Item { Item {
id: _blurCutout id: _blurCutout
x: win._blurCutoutLeft x: win._blurCutoutLeft
@@ -115,610 +78,48 @@ PanelWindow {
height: Math.max(0, win._blurCutoutBottom - win._blurCutoutTop) height: Math.max(0, win._blurCutoutBottom - win._blurCutoutTop)
} }
Item { property var _frameBlurRegion: null
id: _popoutBodyBlurAnchor
visible: false
readonly property bool _active: ConnectedModeState.popoutVisible && ConnectedModeState.popoutScreen === win._screenName
readonly property real _dyClamp: (ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom") ? Math.max(-ConnectedModeState.popoutBodyH, Math.min(ConnectedModeState.popoutAnimY * 1.02, ConnectedModeState.popoutBodyH)) : 0
readonly property real _dxClamp: (ConnectedModeState.popoutBarSide === "left" || ConnectedModeState.popoutBarSide === "right") ? Math.max(-ConnectedModeState.popoutBodyW, Math.min(ConnectedModeState.popoutAnimX * 1.02, ConnectedModeState.popoutBodyW)) : 0
x: _active ? ConnectedModeState.popoutBodyX + (ConnectedModeState.popoutBarSide === "right" ? _dxClamp : 0) : 0
y: _active ? ConnectedModeState.popoutBodyY + (ConnectedModeState.popoutBarSide === "bottom" ? _dyClamp : 0) : 0
width: _active ? Math.max(0, ConnectedModeState.popoutBodyW - Math.abs(_dxClamp)) : 0
height: _active ? Math.max(0, ConnectedModeState.popoutBodyH - Math.abs(_dyClamp)) : 0
}
Item {
id: _dockBodyBlurAnchor
visible: false
readonly property bool _active: win._dockState.reveal && win._dockState.bodyW > 0 && win._dockState.bodyH > 0
x: _active ? win._dockState.bodyX + (win._dockSlide.x || 0) : 0
y: _active ? win._dockState.bodyY + (win._dockSlide.y || 0) : 0
width: _active ? win._dockState.bodyW : 0
height: _active ? win._dockState.bodyH : 0
}
Item {
id: _popoutBodyBlurCap
opacity: 0
readonly property string _side: ConnectedModeState.popoutBarSide
readonly property real _capThickness: win._popoutBlurCapThickness()
readonly property bool _active: _popoutBodyBlurAnchor._active && _capThickness > 0 && _popoutBodyBlurAnchor.width > 0 && _popoutBodyBlurAnchor.height > 0
readonly property real _capWidth: (_side === "left" || _side === "right") ? Math.min(_capThickness, _popoutBodyBlurAnchor.width) : _popoutBodyBlurAnchor.width
readonly property real _capHeight: (_side === "top" || _side === "bottom") ? Math.min(_capThickness, _popoutBodyBlurAnchor.height) : _popoutBodyBlurAnchor.height
x: !_active ? 0 : (_side === "right" ? _popoutBodyBlurAnchor.x + _popoutBodyBlurAnchor.width - _capWidth : _popoutBodyBlurAnchor.x)
y: !_active ? 0 : (_side === "bottom" ? _popoutBodyBlurAnchor.y + _popoutBodyBlurAnchor.height - _capHeight : _popoutBodyBlurAnchor.y)
width: _active ? _capWidth : 0
height: _active ? _capHeight : 0
}
Item {
id: _dockBodyBlurCap
opacity: 0
readonly property string _side: win._dockState.barSide
readonly property bool _active: _dockBodyBlurAnchor._active && _dockBodyBlurAnchor.width > 0 && _dockBodyBlurAnchor.height > 0
readonly property real _capWidth: (_side === "left" || _side === "right") ? Math.min(win._dockConnectorRadius(), _dockBodyBlurAnchor.width) : _dockBodyBlurAnchor.width
readonly property real _capHeight: (_side === "top" || _side === "bottom") ? Math.min(win._dockConnectorRadius(), _dockBodyBlurAnchor.height) : _dockBodyBlurAnchor.height
x: !_active ? 0 : (_side === "right" ? _dockBodyBlurAnchor.x + _dockBodyBlurAnchor.width - _capWidth : _dockBodyBlurAnchor.x)
y: !_active ? 0 : (_side === "bottom" ? _dockBodyBlurAnchor.y + _dockBodyBlurAnchor.height - _capHeight : _dockBodyBlurAnchor.y)
width: _active ? _capWidth : 0
height: _active ? _capHeight : 0
}
Item {
id: _popoutLeftConnectorBlurAnchor
opacity: 0
readonly property bool _active: _popoutBodyBlurAnchor._active && win._effectivePopoutCcr > 0
readonly property real _w: win._popoutConnectorWidth(0)
readonly property real _h: win._popoutConnectorHeight(0)
x: _active ? Theme.snap(win._popoutConnectorX(_popoutBodyBlurAnchor.x, _popoutBodyBlurAnchor.width, "left", 0), win._dpr) : 0
y: _active ? Theme.snap(win._popoutConnectorY(_popoutBodyBlurAnchor.y, _popoutBodyBlurAnchor.height, "left", 0), win._dpr) : 0
width: _active ? _w : 0
height: _active ? _h : 0
}
Item {
id: _popoutRightConnectorBlurAnchor
opacity: 0
readonly property bool _active: _popoutBodyBlurAnchor._active && win._effectivePopoutCcr > 0
readonly property real _w: win._popoutConnectorWidth(0)
readonly property real _h: win._popoutConnectorHeight(0)
x: _active ? Theme.snap(win._popoutConnectorX(_popoutBodyBlurAnchor.x, _popoutBodyBlurAnchor.width, "right", 0), win._dpr) : 0
y: _active ? Theme.snap(win._popoutConnectorY(_popoutBodyBlurAnchor.y, _popoutBodyBlurAnchor.height, "right", 0), win._dpr) : 0
width: _active ? _w : 0
height: _active ? _h : 0
}
Item {
id: _popoutLeftConnectorCutout
opacity: 0
readonly property bool _active: _popoutLeftConnectorBlurAnchor.width > 0 && _popoutLeftConnectorBlurAnchor.height > 0
readonly property string _arcCorner: win._connectorArcCorner(ConnectedModeState.popoutBarSide, "left")
x: _active ? win._connectorCutoutX(_popoutLeftConnectorBlurAnchor.x, _popoutLeftConnectorBlurAnchor.width, _arcCorner, win._effectivePopoutCcr) : 0
y: _active ? win._connectorCutoutY(_popoutLeftConnectorBlurAnchor.y, _popoutLeftConnectorBlurAnchor.height, _arcCorner, win._effectivePopoutCcr) : 0
width: _active ? win._effectivePopoutCcr * 2 : 0
height: _active ? win._effectivePopoutCcr * 2 : 0
}
Item {
id: _popoutRightConnectorCutout
opacity: 0
readonly property bool _active: _popoutRightConnectorBlurAnchor.width > 0 && _popoutRightConnectorBlurAnchor.height > 0
readonly property string _arcCorner: win._connectorArcCorner(ConnectedModeState.popoutBarSide, "right")
x: _active ? win._connectorCutoutX(_popoutRightConnectorBlurAnchor.x, _popoutRightConnectorBlurAnchor.width, _arcCorner, win._effectivePopoutCcr) : 0
y: _active ? win._connectorCutoutY(_popoutRightConnectorBlurAnchor.y, _popoutRightConnectorBlurAnchor.height, _arcCorner, win._effectivePopoutCcr) : 0
width: _active ? win._effectivePopoutCcr * 2 : 0
height: _active ? win._effectivePopoutCcr * 2 : 0
}
Item {
id: _dockLeftConnectorBlurAnchor
opacity: 0
readonly property bool _active: _dockBodyBlurAnchor._active && win._dockConnectorRadius() > 0
readonly property real _w: win._dockConnectorWidth(0)
readonly property real _h: win._dockConnectorHeight(0)
x: _active ? Theme.snap(win._dockConnectorX(_dockBodyBlurAnchor.x, _dockBodyBlurAnchor.width, "left", 0), win._dpr) : 0
y: _active ? Theme.snap(win._dockConnectorY(_dockBodyBlurAnchor.y, _dockBodyBlurAnchor.height, "left", 0), win._dpr) : 0
width: _active ? _w : 0
height: _active ? _h : 0
}
Item {
id: _dockRightConnectorBlurAnchor
opacity: 0
readonly property bool _active: _dockBodyBlurAnchor._active && win._dockConnectorRadius() > 0
readonly property real _w: win._dockConnectorWidth(0)
readonly property real _h: win._dockConnectorHeight(0)
x: _active ? Theme.snap(win._dockConnectorX(_dockBodyBlurAnchor.x, _dockBodyBlurAnchor.width, "right", 0), win._dpr) : 0
y: _active ? Theme.snap(win._dockConnectorY(_dockBodyBlurAnchor.y, _dockBodyBlurAnchor.height, "right", 0), win._dpr) : 0
width: _active ? _w : 0
height: _active ? _h : 0
}
Item {
id: _dockLeftConnectorCutout
opacity: 0
readonly property bool _active: _dockLeftConnectorBlurAnchor.width > 0 && _dockLeftConnectorBlurAnchor.height > 0
readonly property string _arcCorner: win._connectorArcCorner(win._dockState.barSide, "left")
x: _active ? win._connectorCutoutX(_dockLeftConnectorBlurAnchor.x, _dockLeftConnectorBlurAnchor.width, _arcCorner, win._dockConnectorRadius()) : 0
y: _active ? win._connectorCutoutY(_dockLeftConnectorBlurAnchor.y, _dockLeftConnectorBlurAnchor.height, _arcCorner, win._dockConnectorRadius()) : 0
width: _active ? win._dockConnectorRadius() * 2 : 0
height: _active ? win._dockConnectorRadius() * 2 : 0
}
Item {
id: _dockRightConnectorCutout
opacity: 0
readonly property bool _active: _dockRightConnectorBlurAnchor.width > 0 && _dockRightConnectorBlurAnchor.height > 0
readonly property string _arcCorner: win._connectorArcCorner(win._dockState.barSide, "right")
x: _active ? win._connectorCutoutX(_dockRightConnectorBlurAnchor.x, _dockRightConnectorBlurAnchor.width, _arcCorner, win._dockConnectorRadius()) : 0
y: _active ? win._connectorCutoutY(_dockRightConnectorBlurAnchor.y, _dockRightConnectorBlurAnchor.height, _arcCorner, win._dockConnectorRadius()) : 0
width: _active ? win._dockConnectorRadius() * 2 : 0
height: _active ? win._dockConnectorRadius() * 2 : 0
}
Item {
id: _notifBodyBlurAnchor
visible: false
readonly property bool _active: win._frameActive && win._notifState.visible && win._notifState.bodyW > 0 && win._notifState.bodyH > 0
x: _active ? Theme.snap(win._notifState.bodyX, win._dpr) : 0
y: _active ? Theme.snap(win._notifState.bodyY, win._dpr) : 0
width: _active ? Theme.snap(win._notifState.bodyW, win._dpr) : 0
height: _active ? Theme.snap(win._notifState.bodyH, win._dpr) : 0
}
Item {
id: _notifBodyBlurCap
opacity: 0
readonly property string _side: win._notifState.barSide
readonly property bool _active: _notifBodyBlurAnchor._active && _notifBodyBlurAnchor.width > 0 && _notifBodyBlurAnchor.height > 0 && win._notifConnectorRadius() > 0
readonly property real _capWidth: (_side === "left" || _side === "right") ? Math.min(win._notifConnectorRadius(), _notifBodyBlurAnchor.width) : _notifBodyBlurAnchor.width
readonly property real _capHeight: (_side === "top" || _side === "bottom") ? Math.min(win._notifConnectorRadius(), _notifBodyBlurAnchor.height) : _notifBodyBlurAnchor.height
x: !_active ? 0 : (_side === "right" ? _notifBodyBlurAnchor.x + _notifBodyBlurAnchor.width - _capWidth : _notifBodyBlurAnchor.x)
y: !_active ? 0 : (_side === "bottom" ? _notifBodyBlurAnchor.y + _notifBodyBlurAnchor.height - _capHeight : _notifBodyBlurAnchor.y)
width: _active ? _capWidth : 0
height: _active ? _capHeight : 0
}
Item {
id: _notifLeftConnectorBlurAnchor
opacity: 0
readonly property bool _active: _notifBodyBlurAnchor._active && win._notifConnectorRadius() > 0
readonly property real _w: win._notifConnectorWidth(0)
readonly property real _h: win._notifConnectorHeight(0)
x: _active ? Theme.snap(win._notifConnectorX(_notifBodyBlurAnchor.x, _notifBodyBlurAnchor.width, "left", 0), win._dpr) : 0
y: _active ? Theme.snap(win._notifConnectorY(_notifBodyBlurAnchor.y, _notifBodyBlurAnchor.height, "left", 0), win._dpr) : 0
width: _active ? _w : 0
height: _active ? _h : 0
}
Item {
id: _notifRightConnectorBlurAnchor
opacity: 0
readonly property bool _active: _notifBodyBlurAnchor._active && win._notifConnectorRadius() > 0
readonly property real _w: win._notifConnectorWidth(0)
readonly property real _h: win._notifConnectorHeight(0)
x: _active ? Theme.snap(win._notifConnectorX(_notifBodyBlurAnchor.x, _notifBodyBlurAnchor.width, "right", 0), win._dpr) : 0
y: _active ? Theme.snap(win._notifConnectorY(_notifBodyBlurAnchor.y, _notifBodyBlurAnchor.height, "right", 0), win._dpr) : 0
width: _active ? _w : 0
height: _active ? _h : 0
}
Item {
id: _notifLeftConnectorCutout
opacity: 0
readonly property bool _active: _notifLeftConnectorBlurAnchor.width > 0 && _notifLeftConnectorBlurAnchor.height > 0
readonly property string _arcCorner: win._connectorArcCorner(win._notifState.barSide, "left")
x: _active ? win._connectorCutoutX(_notifLeftConnectorBlurAnchor.x, _notifLeftConnectorBlurAnchor.width, _arcCorner, win._notifConnectorRadius()) : 0
y: _active ? win._connectorCutoutY(_notifLeftConnectorBlurAnchor.y, _notifLeftConnectorBlurAnchor.height, _arcCorner, win._notifConnectorRadius()) : 0
width: _active ? win._notifConnectorRadius() * 2 : 0
height: _active ? win._notifConnectorRadius() * 2 : 0
}
Item {
id: _notifRightConnectorCutout
opacity: 0
readonly property bool _active: _notifRightConnectorBlurAnchor.width > 0 && _notifRightConnectorBlurAnchor.height > 0
readonly property string _arcCorner: win._connectorArcCorner(win._notifState.barSide, "right")
x: _active ? win._connectorCutoutX(_notifRightConnectorBlurAnchor.x, _notifRightConnectorBlurAnchor.width, _arcCorner, win._notifConnectorRadius()) : 0
y: _active ? win._connectorCutoutY(_notifRightConnectorBlurAnchor.y, _notifRightConnectorBlurAnchor.height, _arcCorner, win._notifConnectorRadius()) : 0
width: _active ? win._notifConnectorRadius() * 2 : 0
height: _active ? win._notifConnectorRadius() * 2 : 0
}
Region {
id: _staticBlurRegion
x: 0
y: 0
width: win._windowRegionWidth
height: win._windowRegionHeight
// Frame cutout (always active when frame is on)
Region {
item: _blurCutout
intersection: Intersection.Subtract
radius: win._blurCutoutRadius
}
// Connected popout blur regions
Region {
item: _popoutBodyBlurAnchor
radius: win._surfaceRadius
}
Region {
item: _popoutBodyBlurCap
}
Region {
item: _popoutLeftConnectorBlurAnchor
Region {
item: _popoutLeftConnectorCutout
intersection: Intersection.Subtract
radius: win._effectivePopoutCcr
}
}
Region {
item: _popoutRightConnectorBlurAnchor
Region {
item: _popoutRightConnectorCutout
intersection: Intersection.Subtract
radius: win._effectivePopoutCcr
}
}
// Connected dock blur regions
Region {
item: _dockBodyBlurAnchor
radius: win._dockBodyBlurRadius()
}
Region {
item: _dockBodyBlurCap
}
Region {
item: _dockLeftConnectorBlurAnchor
Region {
item: _dockLeftConnectorCutout
intersection: Intersection.Subtract
radius: win._dockConnectorRadius()
}
}
Region {
item: _dockRightConnectorBlurAnchor
Region {
item: _dockRightConnectorCutout
intersection: Intersection.Subtract
radius: win._dockConnectorRadius()
}
}
Region {
item: _notifBodyBlurAnchor
radius: win._surfaceRadius
}
Region {
item: _notifBodyBlurCap
}
Region {
item: _notifLeftConnectorBlurAnchor
Region {
item: _notifLeftConnectorCutout
intersection: Intersection.Subtract
radius: win._notifConnectorRadius()
}
}
Region {
item: _notifRightConnectorBlurAnchor
Region {
item: _notifRightConnectorCutout
intersection: Intersection.Subtract
radius: win._notifConnectorRadius()
}
}
}
// Connector position helpers
function _dockBodyBlurRadius() {
return _dockBodyBlurAnchor._active ? Math.max(0, Math.min(win._surfaceRadius, _dockBodyBlurAnchor.width / 2, _dockBodyBlurAnchor.height / 2)) : win._surfaceRadius;
}
function _dockConnectorRadius() {
if (!_dockBodyBlurAnchor._active)
return win._ccr;
const dockSide = win._dockState.barSide;
const thickness = (dockSide === "left" || dockSide === "right") ? _dockBodyBlurAnchor.width : _dockBodyBlurAnchor.height;
const bodyRadius = win._dockBodyBlurRadius();
const maxConnectorRadius = Math.max(0, thickness - bodyRadius - win._seamOverlap);
return Math.max(0, Math.min(win._ccr, bodyRadius, maxConnectorRadius));
}
function _dockConnectorWidth(spacing) {
const isVert = win._dockState.barSide === "left" || win._dockState.barSide === "right";
const radius = win._dockConnectorRadius();
return isVert ? (spacing + radius) : radius;
}
function _dockConnectorHeight(spacing) {
const isVert = win._dockState.barSide === "left" || win._dockState.barSide === "right";
const radius = win._dockConnectorRadius();
return isVert ? radius : (spacing + radius);
}
function _dockConnectorX(baseX, bodyWidth, placement, spacing) {
const dockSide = win._dockState.barSide;
const isVert = dockSide === "left" || dockSide === "right";
const seamX = !isVert ? (placement === "left" ? baseX : baseX + bodyWidth) : (dockSide === "left" ? baseX : baseX + bodyWidth);
const w = _dockConnectorWidth(spacing);
if (!isVert)
return placement === "left" ? seamX - w : seamX;
return dockSide === "left" ? seamX : seamX - w;
}
function _dockConnectorY(baseY, bodyHeight, placement, spacing) {
const dockSide = win._dockState.barSide;
const seamY = dockSide === "top" ? baseY : dockSide === "bottom" ? baseY + bodyHeight : (placement === "left" ? baseY : baseY + bodyHeight);
const h = _dockConnectorHeight(spacing);
if (dockSide === "top")
return seamY;
if (dockSide === "bottom")
return seamY - h;
return placement === "left" ? seamY - h : seamY;
}
function _notifConnectorRadius() {
return win._effectiveNotifCcr;
}
function _notifConnectorWidth(spacing) {
const isVert = win._notifState.barSide === "left" || win._notifState.barSide === "right";
const radius = win._notifConnectorRadius();
return isVert ? (spacing + radius) : radius;
}
function _notifConnectorHeight(spacing) {
const isVert = win._notifState.barSide === "left" || win._notifState.barSide === "right";
const radius = win._notifConnectorRadius();
return isVert ? radius : (spacing + radius);
}
function _notifConnectorX(baseX, bodyWidth, placement, spacing) {
const notifSide = win._notifState.barSide;
const isVert = notifSide === "left" || notifSide === "right";
const seamX = !isVert ? (placement === "left" ? baseX : baseX + bodyWidth) : (notifSide === "left" ? baseX : baseX + bodyWidth);
const w = _notifConnectorWidth(spacing);
if (!isVert)
return placement === "left" ? seamX - w : seamX;
return notifSide === "left" ? seamX : seamX - w;
}
function _notifConnectorY(baseY, bodyHeight, placement, spacing) {
const notifSide = win._notifState.barSide;
const seamY = notifSide === "top" ? baseY : notifSide === "bottom" ? baseY + bodyHeight : (placement === "left" ? baseY : baseY + bodyHeight);
const h = _notifConnectorHeight(spacing);
if (notifSide === "top")
return seamY;
if (notifSide === "bottom")
return seamY - h;
return placement === "left" ? seamY - h : seamY;
}
function _popoutConnectorWidth(spacing) {
const isVert = ConnectedModeState.popoutBarSide === "left" || ConnectedModeState.popoutBarSide === "right";
const radius = win._effectivePopoutCcr;
return isVert ? (spacing + radius) : radius;
}
function _popoutConnectorHeight(spacing) {
const isVert = ConnectedModeState.popoutBarSide === "left" || ConnectedModeState.popoutBarSide === "right";
const radius = win._effectivePopoutCcr;
return isVert ? radius : (spacing + radius);
}
function _popoutConnectorX(baseX, bodyWidth, placement, spacing) {
const popoutSide = ConnectedModeState.popoutBarSide;
const isVert = popoutSide === "left" || popoutSide === "right";
const seamX = !isVert ? (placement === "left" ? baseX : baseX + bodyWidth) : (popoutSide === "left" ? baseX : baseX + bodyWidth);
const w = _popoutConnectorWidth(spacing);
if (!isVert)
return placement === "left" ? seamX - w : seamX;
return popoutSide === "left" ? seamX : seamX - w;
}
function _popoutConnectorY(baseY, bodyHeight, placement, spacing) {
const popoutSide = ConnectedModeState.popoutBarSide;
const seamY = popoutSide === "top" ? baseY : popoutSide === "bottom" ? baseY + bodyHeight : (placement === "left" ? baseY : baseY + bodyHeight);
const h = _popoutConnectorHeight(spacing);
if (popoutSide === "top")
return seamY;
if (popoutSide === "bottom")
return seamY - h;
return placement === "left" ? seamY - h : seamY;
}
function _popoutFillOverlapX() {
return (ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom") ? win._seamOverlap : 0;
}
function _popoutFillOverlapY() {
return (ConnectedModeState.popoutBarSide === "left" || ConnectedModeState.popoutBarSide === "right") ? win._seamOverlap : 0;
}
function _dockFillOverlapX() {
return (win._dockState.barSide === "top" || win._dockState.barSide === "bottom") ? win._seamOverlap : 0;
}
function _dockFillOverlapY() {
return (win._dockState.barSide === "left" || win._dockState.barSide === "right") ? win._seamOverlap : 0;
}
function _popoutArcExtent() {
return (ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom") ? _popoutBodyBlurAnchor.height : _popoutBodyBlurAnchor.width;
}
function _popoutArcVisible() {
if (!_popoutBodyBlurAnchor._active || _popoutBodyBlurAnchor.width <= 0 || _popoutBodyBlurAnchor.height <= 0)
return false;
return win._popoutArcExtent() >= win._ccr * (1 + win._ccr * 0.02);
}
function _popoutBlurCapThickness() {
const extent = win._popoutArcExtent();
return Math.max(0, Math.min(win._effectivePopoutCcr, extent - win._surfaceRadius));
}
function _popoutChromeX() {
const barSide = ConnectedModeState.popoutBarSide;
return ConnectedModeState.popoutBodyX - ((barSide === "top" || barSide === "bottom") ? win._effectivePopoutCcr : 0);
}
function _popoutChromeY() {
const barSide = ConnectedModeState.popoutBarSide;
return ConnectedModeState.popoutBodyY - ((barSide === "left" || barSide === "right") ? win._effectivePopoutCcr : 0);
}
function _popoutChromeWidth() {
const barSide = ConnectedModeState.popoutBarSide;
return ConnectedModeState.popoutBodyW + ((barSide === "top" || barSide === "bottom") ? win._effectivePopoutCcr * 2 : 0);
}
function _popoutChromeHeight() {
const barSide = ConnectedModeState.popoutBarSide;
return ConnectedModeState.popoutBodyH + ((barSide === "left" || barSide === "right") ? win._effectivePopoutCcr * 2 : 0);
}
function _popoutClipX() {
return _popoutBodyBlurAnchor.x - win._popoutChromeX() - win._popoutFillOverlapX();
}
function _popoutClipY() {
return _popoutBodyBlurAnchor.y - win._popoutChromeY() - win._popoutFillOverlapY();
}
function _popoutClipWidth() {
return _popoutBodyBlurAnchor.width + win._popoutFillOverlapX() * 2;
}
function _popoutClipHeight() {
return _popoutBodyBlurAnchor.height + win._popoutFillOverlapY() * 2;
}
function _popoutBodyXInClip() {
return (ConnectedModeState.popoutBarSide === "left" ? _popoutBodyBlurAnchor._dxClamp : 0) - win._popoutFillOverlapX();
}
function _popoutBodyYInClip() {
return (ConnectedModeState.popoutBarSide === "top" ? _popoutBodyBlurAnchor._dyClamp : 0) - win._popoutFillOverlapY();
}
function _popoutBodyFullWidth() {
return ConnectedModeState.popoutBodyW + win._popoutFillOverlapX() * 2;
}
function _popoutBodyFullHeight() {
return ConnectedModeState.popoutBodyH + win._popoutFillOverlapY() * 2;
}
function _dockChromeX() {
const dockSide = win._dockState.barSide;
return _dockBodyBlurAnchor.x - ((dockSide === "top" || dockSide === "bottom") ? win._dockConnectorRadius() : 0);
}
function _dockChromeY() {
const dockSide = win._dockState.barSide;
return _dockBodyBlurAnchor.y - ((dockSide === "left" || dockSide === "right") ? win._dockConnectorRadius() : 0);
}
function _dockChromeWidth() {
const dockSide = win._dockState.barSide;
return _dockBodyBlurAnchor.width + ((dockSide === "top" || dockSide === "bottom") ? win._dockConnectorRadius() * 2 : 0);
}
function _dockChromeHeight() {
const dockSide = win._dockState.barSide;
return _dockBodyBlurAnchor.height + ((dockSide === "left" || dockSide === "right") ? win._dockConnectorRadius() * 2 : 0);
}
function _dockBodyXInChrome() {
return ((win._dockState.barSide === "top" || win._dockState.barSide === "bottom") ? win._dockConnectorRadius() : 0) - win._dockFillOverlapX();
}
function _dockBodyYInChrome() {
return ((win._dockState.barSide === "left" || win._dockState.barSide === "right") ? win._dockConnectorRadius() : 0) - win._dockFillOverlapY();
}
function _connectorArcCorner(barSide, placement) {
if (barSide === "top")
return placement === "left" ? "bottomLeft" : "bottomRight";
if (barSide === "bottom")
return placement === "left" ? "topLeft" : "topRight";
if (barSide === "left")
return placement === "left" ? "topRight" : "bottomRight";
return placement === "left" ? "topLeft" : "bottomLeft";
}
function _connectorCutoutX(connectorX, connectorWidth, arcCorner, radius) {
const r = radius === undefined ? win._effectivePopoutCcr : radius;
return (arcCorner === "topLeft" || arcCorner === "bottomLeft") ? connectorX - r : connectorX + connectorWidth - r;
}
function _connectorCutoutY(connectorY, connectorHeight, arcCorner, radius) {
const r = radius === undefined ? win._effectivePopoutCcr : radius;
return (arcCorner === "topLeft" || arcCorner === "topRight") ? connectorY - r : connectorY + connectorHeight - r;
}
// Blur build / teardown
function _buildBlur() { function _buildBlur() {
_teardownBlur();
// Follow the global blur toggle
if (!BlurService.enabled || !SettingsData.frameBlurEnabled || !win._frameActive || !win.visible)
return;
try { try {
if (!BlurService.enabled || !SettingsData.frameBlurEnabled || !win._frameActive || !win.visible) { const region = Qt.createQmlObject(
win.BackgroundEffect.blurRegion = null; 'import QtQuick; import Quickshell; Region {' +
return; ' property Item cutoutItem;' +
} ' property int cutoutRadius: 0;' +
win.BackgroundEffect.blurRegion = _staticBlurRegion; ' Region {' +
' item: cutoutItem;' +
' intersection: Intersection.Subtract;' +
' radius: cutoutRadius;' +
' }' +
'}',
win, "FrameBlurRegion");
region.x = Qt.binding(() => 0);
region.y = Qt.binding(() => 0);
region.width = Qt.binding(() => win._windowRegionWidth);
region.height = Qt.binding(() => win._windowRegionHeight);
region.cutoutItem = _blurCutout;
region.cutoutRadius = Qt.binding(() => win._blurCutoutRadius);
win.BackgroundEffect.blurRegion = region;
win._frameBlurRegion = region;
} catch (e) { } catch (e) {
console.warn("FrameWindow: Failed to set blur region:", e); console.warn("FrameWindow: Failed to create blur region:", e);
} }
} }
function _teardownBlur() { function _teardownBlur() {
if (!win._frameBlurRegion)
return;
try { try {
win.BackgroundEffect.blurRegion = null; win.BackgroundEffect.blurRegion = null;
} catch (e) {} } catch (e) {}
win._frameBlurRegion.destroy();
win._frameBlurRegion = null;
} }
Timer { Timer {
@@ -729,44 +130,24 @@ PanelWindow {
Connections { Connections {
target: SettingsData target: SettingsData
function onFrameBlurEnabledChanged() { function onFrameBlurEnabledChanged() { _blurRebuildTimer.restart(); }
_blurRebuildTimer.restart(); function onFrameEnabledChanged() { _blurRebuildTimer.restart(); }
} function onFrameThicknessChanged() { _blurRebuildTimer.restart(); }
function onFrameEnabledChanged() { function onFrameBarSizeChanged() { _blurRebuildTimer.restart(); }
_blurRebuildTimer.restart(); function onFrameOpacityChanged() { _blurRebuildTimer.restart(); }
} function onFrameRoundingChanged() { _blurRebuildTimer.restart(); }
function onFrameThicknessChanged() { function onFrameScreenPreferencesChanged() { _blurRebuildTimer.restart(); }
_blurRebuildTimer.restart(); function onBarConfigsChanged() { _blurRebuildTimer.restart(); }
}
function onFrameBarSizeChanged() {
_blurRebuildTimer.restart();
}
function onFrameOpacityChanged() {
_blurRebuildTimer.restart();
}
function onFrameRoundingChanged() {
_blurRebuildTimer.restart();
}
function onFrameScreenPreferencesChanged() {
_blurRebuildTimer.restart();
}
function onBarConfigsChanged() {
_blurRebuildTimer.restart();
}
function onConnectedFrameModeActiveChanged() {
_blurRebuildTimer.restart();
}
} }
Connections { Connections {
target: BlurService target: BlurService
function onEnabledChanged() { function onEnabledChanged() { _blurRebuildTimer.restart(); }
_blurRebuildTimer.restart();
}
} }
onVisibleChanged: { onVisibleChanged: {
if (visible) { if (visible) {
win._frameBlurRegion = null;
_blurRebuildTimer.restart(); _blurRebuildTimer.restart();
} else { } else {
_teardownBlur(); _teardownBlur();
@@ -776,176 +157,13 @@ PanelWindow {
Component.onCompleted: Qt.callLater(() => win._buildBlur()) Component.onCompleted: Qt.callLater(() => win._buildBlur())
Component.onDestruction: win._teardownBlur() Component.onDestruction: win._teardownBlur()
// Frame border
FrameBorder { FrameBorder {
anchors.fill: parent anchors.fill: parent
visible: win._frameActive && !win._connectedActive visible: win._frameActive
cutoutTopInset: win.cutoutTopInset cutoutTopInset: win.cutoutTopInset
cutoutBottomInset: win.cutoutBottomInset cutoutBottomInset: win.cutoutBottomInset
cutoutLeftInset: win.cutoutLeftInset cutoutLeftInset: win.cutoutLeftInset
cutoutRightInset: win.cutoutRightInset cutoutRightInset: win.cutoutRightInset
cutoutRadius: win.cutoutRadius cutoutRadius: win.cutoutRadius
} }
// Connected chrome fills
Item {
id: _connectedSurfaceLayer
anchors.fill: parent
visible: win._connectedActive
opacity: win._surfaceOpacity
// Skip FBO when disabled, or when neither elevation nor alpha blend is active
layer.enabled: !win._disableLayer && (Theme.elevationEnabled || win._surfaceOpacity < 1)
layer.smooth: false
layer.effect: MultiEffect {
readonly property var level: Theme.elevationLevel2
readonly property real _shadowBlur: Theme.elevationEnabled ? (level && level.blurPx !== undefined ? level.blurPx : 0) : 0
readonly property real _shadowSpread: Theme.elevationEnabled ? (level && level.spreadPx !== undefined ? level.spreadPx : 0) : 0
autoPaddingEnabled: true
blurEnabled: false
maskEnabled: false
shadowEnabled: !win._disableLayer && Theme.elevationEnabled
shadowBlur: Math.max(0, Math.min(1, _shadowBlur / Math.max(1, Theme.elevationBlurMax)))
shadowScale: 1 + (2 * _shadowSpread) / Math.max(1, Math.min(_connectedSurfaceLayer.width, _connectedSurfaceLayer.height))
shadowHorizontalOffset: Theme.elevationOffsetXFor(level, Theme.elevationLightDirection, 4)
shadowVerticalOffset: Theme.elevationOffsetYFor(level, Theme.elevationLightDirection, 4)
shadowColor: Theme.elevationShadowColor(level)
shadowOpacity: 1
}
FrameBorder {
anchors.fill: parent
borderColor: win._opaqueSurfaceColor
cutoutTopInset: win.cutoutTopInset
cutoutBottomInset: win.cutoutBottomInset
cutoutLeftInset: win.cutoutLeftInset
cutoutRightInset: win.cutoutRightInset
cutoutRadius: win.cutoutRadius
}
Item {
id: _connectedChrome
anchors.fill: parent
visible: true
Item {
id: _popoutChrome
visible: ConnectedModeState.popoutVisible && ConnectedModeState.popoutScreen === win._screenName
x: win._popoutChromeX()
y: win._popoutChromeY()
width: win._popoutChromeWidth()
height: win._popoutChromeHeight()
Item {
id: _popoutClip
readonly property bool _barHoriz: ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom"
// Expand clip by ccr on bar axis to include arc columns
x: win._popoutClipX() - (_barHoriz ? win._effectivePopoutCcr : 0)
y: win._popoutClipY() - (_barHoriz ? 0 : win._effectivePopoutCcr)
width: win._popoutClipWidth() + (_barHoriz ? win._effectivePopoutCcr * 2 : 0)
height: win._popoutClipHeight() + (_barHoriz ? 0 : win._effectivePopoutCcr * 2)
clip: true
ConnectedShape {
id: _popoutShape
visible: _popoutBodyBlurAnchor._active && _popoutBodyBlurAnchor.width > 0 && _popoutBodyBlurAnchor.height > 0
barSide: ConnectedModeState.popoutBarSide
bodyWidth: win._popoutClipWidth()
bodyHeight: win._popoutClipHeight()
connectorRadius: win._effectivePopoutCcr
surfaceRadius: win._surfaceRadius
fillColor: win._opaqueSurfaceColor
x: 0
y: 0
}
}
}
Item {
id: _dockChrome
visible: _dockBodyBlurAnchor._active
x: win._dockChromeX()
y: win._dockChromeY()
width: win._dockChromeWidth()
height: win._dockChromeHeight()
Rectangle {
id: _dockFill
x: win._dockBodyXInChrome()
y: win._dockBodyYInChrome()
width: _dockBodyBlurAnchor.width + win._dockFillOverlapX() * 2
height: _dockBodyBlurAnchor.height + win._dockFillOverlapY() * 2
color: win._opaqueSurfaceColor
z: 1
readonly property string _dockSide: win._dockState.barSide
readonly property real _dockRadius: win._dockBodyBlurRadius()
topLeftRadius: (_dockSide === "top" || _dockSide === "left") ? 0 : _dockRadius
topRightRadius: (_dockSide === "top" || _dockSide === "right") ? 0 : _dockRadius
bottomLeftRadius: (_dockSide === "bottom" || _dockSide === "left") ? 0 : _dockRadius
bottomRightRadius: (_dockSide === "bottom" || _dockSide === "right") ? 0 : _dockRadius
}
ConnectedCorner {
id: _connDockLeft
visible: _dockBodyBlurAnchor._active
barSide: win._dockState.barSide
placement: "left"
spacing: 0
connectorRadius: win._dockConnectorRadius()
color: win._opaqueSurfaceColor
dpr: win._dpr
x: Theme.snap(win._dockConnectorX(_dockBodyBlurAnchor.x, _dockBodyBlurAnchor.width, "left", 0) - _dockChrome.x, win._dpr)
y: Theme.snap(win._dockConnectorY(_dockBodyBlurAnchor.y, _dockBodyBlurAnchor.height, "left", 0) - _dockChrome.y, win._dpr)
}
ConnectedCorner {
id: _connDockRight
visible: _dockBodyBlurAnchor._active
barSide: win._dockState.barSide
placement: "right"
spacing: 0
connectorRadius: win._dockConnectorRadius()
color: win._opaqueSurfaceColor
dpr: win._dpr
x: Theme.snap(win._dockConnectorX(_dockBodyBlurAnchor.x, _dockBodyBlurAnchor.width, "right", 0) - _dockChrome.x, win._dpr)
y: Theme.snap(win._dockConnectorY(_dockBodyBlurAnchor.y, _dockBodyBlurAnchor.height, "right", 0) - _dockChrome.y, win._dpr)
}
}
}
Item {
id: _notifChrome
visible: _notifBodyBlurAnchor._active
readonly property string _notifSide: win._notifState.barSide
readonly property bool _isHoriz: _notifSide === "top" || _notifSide === "bottom"
readonly property real _notifCcr: win._effectiveNotifCcr
readonly property real _sideUnderlap: _isHoriz ? 0 : win._seamOverlap
readonly property real _bodyW: Theme.snap(_notifBodyBlurAnchor.width + _sideUnderlap, win._dpr)
readonly property real _bodyH: Theme.snap(_notifBodyBlurAnchor.height, win._dpr)
z: _isHoriz ? 0 : -1
x: Theme.snap(_notifBodyBlurAnchor.x - (_isHoriz ? _notifCcr : (_notifSide === "left" ? _sideUnderlap : 0)), win._dpr)
y: Theme.snap(_notifBodyBlurAnchor.y - (_isHoriz ? 0 : _notifCcr), win._dpr)
width: _isHoriz ? Theme.snap(_bodyW + _notifCcr * 2, win._dpr) : _bodyW
height: Theme.snap(_bodyH + (_isHoriz ? 0 : _notifCcr * 2), win._dpr)
ConnectedShape {
visible: _notifBodyBlurAnchor._active && _notifBodyBlurAnchor.width > 0 && _notifBodyBlurAnchor.height > 0
barSide: _notifChrome._notifSide
bodyWidth: _notifChrome._bodyW
bodyHeight: _notifChrome._bodyH
connectorRadius: _notifChrome._notifCcr
surfaceRadius: win._surfaceRadius
fillColor: win._opaqueSurfaceColor
x: 0
y: 0
}
}
}
} }
@@ -1338,7 +1338,7 @@ Item {
enabled: MprisController.activePlayer?.canGoPrevious ?? false enabled: MprisController.activePlayer?.canGoPrevious ?? false
hoverEnabled: enabled hoverEnabled: enabled
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: MprisController.previousOrRewind() onClicked: MprisController.activePlayer?.previous()
} }
} }
@@ -34,12 +34,11 @@ Rectangle {
readonly property real actionButtonHeight: compactMode ? 20 : 24 readonly property real actionButtonHeight: compactMode ? 20 : 24
readonly property real collapsedContentHeight: Math.max(iconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2)) readonly property real collapsedContentHeight: Math.max(iconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2))
readonly property real baseCardHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing readonly property real baseCardHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing
readonly property bool connectedFrameMode: SettingsData.connectedFrameModeActive
width: parent ? parent.width : 400 width: parent ? parent.width : 400
height: expanded ? (expandedContent.height + cardPadding * 2) : (baseCardHeight + collapsedContent.extraHeight) height: expanded ? (expandedContent.height + cardPadding * 2) : (baseCardHeight + collapsedContent.extraHeight)
readonly property real targetHeight: expanded ? (expandedContent.height + cardPadding * 2) : (baseCardHeight + collapsedContent.extraHeight) readonly property real targetHeight: expanded ? (expandedContent.height + cardPadding * 2) : (baseCardHeight + collapsedContent.extraHeight)
radius: connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius radius: Theme.cornerRadius
scale: (cardHoverHandler.hovered ? 1.004 : 1.0) * listLevelAdjacentScaleInfluence scale: (cardHoverHandler.hovered ? 1.004 : 1.0) * listLevelAdjacentScaleInfluence
readonly property bool shadowsAllowed: Theme.elevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" readonly property bool shadowsAllowed: Theme.elevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
readonly property var shadowElevation: Theme.elevationLevel1 readonly property var shadowElevation: Theme.elevationLevel1
@@ -101,8 +100,6 @@ Rectangle {
if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) { if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) {
return Theme.primaryHoverLight; return Theme.primaryHoverLight;
} }
if (connectedFrameMode)
return Theme.popupLayerColor(Theme.surfaceContainerHigh);
return Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency); return Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency);
} }
border.color: { border.color: {
@@ -962,9 +959,9 @@ Rectangle {
Behavior on height { Behavior on height {
enabled: root.__initialized && root.userInitiatedExpansion && root.animateExpansion enabled: root.__initialized && root.userInitiatedExpansion && root.animateExpansion
NumberAnimation { NumberAnimation {
duration: root.connectedFrameMode ? Theme.variantDuration(Theme.popoutAnimationDuration, root.expanded) : (root.expanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration) duration: root.expanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: root.connectedFrameMode ? (root.expanded ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : Theme.expressiveCurves.emphasized easing.bezierCurve: Theme.expressiveCurves.emphasized
onRunningChanged: { onRunningChanged: {
if (running) { if (running) {
root.isAnimating = true; root.isAnimating = true;
@@ -39,9 +39,11 @@ DankPopout {
} }
} }
popupWidth: 400 popupWidth: triggerScreen ? Math.min(500, Math.max(380, triggerScreen.width - 48)) : 400
popupHeight: stablePopupHeight popupHeight: stablePopupHeight
positioning: "" positioning: ""
animationScaleCollapsed: 0.94
animationOffset: 0
suspendShadowWhileResizing: false suspendShadowWhileResizing: false
screen: triggerScreen screen: triggerScreen
@@ -10,29 +10,13 @@ import qs.Widgets
PanelWindow { PanelWindow {
id: win id: win
readonly property bool connectedFrameMode: SettingsData.frameEnabled
&& Theme.isConnectedEffect
&& SettingsData.isScreenInPreferences(win.screen, SettingsData.frameScreenPreferences)
readonly property string notifBarSide: {
const pos = SettingsData.notificationPopupPosition;
if (pos === -1) return "top";
switch (pos) {
case SettingsData.Position.Top: return "right";
case SettingsData.Position.Left: return "left";
case SettingsData.Position.BottomCenter: return "bottom";
case SettingsData.Position.Right: return "right";
case SettingsData.Position.Bottom: return "left";
default: return "top";
}
}
WindowBlur { WindowBlur {
targetWindow: win targetWindow: win
blurX: content.x + content.cardInset + swipeTx.x + tx.x blurX: content.x + content.cardInset + swipeTx.x + tx.x
blurY: content.y + content.cardInset + swipeTx.y + tx.y blurY: content.y + content.cardInset + swipeTx.y + tx.y
blurWidth: !win._finalized && !win.connectedFrameMode ? Math.max(0, content.width - content.cardInset * 2) : 0 blurWidth: !win._finalized ? Math.max(0, content.width - content.cardInset * 2) : 0
blurHeight: !win._finalized && !win.connectedFrameMode ? Math.max(0, content.height - content.cardInset * 2) : 0 blurHeight: !win._finalized ? Math.max(0, content.height - content.cardInset * 2) : 0
blurRadius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius blurRadius: Theme.cornerRadius
} }
WlrLayershell.namespace: "dms:notification-popup" WlrLayershell.namespace: "dms:notification-popup"
@@ -41,15 +25,6 @@ PanelWindow {
required property string notificationId required property string notificationId
readonly property bool hasValidData: notificationData && notificationData.notification readonly property bool hasValidData: notificationData && notificationData.notification
readonly property alias hovered: cardHoverHandler.hovered readonly property alias hovered: cardHoverHandler.hovered
readonly property alias swipeActive: content.swipeActive
readonly property alias swipeDismissing: content.swipeDismissing
readonly property bool swipeDismissTowardEdge: {
if (content.swipeDismissing)
return _swipeDismissesTowardFrameEdge();
if (content.swipeActive)
return content.swipeOffset * _frameEdgeSwipeDirection() > 0;
return false;
}
property int screenY: 0 property int screenY: 0
property bool exiting: false property bool exiting: false
property bool _isDestroying: false property bool _isDestroying: false
@@ -57,29 +32,6 @@ PanelWindow {
property real _lastReportedAlignedHeight: -1 property real _lastReportedAlignedHeight: -1
property real _storedTopMargin: 0 property real _storedTopMargin: 0
property real _storedBottomMargin: 0 property real _storedBottomMargin: 0
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
readonly property real entryTravel: {
const base = Math.abs(Theme.effectAnimOffset);
if (directionalEffect) {
if (isCenterPosition)
return Math.max(base, Math.round(content.height * 1.1));
return Math.max(base, Math.round(content.width * 0.95));
}
if (depthEffect)
return Math.max(base, 44);
return base;
}
readonly property real exitTravel: {
if (directionalEffect) {
if (isCenterPosition)
return Math.max(1, content.height);
return Math.max(1, content.width);
}
if (depthEffect)
return Math.round(entryTravel * 1.35);
return Anims.slidePx;
}
readonly property string clearText: I18n.tr("Dismiss") readonly property string clearText: I18n.tr("Dismiss")
property bool descriptionExpanded: false property bool descriptionExpanded: false
readonly property bool hasExpandableBody: (notificationData?.htmlBody || "").replace(/<[^>]*>/g, "").trim().length > 0 readonly property bool hasExpandableBody: (notificationData?.htmlBody || "").replace(/<[^>]*>/g, "").trim().length > 0
@@ -109,7 +61,6 @@ PanelWindow {
signal exitStarted signal exitStarted
signal exitFinished signal exitFinished
signal popupHeightChanged signal popupHeightChanged
signal popupChromeGeometryChanged
function startExit() { function startExit() {
if (exiting || _isDestroying) { if (exiting || _isDestroying) {
@@ -117,7 +68,6 @@ PanelWindow {
} }
exiting = true; exiting = true;
exitStarted(); exitStarted();
popupChromeGeometryChanged();
exitAnim.restart(); exitAnim.restart();
exitWatchdog.restart(); exitWatchdog.restart();
if (NotificationService.removeFromVisibleNotifications) if (NotificationService.removeFromVisibleNotifications)
@@ -197,8 +147,7 @@ PanelWindow {
id: implicitHeightAnim id: implicitHeightAnim
duration: descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration duration: descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: descriptionExpanded ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve easing.bezierCurve: Theme.expressiveCurves.emphasized
onFinished: win.popupHeightChanged()
} }
} }
@@ -246,8 +195,7 @@ PanelWindow {
readonly property real maxPopupShadowBlurPx: Math.max((Theme.elevationLevel3 && Theme.elevationLevel3.blurPx !== undefined) ? Theme.elevationLevel3.blurPx : 12, (Theme.elevationLevel4 && Theme.elevationLevel4.blurPx !== undefined) ? Theme.elevationLevel4.blurPx : 16) readonly property real maxPopupShadowBlurPx: Math.max((Theme.elevationLevel3 && Theme.elevationLevel3.blurPx !== undefined) ? Theme.elevationLevel3.blurPx : 12, (Theme.elevationLevel4 && Theme.elevationLevel4.blurPx !== undefined) ? Theme.elevationLevel4.blurPx : 16)
readonly property real maxPopupShadowOffsetXPx: Math.max(Math.abs(Theme.elevationOffsetX(Theme.elevationLevel3)), Math.abs(Theme.elevationOffsetX(Theme.elevationLevel4))) readonly property real maxPopupShadowOffsetXPx: Math.max(Math.abs(Theme.elevationOffsetX(Theme.elevationLevel3)), Math.abs(Theme.elevationOffsetX(Theme.elevationLevel4)))
readonly property real maxPopupShadowOffsetYPx: Math.max(Math.abs(Theme.elevationOffsetY(Theme.elevationLevel3, 6)), Math.abs(Theme.elevationOffsetY(Theme.elevationLevel4, 8))) readonly property real maxPopupShadowOffsetYPx: Math.max(Math.abs(Theme.elevationOffsetY(Theme.elevationLevel3, 6)), Math.abs(Theme.elevationOffsetY(Theme.elevationLevel4, 8)))
readonly property bool popupWindowShadowActive: Theme.elevationEnabled && SettingsData.notificationPopupShadowEnabled && !connectedFrameMode readonly property real windowShadowPad: Theme.elevationEnabled && SettingsData.notificationPopupShadowEnabled ? Theme.snap(Math.max(16, maxPopupShadowBlurPx + Math.max(maxPopupShadowOffsetXPx, maxPopupShadowOffsetYPx) + 8), dpr) : 0
readonly property real windowShadowPad: popupWindowShadowActive ? Theme.snap(Math.max(16, maxPopupShadowBlurPx + Math.max(maxPopupShadowOffsetXPx, maxPopupShadowOffsetYPx) + 8), dpr) : 0
anchors.top: true anchors.top: true
anchors.left: true anchors.left: true
@@ -292,24 +240,12 @@ PanelWindow {
}); });
} }
function _frameEdgeInset(side) {
if (!screen)
return 0;
const edges = SettingsData.getActiveBarEdgesForScreen(screen);
const raw = edges.includes(side) ? SettingsData.frameBarSize : SettingsData.frameThickness;
return Math.max(0, Math.round(Theme.px(raw, dpr)));
}
function getTopMargin() { function getTopMargin() {
const popupPos = SettingsData.notificationPopupPosition; const popupPos = SettingsData.notificationPopupPosition;
const isTop = isTopCenter || popupPos === SettingsData.Position.Top || popupPos === SettingsData.Position.Left; const isTop = isTopCenter || popupPos === SettingsData.Position.Top || popupPos === SettingsData.Position.Left;
if (!isTop) if (!isTop)
return 0; return 0;
if (connectedFrameMode) {
const cornerClear = isCenterPosition ? 0 : (Theme.px(SettingsData.frameRounding, dpr) + Theme.px(Theme.connectedCornerRadius, dpr));
return _frameEdgeInset("top") + cornerClear + screenY;
}
const barInfo = getBarInfo(); const barInfo = getBarInfo();
const base = barInfo.topBar > 0 ? barInfo.topBar : Theme.popupDistance; const base = barInfo.topBar > 0 ? barInfo.topBar : Theme.popupDistance;
return base + screenY; return base + screenY;
@@ -321,10 +257,6 @@ PanelWindow {
if (!isBottom) if (!isBottom)
return 0; return 0;
if (connectedFrameMode) {
const cornerClear = isCenterPosition ? 0 : (Theme.px(SettingsData.frameRounding, dpr) + Theme.px(Theme.connectedCornerRadius, dpr));
return _frameEdgeInset("bottom") + cornerClear + screenY;
}
const barInfo = getBarInfo(); const barInfo = getBarInfo();
const base = barInfo.bottomBar > 0 ? barInfo.bottomBar : Theme.popupDistance; const base = barInfo.bottomBar > 0 ? barInfo.bottomBar : Theme.popupDistance;
return base + screenY; return base + screenY;
@@ -339,8 +271,6 @@ PanelWindow {
if (!isLeft) if (!isLeft)
return 0; return 0;
if (connectedFrameMode)
return _frameEdgeInset("left");
const barInfo = getBarInfo(); const barInfo = getBarInfo();
return barInfo.leftBar > 0 ? barInfo.leftBar : Theme.popupDistance; return barInfo.leftBar > 0 ? barInfo.leftBar : Theme.popupDistance;
} }
@@ -354,8 +284,6 @@ PanelWindow {
if (!isRight) if (!isRight)
return 0; return 0;
if (connectedFrameMode)
return _frameEdgeInset("right");
const barInfo = getBarInfo(); const barInfo = getBarInfo();
return barInfo.rightBar > 0 ? barInfo.rightBar : Theme.popupDistance; return barInfo.rightBar > 0 ? barInfo.rightBar : Theme.popupDistance;
} }
@@ -400,89 +328,10 @@ PanelWindow {
return Theme.snap(getContentY() - windowShadowPad, dpr); return Theme.snap(getContentY() - windowShadowPad, dpr);
} }
function _swipeDismissTarget() {
return (content.swipeDismissDirection < 0 ? -1 : 1) * content.width;
}
function _frameEdgeSwipeDirection() {
const popupPos = SettingsData.notificationPopupPosition;
return (popupPos === SettingsData.Position.Left || popupPos === SettingsData.Position.Bottom) ? -1 : 1;
}
function _swipeDismissesTowardFrameEdge() {
return content.swipeDismissDirection === _frameEdgeSwipeDirection();
}
function popupChromeMotionActive() {
return popupChromeOpenProgress() < 1 || exiting || content.swipeActive || content.swipeDismissing || Math.abs(content.swipeOffset) > 0.5;
}
function popupLayoutReservesSlot() {
return !content.swipeDismissing;
}
function popupChromeReservesSlot() {
return !content.swipeDismissing;
}
function _chromeMotionOffset() {
return isCenterPosition ? tx.y : tx.x;
}
function _chromeCardTravel() {
return Math.max(1, isCenterPosition ? alignedHeight : alignedWidth);
}
function popupChromeOpenProgress() {
if (exiting || content.swipeDismissing)
return 1;
return Math.max(0, Math.min(1, 1 - Math.abs(_chromeMotionOffset()) / _chromeCardTravel()));
}
function popupChromeReleaseProgress() {
if (exiting) {
const exitRel = Math.max(0, Math.min(1, Math.abs(_chromeMotionOffset()) / _chromeCardTravel()));
if (content.swipeDismissing) {
const swipeRel = Math.max(0, Math.min(1, Math.abs(content.swipeOffset) / Math.max(1, content.swipeTravelDistance)));
return Math.max(exitRel, swipeRel);
}
return exitRel;
}
if (content.swipeDismissing)
return Math.max(0, Math.min(1, Math.abs(content.swipeOffset) / Math.max(1, content.swipeTravelDistance)));
if (content.swipeActive && content.swipeOffset * _frameEdgeSwipeDirection() > 0)
return Math.max(0, Math.min(1, Math.abs(content.swipeOffset) / Math.max(1, content.swipeTravelDistance)));
return 0;
}
function popupChromeFollowsCardMotion() {
return false;
}
function popupChromeMotionX() {
if (!popupChromeMotionActive() || isCenterPosition)
return 0;
const motion = content.swipeOffset + tx.x;
if (content.swipeDismissing && !_swipeDismissesTowardFrameEdge())
return exiting ? Theme.snap(tx.x, dpr) : 0;
if (content.swipeActive && motion * _frameEdgeSwipeDirection() < 0)
return 0;
return Theme.snap(motion, dpr);
}
function popupChromeMotionY() {
return popupChromeMotionActive() ? Theme.snap(tx.y, dpr) : 0;
}
readonly property bool screenValid: win.screen && !_isDestroying readonly property bool screenValid: win.screen && !_isDestroying
readonly property real dpr: screenValid ? CompositorService.getScreenScale(win.screen) : 1 readonly property real dpr: screenValid ? CompositorService.getScreenScale(win.screen) : 1
readonly property real alignedWidth: Theme.px(Math.max(0, implicitWidth - (windowShadowPad * 2)), dpr) readonly property real alignedWidth: Theme.px(Math.max(0, implicitWidth - (windowShadowPad * 2)), dpr)
readonly property real alignedHeight: Theme.px(Math.max(0, implicitHeight - (windowShadowPad * 2)), dpr) readonly property real alignedHeight: Theme.px(Math.max(0, implicitHeight - (windowShadowPad * 2)), dpr)
onScreenYChanged: popupChromeGeometryChanged()
onScreenChanged: popupChromeGeometryChanged()
onConnectedFrameModeChanged: popupChromeGeometryChanged()
onAlignedWidthChanged: popupChromeGeometryChanged()
onAlignedHeightChanged: popupChromeGeometryChanged()
Item { Item {
id: content id: content
@@ -491,7 +340,7 @@ PanelWindow {
y: Theme.snap(windowShadowPad, dpr) y: Theme.snap(windowShadowPad, dpr)
width: alignedWidth width: alignedWidth
height: alignedHeight height: alignedHeight
visible: !win._finalized && !chromeOnlyExit visible: !win._finalized
scale: cardHoverHandler.hovered ? 1.01 : 1.0 scale: cardHoverHandler.hovered ? 1.01 : 1.0
transformOrigin: Item.Center transformOrigin: Item.Center
@@ -503,27 +352,15 @@ PanelWindow {
} }
property real swipeOffset: 0 property real swipeOffset: 0
property real swipeDismissDirection: 1 readonly property real dismissThreshold: isCenterPosition ? height * 0.4 : width * 0.35
property bool chromeOnlyExit: false
readonly property real dismissThreshold: width * 0.35
readonly property real swipeFadeStartRatio: 0.75 readonly property real swipeFadeStartRatio: 0.75
readonly property real swipeTravelDistance: width readonly property real swipeTravelDistance: isCenterPosition ? height : width
readonly property real swipeFadeStartOffset: swipeTravelDistance * swipeFadeStartRatio readonly property real swipeFadeStartOffset: swipeTravelDistance * swipeFadeStartRatio
readonly property real swipeFadeDistance: Math.max(1, swipeTravelDistance - swipeFadeStartOffset) readonly property real swipeFadeDistance: Math.max(1, swipeTravelDistance - swipeFadeStartOffset)
readonly property bool swipeActive: swipeDragHandler.active readonly property bool swipeActive: swipeDragHandler.active
property bool swipeDismissing: false property bool swipeDismissing: false
onSwipeDismissingChanged: {
if (!win.connectedFrameMode)
return;
win.popupHeightChanged();
win.popupChromeGeometryChanged();
}
onSwipeOffsetChanged: {
if (win.connectedFrameMode)
win.popupChromeGeometryChanged();
}
readonly property bool shadowsAllowed: win.popupWindowShadowActive readonly property bool shadowsAllowed: Theme.elevationEnabled && SettingsData.notificationPopupShadowEnabled
readonly property var elevLevel: cardHoverHandler.hovered ? Theme.elevationLevel4 : Theme.elevationLevel3 readonly property var elevLevel: cardHoverHandler.hovered ? Theme.elevationLevel4 : Theme.elevationLevel3
readonly property real cardInset: Theme.snap(4, win.dpr) readonly property real cardInset: Theme.snap(4, win.dpr)
readonly property real shadowRenderPadding: shadowsAllowed ? Theme.snap(Math.max(16, shadowBlurPx + Math.max(Math.abs(shadowOffsetX), Math.abs(shadowOffsetY)) + 8), win.dpr) : 0 readonly property real shadowRenderPadding: shadowsAllowed ? Theme.snap(Math.max(16, shadowBlurPx + Math.max(Math.abs(shadowOffsetX), Math.abs(shadowOffsetY)) + 8), win.dpr) : 0
@@ -571,12 +408,8 @@ PanelWindow {
sourceRect.y: content.shadowRenderPadding + content.cardInset sourceRect.y: content.shadowRenderPadding + content.cardInset
sourceRect.width: Math.max(0, content.width - (content.cardInset * 2)) sourceRect.width: Math.max(0, content.width - (content.cardInset * 2))
sourceRect.height: Math.max(0, content.height - (content.cardInset * 2)) sourceRect.height: Math.max(0, content.height - (content.cardInset * 2))
sourceRect.radius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius sourceRect.radius: Theme.cornerRadius
sourceRect.color: win.connectedFrameMode ? Theme.popupLayerColor(Theme.surfaceContainer) : Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) sourceRect.color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
sourceRect.antialiasing: true
sourceRect.layer.enabled: win.connectedFrameMode
sourceRect.layer.smooth: true
sourceRect.layer.textureSize: win.connectedFrameMode && win.dpr > 1 ? Qt.size(Math.ceil(sourceRect.width * win.dpr), Math.ceil(sourceRect.height * win.dpr)) : Qt.size(0, 0)
sourceRect.border.color: notificationData && notificationData.urgency === NotificationUrgency.Critical ? Theme.withAlpha(Theme.primary, 0.3) : Theme.withAlpha(Theme.outline, 0.08) sourceRect.border.color: notificationData && notificationData.urgency === NotificationUrgency.Critical ? Theme.withAlpha(Theme.primary, 0.3) : Theme.withAlpha(Theme.outline, 0.08)
sourceRect.border.width: notificationData && notificationData.urgency === NotificationUrgency.Critical ? 2 : 0 sourceRect.border.width: notificationData && notificationData.urgency === NotificationUrgency.Critical ? 2 : 0
@@ -614,10 +447,10 @@ PanelWindow {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
anchors.margins: content.cardInset anchors.margins: content.cardInset
radius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius radius: Theme.cornerRadius
color: "transparent" color: "transparent"
border.color: win.connectedFrameMode ? "transparent" : BlurService.borderColor border.color: BlurService.borderColor
border.width: win.connectedFrameMode ? 0 : BlurService.borderWidth border.width: BlurService.borderWidth
z: 100 z: 100
} }
@@ -1017,15 +850,14 @@ PanelWindow {
DragHandler { DragHandler {
id: swipeDragHandler id: swipeDragHandler
target: null target: null
xAxis.enabled: true xAxis.enabled: !isCenterPosition
yAxis.enabled: false yAxis.enabled: isCenterPosition
onActiveChanged: { onActiveChanged: {
if (active || win.exiting || content.swipeDismissing) if (active || win.exiting || content.swipeDismissing)
return; return;
if (Math.abs(content.swipeOffset) > content.dismissThreshold) { if (Math.abs(content.swipeOffset) > content.dismissThreshold) {
content.swipeDismissDirection = content.swipeOffset < 0 ? -1 : 1;
content.swipeDismissing = true; content.swipeDismissing = true;
swipeDismissAnim.start(); swipeDismissAnim.start();
} else { } else {
@@ -1037,7 +869,15 @@ PanelWindow {
if (win.exiting) if (win.exiting)
return; return;
content.swipeOffset = translation.x; const raw = isCenterPosition ? translation.y : translation.x;
if (isTopCenter) {
content.swipeOffset = Math.min(0, raw);
} else if (isBottomCenter) {
content.swipeOffset = Math.max(0, raw);
} else {
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
content.swipeOffset = isLeft ? Math.min(0, raw) : Math.max(0, raw);
}
} }
} }
@@ -1068,28 +908,20 @@ PanelWindow {
id: swipeDismissAnim id: swipeDismissAnim
target: content target: content
property: "swipeOffset" property: "swipeOffset"
to: win._swipeDismissTarget() to: isTopCenter ? -content.height : isBottomCenter ? content.height : (SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom ? -content.width : content.width)
duration: Theme.notificationExitDuration duration: Theme.notificationExitDuration
easing.type: Easing.OutCubic easing.type: Easing.OutCubic
onStopped: { onStopped: {
const inwardConnectedExit = win.connectedFrameMode && !win.isCenterPosition && !win._swipeDismissesTowardFrameEdge(); NotificationService.dismissNotification(notificationData);
if (inwardConnectedExit) win.forceExit();
content.chromeOnlyExit = true;
if (win.connectedFrameMode) {
win.startExit();
NotificationService.dismissNotification(notificationData);
} else {
NotificationService.dismissNotification(notificationData);
win.forceExit();
}
} }
} }
transform: [ transform: [
Translate { Translate {
id: swipeTx id: swipeTx
x: content.swipeOffset x: isCenterPosition ? 0 : content.swipeOffset
y: 0 y: isCenterPosition ? content.swipeOffset : 0
}, },
Translate { Translate {
id: tx id: tx
@@ -1097,17 +929,9 @@ PanelWindow {
if (isCenterPosition) if (isCenterPosition)
return 0; return 0;
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom; const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
return isLeft ? -entryTravel : entryTravel; return isLeft ? -Anims.slidePx : Anims.slidePx;
}
y: isTopCenter ? -entryTravel : isBottomCenter ? entryTravel : 0
onXChanged: {
if (win.connectedFrameMode)
win.popupChromeGeometryChanged();
}
onYChanged: {
if (win.connectedFrameMode)
win.popupChromeGeometryChanged();
} }
y: isTopCenter ? -Anims.slidePx : isBottomCenter ? Anims.slidePx : 0
} }
] ]
} }
@@ -1119,16 +943,16 @@ PanelWindow {
property: isCenterPosition ? "y" : "x" property: isCenterPosition ? "y" : "x"
from: { from: {
if (isTopCenter) if (isTopCenter)
return -entryTravel; return -Anims.slidePx;
if (isBottomCenter) if (isBottomCenter)
return entryTravel; return Anims.slidePx;
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom; const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
return isLeft ? -entryTravel : entryTravel; return isLeft ? -Anims.slidePx : Anims.slidePx;
} }
to: 0 to: 0
duration: Theme.notificationEnterDuration duration: Theme.notificationEnterDuration
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantPopoutEnterCurve easing.bezierCurve: isCenterPosition ? Theme.expressiveCurves.standardDecel : Theme.expressiveCurves.emphasizedDecel
onStopped: { onStopped: {
if (!win.exiting && !win._isDestroying) { if (!win.exiting && !win._isDestroying) {
if (isCenterPosition) { if (isCenterPosition) {
@@ -1153,35 +977,35 @@ PanelWindow {
from: 0 from: 0
to: { to: {
if (isTopCenter) if (isTopCenter)
return -exitTravel; return -Anims.slidePx;
if (isBottomCenter) if (isBottomCenter)
return exitTravel; return Anims.slidePx;
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom; const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
return isLeft ? -exitTravel : exitTravel; return isLeft ? -Anims.slidePx : Anims.slidePx;
} }
duration: Theme.notificationExitDuration duration: Theme.notificationExitDuration
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantPopoutExitCurve easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
} }
NumberAnimation { NumberAnimation {
target: content target: content
property: "opacity" property: "opacity"
from: 1 from: 1
to: Theme.isDirectionalEffect ? 1 : 0 to: 0
duration: Theme.notificationExitDuration duration: Theme.notificationExitDuration
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantPopoutExitCurve easing.bezierCurve: Theme.expressiveCurves.standardAccel
} }
NumberAnimation { NumberAnimation {
target: content target: content
property: "scale" property: "scale"
from: 1 from: 1
to: Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed to: 0.98
duration: Theme.notificationExitDuration duration: Theme.notificationExitDuration
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantPopoutExitCurve easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
} }
} }
@@ -8,44 +8,23 @@ QtObject {
property var modelData property var modelData
property int topMargin: 0 property int topMargin: 0
readonly property bool compactMode: SettingsData.notificationCompactMode readonly property bool compactMode: SettingsData.notificationCompactMode
readonly property bool notificationConnectedMode: SettingsData.frameEnabled
&& Theme.isConnectedEffect
&& SettingsData.isScreenInPreferences(manager.modelData, SettingsData.frameScreenPreferences)
readonly property string notifBarSide: {
const pos = SettingsData.notificationPopupPosition;
if (pos === -1) return "top";
switch (pos) {
case SettingsData.Position.Top: return "right";
case SettingsData.Position.Left: return "left";
case SettingsData.Position.BottomCenter: return "bottom";
case SettingsData.Position.Right: return "right";
case SettingsData.Position.Bottom: return "left";
default: return "top";
}
}
readonly property real cardPadding: compactMode ? Theme.notificationCardPaddingCompact : Theme.notificationCardPadding readonly property real cardPadding: compactMode ? Theme.notificationCardPaddingCompact : Theme.notificationCardPadding
readonly property real popupIconSize: compactMode ? Theme.notificationIconSizeCompact : Theme.notificationIconSizeNormal readonly property real popupIconSize: compactMode ? Theme.notificationIconSizeCompact : Theme.notificationIconSizeNormal
readonly property real actionButtonHeight: compactMode ? 20 : 24 readonly property real actionButtonHeight: compactMode ? 20 : 24
readonly property real contentSpacing: compactMode ? Theme.spacingXS : Theme.spacingS readonly property real contentSpacing: compactMode ? Theme.spacingXS : Theme.spacingS
readonly property real popupSpacing: notificationConnectedMode ? 0 : (compactMode ? 0 : Theme.spacingXS) readonly property real popupSpacing: compactMode ? 0 : Theme.spacingXS
readonly property real collapsedContentHeight: Math.max(popupIconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2)) readonly property real collapsedContentHeight: Math.max(popupIconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2))
readonly property int baseNotificationHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing + popupSpacing readonly property int baseNotificationHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing + popupSpacing
property var popupWindows: [] property var popupWindows: []
property var destroyingWindows: new Set() property var destroyingWindows: new Set()
property var pendingDestroys: [] property var pendingDestroys: []
property int destroyDelayMs: 100 property int destroyDelayMs: 100
property bool _chromeSyncPending: false
readonly property real chromeOpenProgressThreshold: 0.10
readonly property real chromeReleaseTailStart: 0.90
readonly property real chromeReleaseDropProgress: 0.995
property Component popupComponent property Component popupComponent
popupComponent: Component { popupComponent: Component {
NotificationPopup { NotificationPopup {
onExitFinished: manager._onPopupExitFinished(this) onExitFinished: manager._onPopupExitFinished(this)
onExitStarted: manager._onPopupExitStarted(this)
onPopupHeightChanged: manager._onPopupHeightChanged(this) onPopupHeightChanged: manager._onPopupHeightChanged(this)
onPopupChromeGeometryChanged: manager._onPopupChromeGeometryChanged(this)
} }
} }
@@ -129,27 +108,6 @@ QtObject {
return p && p.status !== Component.Null && !p._isDestroying && p.hasValidData; return p && p.status !== Component.Null && !p._isDestroying && p.hasValidData;
} }
function _layoutWindows() {
return popupWindows.filter(p => _isValidWindow(p) && p.notificationData?.popup && !p.exiting && (!p.popupLayoutReservesSlot || p.popupLayoutReservesSlot()));
}
function _chromeWindows() {
return popupWindows.filter(p => {
if (!p || p.status === Component.Null || !p.visible || p._finalized || !p.hasValidData)
return false;
if (!p.notificationData?.popup && !p.exiting)
return false;
if (!p.exiting && p.popupChromeOpenProgress && p.popupChromeOpenProgress() < chromeOpenProgressThreshold)
return false;
// Keep the connected shell until the card is almost fully closed.
if (p.exiting && !p.swipeActive && p.popupChromeReleaseProgress) {
if (p.popupChromeReleaseProgress() > chromeReleaseDropProgress)
return false;
}
return true;
});
}
function _isFocusedScreen() { function _isFocusedScreen() {
if (!SettingsData.notificationFocusedMonitor) if (!SettingsData.notificationFocusedMonitor)
return true; return true;
@@ -158,24 +116,18 @@ QtObject {
} }
function _sync(newWrappers) { function _sync(newWrappers) {
let needsReposition = false;
for (const p of popupWindows.slice()) { for (const p of popupWindows.slice()) {
if (!_isValidWindow(p) || p.exiting) if (!_isValidWindow(p) || p.exiting)
continue; continue;
if (p.notificationData && newWrappers.indexOf(p.notificationData) === -1) { if (p.notificationData && newWrappers.indexOf(p.notificationData) === -1) {
p.notificationData.removedByLimit = true; p.notificationData.removedByLimit = true;
p.notificationData.popup = false; p.notificationData.popup = false;
needsReposition = true;
} }
} }
for (const w of newWrappers) { for (const w of newWrappers) {
if (w && !_hasWindowFor(w) && _isFocusedScreen()) { if (w && !_hasWindowFor(w) && _isFocusedScreen())
_insertAtTop(w); _insertAtTop(w);
needsReposition = false;
}
} }
if (needsReposition)
_repositionAll();
} }
function _popupHeight(p) { function _popupHeight(p) {
@@ -205,7 +157,7 @@ QtObject {
} }
function _repositionAll() { function _repositionAll() {
const active = _layoutWindows(); const active = popupWindows.filter(p => _isValidWindow(p) && p.notificationData?.popup && !p.exiting);
const pinnedSlots = []; const pinnedSlots = [];
for (const p of active) { for (const p of active) {
@@ -229,224 +181,6 @@ QtObject {
win.screenY = currentY; win.screenY = currentY;
currentY += _popupHeight(win); currentY += _popupHeight(win);
} }
_scheduleNotificationChromeSync();
}
function _scheduleNotificationChromeSync() {
if (_chromeSyncPending)
return;
_chromeSyncPending = true;
Qt.callLater(() => {
_chromeSyncPending = false;
_syncNotificationChromeState();
});
}
function _clamp01(value) {
return Math.max(0, Math.min(1, value));
}
function _clipRectFromBarSide(rect, visibleFraction) {
const fraction = _clamp01(visibleFraction);
const w = Math.max(0, rect.right - rect.x);
const h = Math.max(0, rect.bottom - rect.y);
if (notifBarSide === "right") {
rect.x = rect.right - w * fraction;
} else if (notifBarSide === "left") {
rect.right = rect.x + w * fraction;
} else if (notifBarSide === "bottom") {
rect.y = rect.bottom - h * fraction;
} else {
rect.bottom = rect.y + h * fraction;
}
return rect;
}
function _popupChromeVisibleFraction(p) {
if (p.popupChromeReleaseProgress) {
const rel = p.popupChromeReleaseProgress();
if (p.exiting)
return Math.max(0, 1 - rel);
if (rel > 0)
return p.swipeDismissTowardEdge ? Math.max(0, 1 - rel) : 1 - _chromeReleaseTailProgress(rel);
}
if (p.popupChromeOpenProgress)
return _clamp01(p.popupChromeOpenProgress());
return 1;
}
function _popupChromeRect(p, useMotionOffset) {
if (!p || !p.screen)
return null;
const x = p.getContentX ? p.getContentX() : 0;
const y = p.getContentY ? p.getContentY() : 0;
const w = p.alignedWidth || 0;
const h = Math.max(p.alignedHeight || 0, baseNotificationHeight);
if (w <= 0 || h <= 0)
return null;
const rect = {
x: x,
y: y,
right: x + w,
bottom: y + h
};
if (!useMotionOffset)
return rect;
if (p.popupChromeFollowsCardMotion && p.popupChromeFollowsCardMotion()) {
const motionX = p.popupChromeMotionX ? p.popupChromeMotionX() : 0;
const motionY = p.popupChromeMotionY ? p.popupChromeMotionY() : 0;
rect.x += motionX;
rect.y += motionY;
rect.right += motionX;
rect.bottom += motionY;
return rect;
}
return _clipRectFromBarSide(rect, _popupChromeVisibleFraction(p));
}
function _chromeReleaseTailProgress(rawProgress) {
const progress = Math.max(0, Math.min(1, rawProgress));
if (progress <= chromeReleaseTailStart)
return 0;
return Math.max(0, Math.min(1, (progress - chromeReleaseTailStart) / Math.max(0.001, 1 - chromeReleaseTailStart)));
}
function _popupChromeBoundsRect(p, trailing, useMotionOffset) {
const rect = _popupChromeRect(p, useMotionOffset);
if (!rect || p !== trailing || !p.popupChromeReleaseProgress)
return rect;
const progress = _chromeReleaseTailProgress(p.popupChromeReleaseProgress());
if (progress <= 0)
return rect;
const anchorsTop = _stackAnchorsTop();
const h = Math.max(0, rect.bottom - rect.y);
const shrink = h * progress;
if (anchorsTop)
rect.bottom = Math.max(rect.y, rect.bottom - shrink);
else
rect.y = Math.min(rect.bottom, rect.y + shrink);
return rect;
}
function _stackAnchorsTop() {
const pos = SettingsData.notificationPopupPosition;
return pos === -1 || pos === SettingsData.Position.Top || pos === SettingsData.Position.Left;
}
function _trailingChromeWindow(candidates) {
const anchorsTop = _stackAnchorsTop();
let trailing = null;
let edge = anchorsTop ? -Infinity : Infinity;
for (const p of candidates) {
const rect = _popupChromeRect(p, false);
if (!rect)
continue;
const candidateEdge = anchorsTop ? rect.bottom : rect.y;
if ((anchorsTop && candidateEdge > edge) || (!anchorsTop && candidateEdge < edge)) {
edge = candidateEdge;
trailing = p;
}
}
return trailing;
}
function _chromeWindowReservesSlot(p, trailing) {
if (p === trailing)
return true;
return !p.popupChromeReservesSlot || p.popupChromeReservesSlot();
}
function _stackAnchoredChromeEdge(candidates) {
const anchorsTop = _stackAnchorsTop();
let edge = anchorsTop ? Infinity : -Infinity;
for (const p of candidates) {
const rect = _popupChromeRect(p, false);
if (!rect)
continue;
if (anchorsTop && rect.y < edge)
edge = rect.y;
if (!anchorsTop && rect.bottom > edge)
edge = rect.bottom;
}
if (edge === Infinity || edge === -Infinity)
return null;
return {
anchorsTop: anchorsTop,
edge: edge
};
}
function _syncNotificationChromeState() {
const screenName = manager.modelData?.name || "";
if (!screenName)
return;
if (!notificationConnectedMode) {
ConnectedModeState.clearNotificationState(screenName);
return;
}
const chromeCandidates = _chromeWindows();
if (chromeCandidates.length === 0) {
ConnectedModeState.clearNotificationState(screenName);
return;
}
const trailing = chromeCandidates.length > 1 ? _trailingChromeWindow(chromeCandidates) : null;
let active = chromeCandidates;
if (chromeCandidates.length > 1) {
const reserving = chromeCandidates.filter(p => _chromeWindowReservesSlot(p, trailing));
if (reserving.length > 0)
active = reserving;
}
let minX = Infinity;
let minY = Infinity;
let maxXEnd = -Infinity;
let maxYEnd = -Infinity;
const useMotionOffset = active.length === 1 && active[0].popupChromeMotionActive && active[0].popupChromeMotionActive();
for (const p of active) {
const rect = _popupChromeBoundsRect(p, trailing, useMotionOffset);
if (!rect)
continue;
if (rect.x < minX)
minX = rect.x;
if (rect.y < minY)
minY = rect.y;
if (rect.right > maxXEnd)
maxXEnd = rect.right;
if (rect.bottom > maxYEnd)
maxYEnd = rect.bottom;
}
const stackEdge = _stackAnchoredChromeEdge(chromeCandidates);
if (stackEdge !== null) {
if (stackEdge.anchorsTop && stackEdge.edge < minY)
minY = stackEdge.edge;
if (!stackEdge.anchorsTop && stackEdge.edge > maxYEnd)
maxYEnd = stackEdge.edge;
}
if (minX === Infinity || minY === Infinity || maxXEnd <= minX || maxYEnd <= minY) {
ConnectedModeState.clearNotificationState(screenName);
return;
}
ConnectedModeState.setNotificationState(screenName, {
visible: true,
barSide: notifBarSide,
bodyX: minX,
bodyY: minY,
bodyW: maxXEnd - minX,
bodyH: maxYEnd - minY
});
}
function _onPopupChromeGeometryChanged(p) {
if (!p || popupWindows.indexOf(p) === -1)
return;
_scheduleNotificationChromeSync();
} }
function _onPopupHeightChanged(p) { function _onPopupHeightChanged(p) {
@@ -457,12 +191,6 @@ QtObject {
_repositionAll(); _repositionAll();
} }
function _onPopupExitStarted(p) {
if (!p || popupWindows.indexOf(p) === -1)
return;
_repositionAll();
}
function _onPopupExitFinished(p) { function _onPopupExitFinished(p) {
if (!p) if (!p)
return; return;
@@ -499,15 +227,8 @@ QtObject {
} }
popupWindows = []; popupWindows = [];
destroyingWindows.clear(); destroyingWindows.clear();
_chromeSyncPending = false;
_syncNotificationChromeState();
} }
onNotificationConnectedModeChanged: _scheduleNotificationChromeSync()
onNotifBarSideChanged: _scheduleNotificationChromeSync()
onModelDataChanged: _scheduleNotificationChromeSync()
onTopMarginChanged: _repositionAll()
onPopupWindowsChanged: { onPopupWindowsChanged: {
if (popupWindows.length > 0 && !sweeper.running) { if (popupWindows.length > 0 && !sweeper.running) {
sweeper.start(); sweeper.start();
@@ -27,7 +27,6 @@ Item {
const pos = selectedBarConfig?.position ?? SettingsData.Position.Top; const pos = selectedBarConfig?.position ?? SettingsData.Position.Top;
return pos === SettingsData.Position.Left || pos === SettingsData.Position.Right; return pos === SettingsData.Position.Left || pos === SettingsData.Position.Right;
} }
readonly property bool connectedFrameModeActive: SettingsData.connectedFrameModeActive
Timer { Timer {
id: horizontalBarChangeDebounce id: horizontalBarChangeDebounce
@@ -1111,35 +1110,6 @@ Item {
} }
} }
Item {
visible: dankBarTab.connectedFrameModeActive
width: parent.width
implicitHeight: connectedFrameStyleNote.implicitHeight + Theme.spacingS * 2
Row {
id: connectedFrameStyleNote
x: Theme.spacingM
width: parent.width - Theme.spacingM * 2
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "frame_source"
size: Theme.fontSizeMedium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Connected Frame mode keeps bar shadow override, border, and corner overrides off while active")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width - Theme.fontSizeMedium - Theme.spacingS
}
}
}
SettingsCard { SettingsCard {
id: shadowCard id: shadowCard
iconName: "layers" iconName: "layers"
@@ -1148,8 +1118,6 @@ Item {
collapsible: true collapsible: true
expanded: true expanded: true
visible: selectedBarConfig?.enabled visible: selectedBarConfig?.enabled
enabled: !dankBarTab.connectedFrameModeActive
opacity: dankBarTab.connectedFrameModeActive ? 0.5 : 1.0
readonly property bool shadowActive: (selectedBarConfig?.shadowIntensity ?? 0) > 0 readonly property bool shadowActive: (selectedBarConfig?.shadowIntensity ?? 0) > 0
readonly property bool isCustomColor: (selectedBarConfig?.shadowColorMode ?? "default") === "custom" readonly property bool isCustomColor: (selectedBarConfig?.shadowColorMode ?? "default") === "custom"
@@ -1483,8 +1451,6 @@ Item {
iconName: "border_style" iconName: "border_style"
title: I18n.tr("Border") title: I18n.tr("Border")
visible: selectedBarConfig?.enabled visible: selectedBarConfig?.enabled
enabled: !dankBarTab.connectedFrameModeActive
opacity: dankBarTab.connectedFrameModeActive ? 0.5 : 1.0
checked: selectedBarConfig?.borderEnabled ?? false checked: selectedBarConfig?.borderEnabled ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, { onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
borderEnabled: checked borderEnabled: checked
-40
View File
@@ -7,9 +7,6 @@ import qs.Modules.Settings.Widgets
Item { Item {
id: root id: root
readonly property bool connectedFrameModeActive: SettingsData.frameEnabled
&& SettingsData.motionEffect === 1
&& SettingsData.directionalAnimationMode === 3
FileBrowserModal { FileBrowserModal {
id: dockLogoFileBrowser id: dockLogoFileBrowser
@@ -547,8 +544,6 @@ Item {
SettingsSliderRow { SettingsSliderRow {
text: I18n.tr("Exclusive Zone Offset") text: I18n.tr("Exclusive Zone Offset")
enabled: !root.connectedFrameModeActive
opacity: root.connectedFrameModeActive ? 0.5 : 1.0
value: SettingsData.dockBottomGap value: SettingsData.dockBottomGap
minimum: -100 minimum: -100
maximum: 100 maximum: 100
@@ -558,8 +553,6 @@ Item {
SettingsSliderRow { SettingsSliderRow {
text: I18n.tr("Margin") text: I18n.tr("Margin")
enabled: !root.connectedFrameModeActive
opacity: root.connectedFrameModeActive ? 0.5 : 1.0
value: SettingsData.dockMargin value: SettingsData.dockMargin
minimum: 0 minimum: 0
maximum: 100 maximum: 100
@@ -568,42 +561,11 @@ Item {
} }
} }
Item {
visible: root.connectedFrameModeActive
width: parent.width
implicitHeight: dockConnectedNote.implicitHeight + Theme.spacingS * 2
Row {
id: dockConnectedNote
x: Theme.spacingM
width: parent.width - Theme.spacingM * 2
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "frame_source"
size: Theme.fontSizeMedium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Connected Frame mode manages dock edge offset, transparency, blur, and border styling")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width - Theme.fontSizeMedium - Theme.spacingS
}
}
}
SettingsCard { SettingsCard {
width: parent.width width: parent.width
iconName: "opacity" iconName: "opacity"
title: I18n.tr("Transparency") title: I18n.tr("Transparency")
settingKey: "dockTransparency" settingKey: "dockTransparency"
enabled: !root.connectedFrameModeActive
opacity: root.connectedFrameModeActive ? 0.5 : 1.0
SettingsSliderRow { SettingsSliderRow {
text: I18n.tr("Dock Transparency") text: I18n.tr("Dock Transparency")
@@ -623,8 +585,6 @@ Item {
settingKey: "dockBorder" settingKey: "dockBorder"
collapsible: true collapsible: true
expanded: false expanded: false
enabled: !root.connectedFrameModeActive
opacity: root.connectedFrameModeActive ? 0.5 : 1.0
SettingsToggleRow { SettingsToggleRow {
text: I18n.tr("Border") text: I18n.tr("Border")
+3 -32
View File
@@ -56,9 +56,6 @@ Item {
settingKey: "frameRounding" settingKey: "frameRounding"
tags: ["frame", "border", "rounding", "radius", "corner"] tags: ["frame", "border", "rounding", "radius", "corner"]
text: I18n.tr("Border Radius") text: I18n.tr("Border Radius")
description: SettingsData.connectedFrameModeActive
? I18n.tr("Controls the radius of the frame and all connected popout, dock, and modal surfaces while Connected Mode is active")
: I18n.tr("Controls the frame border radius. This also becomes the connected surface radius whenever Connected Mode is active")
unit: "px" unit: "px"
minimum: 0 minimum: 0
maximum: 100 maximum: 100
@@ -118,9 +115,8 @@ Item {
SettingsSliderRow { SettingsSliderRow {
id: opacitySlider id: opacitySlider
settingKey: "frameOpacity" settingKey: "frameOpacity"
tags: ["frame", "border", "surface", "popup", "opacity", "transparency"] tags: ["frame", "border", "opacity", "transparency"]
text: I18n.tr("Surface Opacity") text: I18n.tr("Frame Opacity")
description: I18n.tr("Frame border opacity. Controls all surface opacity globally when Connected Mode is active")
unit: "%" unit: "%"
minimum: 0 minimum: 0
maximum: 100 maximum: 100
@@ -265,7 +261,7 @@ Item {
title: I18n.tr("Bar Integration") title: I18n.tr("Bar Integration")
settingKey: "frameBarIntegration" settingKey: "frameBarIntegration"
collapsible: true collapsible: true
expanded: true expanded: false
visible: SettingsData.frameEnabled visible: SettingsData.frameEnabled
SettingsToggleRow { SettingsToggleRow {
@@ -277,31 +273,6 @@ Item {
checked: SettingsData.frameShowOnOverview checked: SettingsData.frameShowOnOverview
onToggled: checked => SettingsData.set("frameShowOnOverview", checked) onToggled: checked => SettingsData.set("frameShowOnOverview", checked)
} }
SettingsToggleRow {
visible: SettingsData.frameEnabled
settingKey: "directionalAnimationMode"
tags: ["frame", "connected", "popout", "corner", "animation"]
text: I18n.tr("Connected Mode")
description: I18n.tr("Popouts emerge flush from the bar edge as one continuous piece (based on Slide)")
checked: SettingsData.connectedFrameModeActive
onToggled: checked => {
if (checked) {
if (SettingsData.directionalAnimationMode !== 3)
SettingsData.set("previousDirectionalMode", SettingsData.directionalAnimationMode);
SettingsData.set("motionEffect", 1);
SettingsData.set("directionalAnimationMode", 3);
} else {
SettingsData.set("directionalAnimationMode", SettingsData.previousDirectionalMode);
}
}
Connections {
target: SettingsData
function onDirectionalAnimationModeChanged() {}
function onMotionEffectChanged() {}
}
}
} }
// Display Assignment // Display Assignment
+2 -64
View File
@@ -606,8 +606,6 @@ Item {
property var allLauncherPlugins: { property var allLauncherPlugins: {
SettingsData.launcherPluginVisibility; SettingsData.launcherPluginVisibility;
SettingsData.launcherPluginOrder; SettingsData.launcherPluginOrder;
SettingsData.dankLauncherV2IncludeFilesInAll;
SettingsData.dankLauncherV2IncludeFoldersInAll;
var plugins = []; var plugins = [];
var builtIn = AppSearchService.getBuiltInLauncherPlugins() || {}; var builtIn = AppSearchService.getBuiltInLauncherPlugins() || {};
for (var pluginId in builtIn) { for (var pluginId in builtIn) {
@@ -618,7 +616,6 @@ Item {
icon: plugin.cornerIcon || "extension", icon: plugin.cornerIcon || "extension",
iconType: "material", iconType: "material",
isBuiltIn: true, isBuiltIn: true,
isVirtual: false,
trigger: AppSearchService.getBuiltInPluginTrigger(pluginId) || "" trigger: AppSearchService.getBuiltInPluginTrigger(pluginId) || ""
}); });
} }
@@ -632,32 +629,9 @@ Item {
icon: rawIcon.startsWith("material:") ? rawIcon.substring(9) : rawIcon.startsWith("unicode:") ? rawIcon.substring(8) : rawIcon, icon: rawIcon.startsWith("material:") ? rawIcon.substring(9) : rawIcon.startsWith("unicode:") ? rawIcon.substring(8) : rawIcon,
iconType: rawIcon.startsWith("unicode:") ? "unicode" : "material", iconType: rawIcon.startsWith("unicode:") ? "unicode" : "material",
isBuiltIn: false, isBuiltIn: false,
isVirtual: false,
trigger: PluginService.getPluginTrigger(pluginId) || "" trigger: PluginService.getPluginTrigger(pluginId) || ""
}); });
} }
if (SettingsData.dankLauncherV2IncludeFilesInAll) {
plugins.push({
id: "__files",
name: I18n.tr("Files"),
icon: "insert_drive_file",
iconType: "material",
isBuiltIn: false,
isVirtual: true,
trigger: "/"
});
}
if (SettingsData.dankLauncherV2IncludeFoldersInAll) {
plugins.push({
id: "__folders",
name: I18n.tr("Folders"),
icon: "folder",
iconType: "material",
isBuiltIn: false,
isVirtual: true,
trigger: "/"
});
}
return SettingsData.getOrderedLauncherPlugins(plugins); return SettingsData.getOrderedLauncherPlugins(plugins);
} }
@@ -776,27 +750,9 @@ Item {
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: Theme.spacingM anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
checked: { checked: SettingsData.getPluginAllowWithoutTrigger(visibilityDelegateItem.modelData.id)
switch (visibilityDelegateItem.modelData.id) {
case "__files":
return SettingsData.dankLauncherV2IncludeFilesInAll;
case "__folders":
return SettingsData.dankLauncherV2IncludeFoldersInAll;
default:
return SettingsData.getPluginAllowWithoutTrigger(visibilityDelegateItem.modelData.id);
}
}
onToggled: function (isChecked) { onToggled: function (isChecked) {
switch (visibilityDelegateItem.modelData.id) { SettingsData.setPluginAllowWithoutTrigger(visibilityDelegateItem.modelData.id, isChecked);
case "__files":
SettingsData.set("dankLauncherV2IncludeFilesInAll", isChecked);
break;
case "__folders":
SettingsData.set("dankLauncherV2IncludeFoldersInAll", isChecked);
break;
default:
SettingsData.setPluginAllowWithoutTrigger(visibilityDelegateItem.modelData.id, isChecked);
}
} }
} }
} }
@@ -884,24 +840,6 @@ Item {
checked: SettingsData.rememberLastQuery checked: SettingsData.rememberLastQuery
onToggled: checked => SettingsData.set("rememberLastQuery", checked) onToggled: checked => SettingsData.set("rememberLastQuery", checked)
} }
SettingsToggleRow {
settingKey: "dankLauncherV2IncludeFilesInAll"
tags: ["launcher", "files", "dsearch", "all", "results", "indexed"]
text: I18n.tr("Include Files in All Tab")
description: I18n.tr("Merge indexed file results into the All tab (requires dsearch).")
checked: SettingsData.dankLauncherV2IncludeFilesInAll
onToggled: checked => SettingsData.set("dankLauncherV2IncludeFilesInAll", checked)
}
SettingsToggleRow {
settingKey: "dankLauncherV2IncludeFoldersInAll"
tags: ["launcher", "folders", "dirs", "dsearch", "all", "results", "indexed"]
text: I18n.tr("Include Folders in All Tab")
description: I18n.tr("Merge indexed folder results into the All tab (requires dsearch).")
checked: SettingsData.dankLauncherV2IncludeFoldersInAll
onToggled: checked => SettingsData.set("dankLauncherV2IncludeFoldersInAll", checked)
}
} }
SettingsCard { SettingsCard {
@@ -46,13 +46,6 @@ Item {
onToggled: checked => SettingsData.set("audioVisualizerEnabled", checked) onToggled: checked => SettingsData.set("audioVisualizerEnabled", checked)
} }
SettingsToggleRow {
text: I18n.tr("Adaptive Media Width")
description: I18n.tr("Shrink the media widget to fit shorter song titles while still respecting the configured maximum size")
checked: SettingsData.mediaAdaptiveWidthEnabled
onToggled: checked => SettingsData.set("mediaAdaptiveWidthEnabled", checked)
}
SettingsDropdownRow { SettingsDropdownRow {
property var scrollOptsInternal: ["volume", "song", "nothing"] property var scrollOptsInternal: ["volume", "song", "nothing"]
property var scrollOptsDisplay: [I18n.tr("Change Volume", "media scroll wheel option"), I18n.tr("Change Song", "media scroll wheel option"), I18n.tr("Nothing", "media scroll wheel option")] property var scrollOptsDisplay: [I18n.tr("Change Volume", "media scroll wheel option"), I18n.tr("Change Song", "media scroll wheel option"), I18n.tr("Nothing", "media scroll wheel option")]
-10
View File
@@ -91,16 +91,6 @@ Item {
visible: AudioService.gsettingsAvailable visible: AudioService.gsettingsAvailable
} }
SettingsToggleRow {
tab: "sounds"
tags: ["sound", "login", "startup", "boot"]
settingKey: "soundLogin"
text: I18n.tr("Login")
description: I18n.tr("Play sound after logging in")
checked: SettingsData.soundLogin
onToggled: checked => SettingsData.set("soundLogin", checked)
}
SettingsToggleRow { SettingsToggleRow {
tab: "sounds" tab: "sounds"
tags: ["sound", "notification", "new"] tags: ["sound", "notification", "new"]
+5 -16
View File
@@ -11,7 +11,6 @@ import qs.Modules.Settings.Widgets
Item { Item {
id: themeColorsTab id: themeColorsTab
readonly property bool connectedFrameModeActive: SettingsData.connectedFrameModeActive
property var cachedIconThemes: SettingsData.availableIconThemes property var cachedIconThemes: SettingsData.availableIconThemes
property var cachedCursorThemes: SettingsData.availableCursorThemes property var cachedCursorThemes: SettingsData.availableCursorThemes
property var cachedMatugenSchemes: Theme.availableMatugenSchemes.map(option => option.label) property var cachedMatugenSchemes: Theme.availableMatugenSchemes.map(option => option.label)
@@ -1616,14 +1615,10 @@ Item {
SettingsSliderRow { SettingsSliderRow {
tab: "theme" tab: "theme"
tags: ["surface", "popup", "transparency", "opacity", "modal"] tags: ["popup", "transparency", "opacity", "modal"]
settingKey: "popupTransparency" settingKey: "popupTransparency"
text: I18n.tr("Surface Opacity") text: I18n.tr("Popup Transparency")
description: themeColorsTab.connectedFrameModeActive description: I18n.tr("Controls opacity of all popouts, modals, and their content layers")
? I18n.tr("Connected Frame mode follows Surface Opacity from the Frame tab for connected popouts, docks, and modal surfaces")
: I18n.tr("Controls opacity of all popouts, modals, and their content layers")
enabled: !themeColorsTab.connectedFrameModeActive
opacity: themeColorsTab.connectedFrameModeActive ? 0.5 : 1.0
value: Math.round(SettingsData.popupTransparency * 100) value: Math.round(SettingsData.popupTransparency * 100)
minimum: 0 minimum: 0
maximum: 100 maximum: 100
@@ -1637,9 +1632,7 @@ Item {
tags: ["corner", "radius", "rounded", "square"] tags: ["corner", "radius", "rounded", "square"]
settingKey: "cornerRadius" settingKey: "cornerRadius"
text: I18n.tr("Corner Radius") text: I18n.tr("Corner Radius")
description: themeColorsTab.connectedFrameModeActive description: I18n.tr("0 = square corners")
? I18n.tr("Controls general UI rounding. Connected frame popouts, docks, and modal surfaces follow Border Radius in the Frame tab while Connected Frame mode is active")
: I18n.tr("0 = square corners")
value: SettingsData.cornerRadius value: SettingsData.cornerRadius
minimum: 0 minimum: 0
maximum: 32 maximum: 32
@@ -1844,11 +1837,7 @@ Item {
tags: ["blur", "background", "transparency", "glass", "frosted"] tags: ["blur", "background", "transparency", "glass", "frosted"]
settingKey: "blurEnabled" settingKey: "blurEnabled"
text: I18n.tr("Background Blur") text: I18n.tr("Background Blur")
description: !BlurService.available description: BlurService.available ? I18n.tr("Blur the background behind bars, popouts, modals, and notifications. Requires compositor support and configuration.") : I18n.tr("Requires a newer version of Quickshell")
? I18n.tr("Requires a newer version of Quickshell")
: (themeColorsTab.connectedFrameModeActive
? I18n.tr("Connected Frame mode follows Frame Blur for connected surfaces while this remains the master blur availability toggle")
: I18n.tr("Blur the background behind bars, popouts, modals, and notifications. Requires compositor support and configuration."))
checked: SettingsData.blurEnabled ?? false checked: SettingsData.blurEnabled ?? false
enabled: BlurService.available enabled: BlurService.available
onToggled: checked => SettingsData.set("blurEnabled", checked) onToggled: checked => SettingsData.set("blurEnabled", checked)
@@ -55,192 +55,6 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXL spacing: Theme.spacingXL
SettingsCard {
tab: "typography"
tags: ["animation", "variant", "style", "slide", "fluent", "dynamic", "motion"]
title: I18n.tr("Animation Style")
settingKey: "animationVariant"
iconName: "auto_awesome_motion"
Item {
width: parent.width
height: animVariantGroup.implicitHeight
clip: true
DankButtonGroup {
id: animVariantGroup
anchors.horizontalCenter: parent.horizontalCenter
buttonPadding: parent.width < 480 ? Theme.spacingS : Theme.spacingL
minButtonWidth: parent.width < 480 ? 64 : 96
textSize: parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium
model: [I18n.tr("Material"), I18n.tr("Fluent"), I18n.tr("Dynamic")]
selectionMode: "single"
currentIndex: SettingsData.animationVariant
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.set("animationVariant", index);
}
Connections {
target: SettingsData
function onAnimationVariantChanged() {
animVariantGroup.currentIndex = SettingsData.animationVariant;
}
}
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.15
}
Item {
width: parent.width
height: variantDescription.implicitHeight + Theme.spacingS * 2
StyledText {
id: variantDescription
x: Theme.spacingM
y: Theme.spacingS
width: parent.width - Theme.spacingM * 2
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
text: {
switch (SettingsData.animationVariant) {
case 1:
return I18n.tr("Fluent: Smooth cubic deceleration in, quick snap out — clean, elegant curves.");
case 2:
return I18n.tr("Dynamic: Spring bezier with overshoot — entry briefly exceeds its target then settles. Expressive and alive.");
default:
return I18n.tr("Material: Material Design 3 Expressive bezier curves. The DMS default feel.");
}
}
}
}
}
SettingsCard {
tab: "typography"
tags: ["animation", "motion", "effect", "slide", "directional", "depth", "spring", "physics"]
title: I18n.tr("Motion Effects")
settingKey: "motionEffect"
iconName: "motion_photos_on"
Item {
width: parent.width
height: motionEffectGroup.implicitHeight
clip: true
DankButtonGroup {
id: motionEffectGroup
anchors.horizontalCenter: parent.horizontalCenter
buttonPadding: parent.width < 480 ? Theme.spacingS : Theme.spacingL
minButtonWidth: parent.width < 480 ? 64 : 96
textSize: parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium
model: [I18n.tr("Standard"), I18n.tr("Directional"), I18n.tr("Depth")]
selectionMode: "single"
currentIndex: SettingsData.motionEffect
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.set("motionEffect", index);
}
Connections {
target: SettingsData
function onMotionEffectChanged() {
motionEffectGroup.currentIndex = SettingsData.motionEffect;
}
}
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.15
}
Item {
width: parent.width
height: motionEffectDescription.implicitHeight + Theme.spacingS * 2
StyledText {
id: motionEffectDescription
x: Theme.spacingM
y: Theme.spacingS
width: parent.width - Theme.spacingM * 2
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
text: {
switch (SettingsData.motionEffect) {
case 1:
return I18n.tr("Directional: Panels glide in from a larger distance at full size — no scale change, pure clean motion.");
case 2:
return I18n.tr("Depth: Panels scale up from small as they slide in — a dramatic pop-forward depth effect.");
default:
return I18n.tr("Standard: Classic Material Design 3 — panels rise from below with a subtle scale. The DMS default.");
}
}
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.15
visible: SettingsData.motionEffect === 1
}
SettingsDropdownRow {
visible: SettingsData.motionEffect === 1
tab: "typography"
tags: ["animation", "directional", "behavior", "overlap", "sticky", "roll", "connected"]
settingKey: "directionalAnimationMode"
text: I18n.tr("Directional Behavior")
description: {
if (SettingsData.connectedFrameModeActive)
return I18n.tr("Popouts emerge flush from the bar edge as a single continuous piece, with corner connectors bridging the junction");
return I18n.tr("How the popout emerges from the DankBar");
}
options: SettingsData.frameEnabled
? [I18n.tr("Overlap"), I18n.tr("Slide"), I18n.tr("Roll"), I18n.tr("Connected")]
: [I18n.tr("Overlap"), I18n.tr("Slide"), I18n.tr("Roll")]
currentValue: {
switch (SettingsData.directionalAnimationMode) {
case 1:
return I18n.tr("Slide");
case 2:
return I18n.tr("Roll");
case 3:
return SettingsData.frameEnabled ? I18n.tr("Connected") : I18n.tr("Slide");
default:
return I18n.tr("Overlap");
}
}
onValueChanged: value => {
if (value === I18n.tr("Slide"))
SettingsData.set("directionalAnimationMode", 1);
else if (value === I18n.tr("Roll"))
SettingsData.set("directionalAnimationMode", 2);
else if (value === I18n.tr("Connected") && SettingsData.frameEnabled) {
if (SettingsData.directionalAnimationMode !== 3)
SettingsData.set("previousDirectionalMode", SettingsData.directionalAnimationMode);
SettingsData.set("directionalAnimationMode", 3);
} else
SettingsData.set("directionalAnimationMode", 0);
}
}
}
SettingsCard { SettingsCard {
tab: "typography" tab: "typography"
tags: ["font", "family", "text", "typography"] tags: ["font", "family", "text", "typography"]
+1 -3
View File
@@ -430,7 +430,7 @@ Item {
"id": widget.id, "id": widget.id,
"enabled": widget.enabled "enabled": widget.enabled
}; };
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion"]; var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge"];
for (var i = 0; i < keys.length; i++) { for (var i = 0; i < keys.length; i++) {
if (widget[keys[i]] !== undefined) if (widget[keys[i]] !== undefined)
result[keys[i]] = widget[keys[i]]; result[keys[i]] = widget[keys[i]];
@@ -712,8 +712,6 @@ Item {
item.barMaxVisibleRunningApps = widget.barMaxVisibleRunningApps; item.barMaxVisibleRunningApps = widget.barMaxVisibleRunningApps;
if (widget.barShowOverflowBadge !== undefined) if (widget.barShowOverflowBadge !== undefined)
item.barShowOverflowBadge = widget.barShowOverflowBadge; item.barShowOverflowBadge = widget.barShowOverflowBadge;
if (widget.trayUseInlineExpansion !== undefined)
item.trayUseInlineExpansion = widget.trayUseInlineExpansion;
} }
widgets.push(item); widgets.push(item);
}); });
@@ -39,7 +39,7 @@ Column {
"id": widget.id, "id": widget.id,
"enabled": widget.enabled "enabled": widget.enabled
}; };
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion"]; var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge"];
for (var i = 0; i < keys.length; i++) { for (var i = 0; i < keys.length; i++) {
if (widget[keys[i]] !== undefined) if (widget[keys[i]] !== undefined)
result[keys[i]] = widget[keys[i]]; result[keys[i]] = widget[keys[i]];
@@ -437,7 +437,7 @@ Column {
Row { Row {
spacing: Theme.spacingXS spacing: Theme.spacingXS
visible: modelData.id === "clock" || modelData.id === "focusedWindow" || modelData.id === "keyboard_layout_name" || modelData.id === "appsDock" || modelData.id === "systemTray" visible: modelData.id === "clock" || modelData.id === "focusedWindow" || modelData.id === "keyboard_layout_name" || modelData.id === "appsDock"
DankActionButton { DankActionButton {
id: compactModeButton id: compactModeButton
@@ -543,39 +543,6 @@ Column {
} }
} }
DankActionButton {
id: trayMenuButton
buttonSize: 32
visible: modelData.id === "systemTray"
iconName: "more_vert"
iconSize: 18
iconColor: Theme.outline
onClicked: {
trayContextMenu.widgetData = modelData;
trayContextMenu.sectionId = root.sectionId;
trayContextMenu.widgetIndex = index;
var buttonPos = trayMenuButton.mapToItem(root, 0, 0);
var popupWidth = trayContextMenu.width;
var popupHeight = trayContextMenu.height;
var xPos = buttonPos.x - popupWidth - Theme.spacingS;
if (xPos < 0)
xPos = buttonPos.x + trayMenuButton.width + Theme.spacingS;
var yPos = buttonPos.y - popupHeight / 2 + trayMenuButton.height / 2;
if (yPos < 0) {
yPos = Theme.spacingS;
} else if (yPos + popupHeight > root.height) {
yPos = root.height - popupHeight - Theme.spacingS;
}
trayContextMenu.x = xPos;
trayContextMenu.y = yPos;
trayContextMenu.open();
}
}
Rectangle { Rectangle {
id: compactModeTooltip id: compactModeTooltip
width: tooltipText.contentWidth + Theme.spacingM * 2 width: tooltipText.contentWidth + Theme.spacingM * 2
@@ -964,88 +931,6 @@ Column {
} }
} }
Popup {
id: trayContextMenu
property var widgetData: null
property string sectionId: ""
property int widgetIndex: -1
readonly property var currentWidgetData: (widgetIndex >= 0 && widgetIndex < root.items.length) ? root.items[widgetIndex] : widgetData
width: 220
height: contentColumn.implicitHeight + Theme.spacingS * 2
padding: 0
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle {
color: Theme.surfaceContainer
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0
}
contentItem: Item {
Column {
id: contentColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 2
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: trayOverflowArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "arrow_selector_tool"
size: 16
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Use Inline Expansion")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
id: trayOverflowToggle
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
width: 40
height: 20
checked: trayContextMenu.currentWidgetData?.trayUseInlineExpansion ?? false
}
MouseArea {
id: trayOverflowArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
const newValue = !(trayContextMenu.currentWidgetData?.trayUseInlineExpansion ?? false);
root.overflowSettingChanged(trayContextMenu.sectionId, trayContextMenu.widgetIndex, "trayUseInlineExpansion", newValue);
}
}
}
}
}
}
Popup { Popup {
id: diskUsageContextMenu id: diskUsageContextMenu
@@ -121,9 +121,9 @@ Scope {
Behavior on opacity { Behavior on opacity {
NumberAnimation { NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen) duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
} }
} }
@@ -154,69 +154,45 @@ Scope {
id: scaleTransform id: scaleTransform
origin.x: contentContainer.width / 2 origin.x: contentContainer.width / 2
origin.y: contentContainer.height / 2 origin.y: contentContainer.height / 2
xScale: overviewScope.overviewOpen ? 1 : Theme.effectScaleCollapsed xScale: overviewScope.overviewOpen ? 1 : 0.96
yScale: overviewScope.overviewOpen ? 1 : Theme.effectScaleCollapsed yScale: overviewScope.overviewOpen ? 1 : 0.96
Behavior on xScale { Behavior on xScale {
NumberAnimation { NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen) duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
} }
} }
Behavior on yScale { Behavior on yScale {
NumberAnimation { NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen) duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
} }
} }
} }
Translate { Translate {
id: motionTransform id: motionTransform
x: { x: 0
if (overviewScope.overviewOpen) y: overviewScope.overviewOpen ? 0 : Theme.spacingL
return 0;
if (Theme.isDirectionalEffect)
return 0;
if (Theme.isDepthEffect)
return Theme.effectAnimOffset * 0.25;
return 0;
}
y: {
if (overviewScope.overviewOpen)
return 0;
if (Theme.isDirectionalEffect)
return -Math.max(contentContainer.height * 0.8, Theme.effectAnimOffset * 1.1);
if (Theme.isDepthEffect)
return Math.max(Theme.effectAnimOffset * 0.85, 28);
return Theme.effectAnimOffset;
}
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
Behavior on y { Behavior on y {
NumberAnimation { NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen) duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
} }
} }
} }
Behavior on opacity { Behavior on opacity {
NumberAnimation { NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen) duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
} }
} }
@@ -202,18 +202,8 @@ Scope {
Item { Item {
id: spotlightContainer id: spotlightContainer
readonly property bool directionalEffect: Theme.isDirectionalEffect x: Theme.snap((parent.width - width) / 2, overlayWindow.dpr)
readonly property bool depthEffect: Theme.isDepthEffect y: Theme.snap((parent.height - height) / 2, overlayWindow.dpr)
readonly property real collapsedMotionX: depthEffect ? Theme.effectAnimOffset * 0.25 : 0
readonly property real collapsedMotionY: {
if (directionalEffect)
return Math.max(height * 0.85, Theme.effectAnimOffset * 1.1);
if (depthEffect)
return Math.max(Theme.effectAnimOffset * 0.8, 30);
return 0;
}
x: Theme.snap((parent.width - width) / 2 + (overlayWindow.shouldShowSpotlight ? 0 : collapsedMotionX), overlayWindow.dpr)
y: Theme.snap((parent.height - height) / 2 + (overlayWindow.shouldShowSpotlight ? 0 : collapsedMotionY), overlayWindow.dpr)
readonly property int baseWidth: { readonly property int baseWidth: {
switch (SettingsData.dankLauncherV2Size) { switch (SettingsData.dankLauncherV2Size) {
@@ -244,8 +234,8 @@ Scope {
readonly property bool animatingOut: niriOverviewScope.isClosing && overlayWindow.isSpotlightScreen readonly property bool animatingOut: niriOverviewScope.isClosing && overlayWindow.isSpotlightScreen
scale: Theme.isDirectionalEffect ? 1 : (overlayWindow.shouldShowSpotlight ? 1.0 : Theme.effectScaleCollapsed) scale: overlayWindow.shouldShowSpotlight ? 1.0 : 0.96
opacity: Theme.isDirectionalEffect ? 1 : (overlayWindow.shouldShowSpotlight ? 1 : 0) opacity: overlayWindow.shouldShowSpotlight ? 1 : 0
visible: overlayWindow.shouldShowSpotlight || animatingOut visible: overlayWindow.shouldShowSpotlight || animatingOut
enabled: overlayWindow.shouldShowSpotlight enabled: overlayWindow.shouldShowSpotlight
@@ -255,11 +245,10 @@ Scope {
Behavior on scale { Behavior on scale {
id: scaleAnimation id: scaleAnimation
enabled: !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight) duration: Theme.expressiveDurations.fast
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
onRunningChanged: { onRunningChanged: {
if (running || !spotlightContainer.animatingOut) if (running || !spotlightContainer.animatingOut)
return; return;
@@ -269,27 +258,10 @@ Scope {
} }
Behavior on opacity { Behavior on opacity {
enabled: !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight) duration: Theme.expressiveDurations.fast
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
}
}
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight)
easing.type: Easing.BezierSpline
easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
Behavior on y {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight)
easing.type: Easing.BezierSpline
easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
} }
} }
@@ -62,30 +62,30 @@ Item {
Behavior on x { Behavior on x {
NumberAnimation { NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen) duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantModalEnterCurve easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
} }
} }
Behavior on y { Behavior on y {
NumberAnimation { NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen) duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantModalEnterCurve easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
} }
} }
Behavior on width { Behavior on width {
NumberAnimation { NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen) duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantModalEnterCurve easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
} }
} }
Behavior on height { Behavior on height {
NumberAnimation { NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen) duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantModalEnterCurve easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
} }
} }
@@ -124,16 +124,16 @@ Item {
Behavior on width { Behavior on width {
NumberAnimation { NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen) duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantModalEnterCurve easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
} }
} }
Behavior on height { Behavior on height {
NumberAnimation { NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen) duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantModalEnterCurve easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
} }
} }
} }
+2 -56
View File
@@ -26,7 +26,6 @@ Singleton {
property var powerUnplugSound: null property var powerUnplugSound: null
property var normalNotificationSound: null property var normalNotificationSound: null
property var criticalNotificationSound: null property var criticalNotificationSound: null
property var loginSound: null
property real notificationsVolume: 1.0 property real notificationsVolume: 1.0
property bool notificationsAudioMuted: false property bool notificationsAudioMuted: false
@@ -68,16 +67,6 @@ Singleton {
} }
} }
// Used in playLoginSoundIfApplicable()
Process {
id: loginSoundChecker
onExited: (exitCode) => {
if (exitCode === 0) {
playLoginSound();
}
}
}
function getAvailableSinks() { function getAvailableSinks() {
const hidden = SessionData.hiddenOutputDeviceNames ?? []; const hidden = SessionData.hiddenOutputDeviceNames ?? [];
return Pipewire.nodes.values.filter(node => node.audio && node.isSink && !node.isStream && !hidden.includes(node.name)); return Pipewire.nodes.values.filter(node => node.audio && node.isSink && !node.isStream && !hidden.includes(node.name));
@@ -406,7 +395,7 @@ EOFCONFIG
const themesToSearch = themeName !== "freedesktop" ? `${themeName} freedesktop` : themeName; const themesToSearch = themeName !== "freedesktop" ? `${themeName} freedesktop` : themeName;
const script = ` const script = `
for event_key in audio-volume-change power-plug power-unplug message message-new-instant desktop-login; do for event_key in audio-volume-change power-plug power-unplug message message-new-instant; do
found=0 found=0
case "$event_key" in case "$event_key" in
@@ -468,8 +457,7 @@ EOFCONFIG
"power-plug": "../assets/sounds/plasma/power-plug.wav", "power-plug": "../assets/sounds/plasma/power-plug.wav",
"power-unplug": "../assets/sounds/plasma/power-unplug.wav", "power-unplug": "../assets/sounds/plasma/power-unplug.wav",
"message": "../assets/sounds/freedesktop/message.wav", "message": "../assets/sounds/freedesktop/message.wav",
"message-new-instant": "../assets/sounds/freedesktop/message-new-instant.wav", "message-new-instant": "../assets/sounds/freedesktop/message-new-instant.wav"
"desktop-login": "../assets/sounds/freedesktop/desktop-login.wav"
}; };
const specialConditions = { const specialConditions = {
@@ -563,10 +551,6 @@ EOFCONFIG
criticalNotificationSound.destroy(); criticalNotificationSound.destroy();
criticalNotificationSound = null; criticalNotificationSound = null;
} }
if (loginSound) {
loginSound.destroy();
loginSound = null;
}
} }
function createSoundPlayers() { function createSoundPlayers() {
@@ -638,19 +622,6 @@ EOFCONFIG
} }
} }
`, root, "AudioService.CriticalNotificationSound"); `, root, "AudioService.CriticalNotificationSound");
const loginPath = getSoundPath("desktop-login");
loginSound = Qt.createQmlObject(`
import QtQuick
import QtMultimedia
MediaPlayer {
source: "${loginPath}"
audioOutput: AudioOutput {
${deviceProperty}volume: notificationsVolume
}
}
`, root, "AudioService.LoginSound");
} catch (e) { } catch (e) {
console.warn("AudioService: Error creating sound players:", e); console.warn("AudioService: Error creating sound players:", e);
} }
@@ -690,31 +661,6 @@ EOFCONFIG
criticalNotificationSound.play(); criticalNotificationSound.play();
} }
function playLoginSound() {
if (!soundsAvailable || !loginSound || notificationsAudioMuted || isMediaPlaying()) {
return;
}
loginSound.play();
}
function playLoginSoundIfApplicable() {
if (SettingsData.soundsEnabled && SettingsData.soundLogin && !notificationsAudioMuted) {
// plays login sound on session start, but only if a specific file doesn't exist,
// to prevent it from playing on every DMS restart during the session
const runtimeDir = Quickshell.env("XDG_RUNTIME_DIR");
const sessionId = Quickshell.env("XDG_SESSION_ID") || "0";
if (!runtimeDir) return;
const loginFile = `${runtimeDir}/danklinux.login-${sessionId}`;
// if file doesn't exist, touch it (0)
// If it exists, do nothing (1)
loginSoundChecker.command = ["sh", "-c", `[ ! -f ${loginFile} ] && touch ${loginFile}`];
loginSoundChecker.running = true;
}
}
function playVolumeChangeSoundIfEnabled() { function playVolumeChangeSoundIfEnabled() {
if (SettingsData.soundsEnabled && SettingsData.soundVolumeChanged && !notificationsAudioMuted) { if (SettingsData.soundsEnabled && SettingsData.soundVolumeChanged && !notificationsAudioMuted) {
playVolumeChangeSound(); playVolumeChangeSound();
-6
View File
@@ -255,12 +255,6 @@ Singleton {
return pinnedEntries.some(pinnedEntry => pinnedEntry.hash === entryHash); return pinnedEntries.some(pinnedEntry => pinnedEntry.hash === entryHash);
} }
onClipboardAvailableChanged: {
if (!clipboardAvailable || refCount <= 0)
return;
refresh();
}
Connections { Connections {
target: DMSService target: DMSService
enabled: root.refCount > 0 enabled: root.refCount > 0
+1 -2
View File
@@ -138,8 +138,7 @@ Singleton {
} }
} }
const procId = "dsearch-search-" + (params?.type || "all"); Proc.runCommand("dsearch-search", args, (stdout, exitCode) => {
Proc.runCommand(procId, args, (stdout, exitCode) => {
if (exitCode === 0) { if (exitCode === 0) {
try { try {
const response = JSON.parse(stdout); const response = JSON.parse(stdout);
+24 -100
View File
@@ -819,7 +819,6 @@ Singleton {
if (event.event === "unlock" || event.event === "resume") { if (event.event === "unlock" || event.event === "resume") {
suppressOsd = true; suppressOsd = true;
osdSuppressTimer.restart(); osdSuppressTimer.restart();
evaluateNightMode();
} }
} }
@@ -1033,6 +1032,7 @@ Singleton {
target: "brightness" target: "brightness"
} }
// IPC Handler for night mode control
IpcHandler { IpcHandler {
function toggle(): string { function toggle(): string {
root.toggleNightMode(); root.toggleNightMode();
@@ -1050,119 +1050,43 @@ Singleton {
} }
function status(): string { function status(): string {
if (!root.gammaControlAvailable) return root.nightModeEnabled ? "Night mode is enabled" : "Night mode is disabled";
return "Night mode: unavailable (no gamma control)"; }
const parts = ["Night mode: " + (root.nightModeEnabled ? "enabled" : "disabled")]; function temperature(value: string): string {
if (!value) {
if (root.gammaCurrentTemp > 0) return "Current temperature: " + SessionData.nightModeTemperature + "K";
parts.push("Current temperature: " + root.gammaCurrentTemp + "K");
parts.push("Target night temperature: " + SessionData.nightModeTemperature + "K");
if (SessionData.nightModeAutoEnabled) {
parts.push("Target day temperature: " + SessionData.nightModeHighTemperature + "K");
parts.push("Automation: " + SessionData.nightModeAutoMode);
parts.push("Period: " + (root.gammaIsDay ? "day" : "night"));
if (root.gammaNextTransition)
parts.push("Next transition: " + root.gammaNextTransition);
if (root.gammaSunriseTime)
parts.push("Sunrise: " + root.gammaSunriseTime);
if (root.gammaSunsetTime)
parts.push("Sunset: " + root.gammaSunsetTime);
} }
return parts.join("\n");
}
function getCurrentTemp(): string {
if (!root.gammaControlAvailable)
return "Gamma control not available";
if (root.gammaCurrentTemp <= 0)
return "No current temperature reported";
return root.gammaCurrentTemp.toString();
}
function getTargetTemp(): string {
return SessionData.nightModeTemperature.toString();
}
function getDayTemp(): string {
return SessionData.nightModeHighTemperature.toString();
}
function setTargetTemp(value: string): string {
if (!value)
return "Usage: night setTargetTemp <2500-6000>";
const temp = parseInt(value); const temp = parseInt(value);
if (isNaN(temp)) if (isNaN(temp)) {
return "Invalid temperature: " + value; return "Invalid temperature. Use a value between 2500 and 6000 (in steps of 500)";
if (temp < 2500 || temp > 6000) }
return "Temperature must be between 2500K and 6000K";
// Validate temperature is in valid range and steps
if (temp < 2500 || temp > 6000) {
return "Temperature must be between 2500K and 6000K";
}
// Round to nearest 500
const rounded = Math.round(temp / 500) * 500; const rounded = Math.round(temp / 500) * 500;
SessionData.setNightModeTemperature(rounded); SessionData.setNightModeTemperature(rounded);
// Restart night mode with new temperature if active
if (root.nightModeEnabled) { if (root.nightModeEnabled) {
switch (true) { if (SessionData.nightModeAutoEnabled) {
case SessionData.nightModeAutoEnabled:
root.startAutomation(); root.startAutomation();
break; } else {
default:
root.applyNightModeDirectly(); root.applyNightModeDirectly();
break;
} }
} }
if (rounded !== temp) if (rounded !== temp) {
return "Night temperature set to " + rounded + "K (rounded from " + temp + "K)"; return "Night mode temperature set to " + rounded + "K (rounded from " + temp + "K)";
return "Night temperature set to " + rounded + "K"; } else {
} return "Night mode temperature set to " + rounded + "K";
}
function setDayTemp(value: string): string {
if (!value)
return "Usage: night setDayTemp <2500-6500>";
const temp = parseInt(value);
if (isNaN(temp))
return "Invalid temperature: " + value;
if (temp < 2500 || temp > 6500)
return "Temperature must be between 2500K and 6500K";
const rounded = Math.round(temp / 500) * 500;
SessionData.setNightModeHighTemperature(rounded);
if (root.nightModeEnabled && SessionData.nightModeAutoEnabled)
root.startAutomation();
if (rounded !== temp)
return "Day temperature set to " + rounded + "K (rounded from " + temp + "K)";
return "Day temperature set to " + rounded + "K";
}
function getSchedule(): string {
if (!SessionData.nightModeAutoEnabled)
return "Automation disabled";
const parts = ["Mode: " + SessionData.nightModeAutoMode];
parts.push("Period: " + (root.gammaIsDay ? "day" : "night"));
if (root.gammaDawnTime)
parts.push("Dawn: " + root.gammaDawnTime);
if (root.gammaSunriseTime)
parts.push("Sunrise: " + root.gammaSunriseTime);
if (root.gammaSunsetTime)
parts.push("Sunset: " + root.gammaSunsetTime);
if (root.gammaNightTime)
parts.push("Night: " + root.gammaNightTime);
if (root.gammaNextTransition)
parts.push("Next transition: " + root.gammaNextTransition);
if (root.gammaSunPosition > 0)
parts.push("Sun position: " + root.gammaSunPosition.toFixed(2) + "°");
return parts.join("\n");
} }
target: "night" target: "night"
+1 -66
View File
@@ -4,75 +4,10 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import qs.Common
Singleton { Singleton {
id: root id: root
readonly property list<MprisPlayer> availablePlayers: Mpris.players.values readonly property list<MprisPlayer> availablePlayers: Mpris.players.values
property MprisPlayer activePlayer: null property MprisPlayer activePlayer: availablePlayers.find(p => p.isPlaying) ?? availablePlayers.find(p => p.canControl && p.canPlay) ?? null
onAvailablePlayersChanged: _resolveActivePlayer()
Component.onCompleted: _resolveActivePlayer()
Instantiator {
model: root.availablePlayers
delegate: Connections {
required property MprisPlayer modelData
target: modelData
function onIsPlayingChanged() {
if (modelData.isPlaying)
root._resolveActivePlayer();
}
}
}
function _resolveActivePlayer(): void {
const playing = availablePlayers.find(p => p.isPlaying);
if (playing) {
activePlayer = playing;
_persistIdentity(playing.identity);
return;
}
if (activePlayer && availablePlayers.indexOf(activePlayer) >= 0)
return;
const savedId = SessionData.lastPlayerIdentity;
if (savedId) {
const match = availablePlayers.find(p => p.identity === savedId);
if (match) {
activePlayer = match;
return;
}
}
activePlayer = availablePlayers.find(p => p.canControl && p.canPlay) ?? null;
if (activePlayer)
_persistIdentity(activePlayer.identity);
}
function setActivePlayer(player: MprisPlayer): void {
activePlayer = player;
if (player)
_persistIdentity(player.identity);
}
function _persistIdentity(identity: string): void {
if (identity && SessionData.lastPlayerIdentity !== identity)
SessionData.set("lastPlayerIdentity", identity);
}
Timer {
interval: 1000
running: root.activePlayer?.playbackState === MprisPlaybackState.Playing
repeat: true
onTriggered: root.activePlayer?.positionChanged()
}
function previousOrRewind(): void {
if (!activePlayer)
return;
if (activePlayer.position > 8 && activePlayer.canSeek)
activePlayer.position = 0;
else if (activePlayer.canGoPrevious)
activePlayer.previous();
}
} }
+2 -14
View File
@@ -6,7 +6,6 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Services.Pipewire import Quickshell.Services.Pipewire
import qs.Services
Singleton { Singleton {
id: root id: root
@@ -59,10 +58,6 @@ Singleton {
} }
readonly property bool screensharingActive: { readonly property bool screensharingActive: {
if (CompositorService.isNiri && NiriService.hasActiveCast) {
return true
}
if (!Pipewire.ready || !Pipewire.nodes?.values) { if (!Pipewire.ready || !Pipewire.nodes?.values) {
return false return false
} }
@@ -79,12 +74,6 @@ Singleton {
} }
} }
if (node.properties && node.properties["media.class"] === "Stream/Output/Video") {
if (looksLikeScreencast(node)) {
return true
}
}
if (node.properties && node.properties["media.class"] === "Stream/Input/Audio") { if (node.properties && node.properties["media.class"] === "Stream/Input/Audio") {
const mediaName = (node.properties["media.name"] || "").toLowerCase() const mediaName = (node.properties["media.name"] || "").toLowerCase()
const appName = (node.properties["application.name"] || "").toLowerCase() const appName = (node.properties["application.name"] || "").toLowerCase()
@@ -121,9 +110,8 @@ Singleton {
} }
const appName = (node.properties && node.properties["application.name"] || "").toLowerCase() const appName = (node.properties && node.properties["application.name"] || "").toLowerCase()
const nodeName = (node.name || "").toLowerCase() const nodeName = (node.name || "").toLowerCase()
const mediaName = (node.properties && node.properties["media.name"] || "").toLowerCase() const combined = appName + " " + nodeName
const combined = appName + " " + nodeName + " " + mediaName return /xdg-desktop-portal|xdpw|screencast|screen|gnome shell|kwin|obs/.test(combined)
return /xdg-desktop-portal|xdpw|screencast|screen-cast|screen|gnome shell|kwin|obs|niri/.test(combined)
} }
function getMicrophoneStatus() { function getMicrophoneStatus() {
+1 -4
View File
@@ -231,10 +231,7 @@ Singleton {
return; return;
isChecking = true; isChecking = true;
hasError = false; hasError = false;
if (pkgManager === "paru" || pkgManager === "yay") { if (updChecker.length > 0) {
const repoCmd = updChecker.length > 0 ? updChecker : `${pkgManager} -Qu`;
updateChecker.command = ["sh", "-c", `(${repoCmd} 2>/dev/null; ${pkgManager} -Qua 2>/dev/null) || true`];
} else if (updChecker.length > 0) {
updateChecker.command = [updChecker].concat(updateCheckerParams[updChecker].listUpdatesSettings.params); updateChecker.command = [updChecker].concat(updateCheckerParams[updChecker].listUpdatesSettings.params);
} else { } else {
updateChecker.command = [pkgManager].concat(packageManagerParams[pkgManager].listUpdatesSettings.params); updateChecker.command = [pkgManager].concat(packageManagerParams[pkgManager].listUpdatesSettings.params);
-159
View File
@@ -1,159 +0,0 @@
import QtQuick
import QtQuick.Shapes
import qs.Common
// Concave arc connector filling the gap between a bar corner and an adjacent surface.
//
// NOTE: FrameWindow now uses ConnectedShape.qml for frame-owned connected chrome
// (unified single-path rendering). This component is still used by DankPopout's
// own shadow source for non-frame-owned chrome (popouts on non-frame screens).
Item {
id: root
property string barSide: "top"
property string placement: "left"
property real spacing: 4
property real connectorRadius: 12
property color color: "transparent"
property real edgeStrokeWidth: 0
property color edgeStrokeColor: color
property real dpr: 1
readonly property bool isHorizontalBar: barSide === "top" || barSide === "bottom"
readonly property bool isPlacementLeft: placement === "left"
readonly property real _edgeStrokeWidth: Math.max(0, edgeStrokeWidth)
readonly property string arcCorner: {
if (barSide === "top")
return isPlacementLeft ? "bottomLeft" : "bottomRight";
if (barSide === "bottom")
return isPlacementLeft ? "topLeft" : "topRight";
if (barSide === "left")
return isPlacementLeft ? "topRight" : "bottomRight";
return isPlacementLeft ? "topLeft" : "bottomLeft";
}
readonly property real pathStartX: {
switch (arcCorner) {
case "topLeft":
return width;
case "topRight":
case "bottomLeft":
return 0;
default:
return 0;
}
}
readonly property real pathStartY: {
switch (arcCorner) {
case "bottomRight":
return height;
default:
return 0;
}
}
readonly property real firstLineX: {
switch (arcCorner) {
case "topLeft":
case "bottomLeft":
return width;
default:
return 0;
}
}
readonly property real firstLineY: {
switch (arcCorner) {
case "topLeft":
case "topRight":
return height;
default:
return 0;
}
}
readonly property real secondLineX: {
switch (arcCorner) {
case "topRight":
case "bottomLeft":
case "bottomRight":
return width;
default:
return 0;
}
}
readonly property real secondLineY: {
switch (arcCorner) {
case "topLeft":
case "topRight":
case "bottomLeft":
return height;
default:
return 0;
}
}
readonly property real arcCenterX: arcCorner === "topRight" || arcCorner === "bottomRight" ? width : 0
readonly property real arcCenterY: arcCorner === "bottomLeft" || arcCorner === "bottomRight" ? height : 0
readonly property real arcStartAngle: {
switch (arcCorner) {
case "topLeft":
case "topRight":
return 90;
case "bottomLeft":
return 0;
default:
return -90;
}
}
readonly property real arcSweepAngle: {
switch (arcCorner) {
case "topRight":
return 90;
default:
return -90;
}
}
width: isHorizontalBar ? connectorRadius : (spacing + connectorRadius)
height: isHorizontalBar ? (spacing + connectorRadius) : connectorRadius
Shape {
x: -root._edgeStrokeWidth
y: -root._edgeStrokeWidth
width: root.width + root._edgeStrokeWidth * 2
height: root.height + root._edgeStrokeWidth * 2
asynchronous: false
antialiasing: true
preferredRendererType: Shape.CurveRenderer
layer.enabled: true
layer.smooth: true
layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0)
ShapePath {
fillColor: root.color
strokeColor: root._edgeStrokeWidth > 0 ? root.edgeStrokeColor : "transparent"
strokeWidth: root._edgeStrokeWidth * 2
joinStyle: ShapePath.RoundJoin
capStyle: ShapePath.RoundCap
fillRule: ShapePath.WindingFill
startX: root.pathStartX + root._edgeStrokeWidth
startY: root.pathStartY + root._edgeStrokeWidth
PathLine {
x: root.firstLineX + root._edgeStrokeWidth
y: root.firstLineY + root._edgeStrokeWidth
}
PathLine {
x: root.secondLineX + root._edgeStrokeWidth
y: root.secondLineY + root._edgeStrokeWidth
}
PathAngleArc {
centerX: root.arcCenterX + root._edgeStrokeWidth
centerY: root.arcCenterY + root._edgeStrokeWidth
radiusX: root.connectorRadius
radiusY: root.connectorRadius
startAngle: root.arcStartAngle
sweepAngle: root.arcSweepAngle
}
}
}
}
-287
View File
@@ -1,287 +0,0 @@
import QtQuick
import QtQuick.Shapes
import qs.Common
// Unified connected silhouette: body + concave arcs as one ShapePath.
// PathArc pattern 4 arcs + 4 lines, no sibling alignment.
Item {
id: root
property string barSide: "top"
property real bodyWidth: 0
property real bodyHeight: 0
property real connectorRadius: 12
property real surfaceRadius: 12
property color fillColor: "transparent"
// Derived layout
readonly property bool _horiz: barSide === "top" || barSide === "bottom"
readonly property real _cr: Math.max(0, connectorRadius)
readonly property real _sr: Math.max(0, Math.min(surfaceRadius, (_horiz ? bodyWidth : bodyHeight) / 2, (_horiz ? bodyHeight : bodyWidth) / 2))
// Root-level aliases PathArc/PathLine elements can't use `parent`.
readonly property real _bw: bodyWidth
readonly property real _bh: bodyHeight
readonly property real _totalW: _horiz ? _bw + _cr * 2 : _bw
readonly property real _totalH: _horiz ? _bh : _bh + _cr * 2
width: _totalW
height: _totalH
readonly property real bodyX: _horiz ? _cr : 0
readonly property real bodyY: _horiz ? 0 : _cr
Shape {
anchors.fill: parent
asynchronous: false
preferredRendererType: Shape.CurveRenderer
antialiasing: true
ShapePath {
fillColor: root.fillColor
strokeWidth: -1
fillRule: ShapePath.WindingFill
// CW path: bar edge concave arc body convex arc far edge convex arc body concave arc
startX: root.barSide === "right" ? root._totalW : 0
startY: {
switch (root.barSide) {
case "bottom":
return root._totalH;
case "left":
return root._totalH;
case "right":
return 0;
default:
return 0;
}
}
// Bar edge
PathLine {
x: {
switch (root.barSide) {
case "left":
return 0;
case "right":
return root._totalW;
default:
return root._totalW;
}
}
y: {
switch (root.barSide) {
case "bottom":
return root._totalH;
case "left":
return 0;
case "right":
return root._totalH;
default:
return 0;
}
}
}
// Concave arc 1
PathArc {
relativeX: {
switch (root.barSide) {
case "left":
return root._cr;
case "right":
return -root._cr;
default:
return -root._cr;
}
}
relativeY: {
switch (root.barSide) {
case "bottom":
return -root._cr;
case "left":
return root._cr;
case "right":
return -root._cr;
default:
return root._cr;
}
}
radiusX: root._cr
radiusY: root._cr
direction: root.barSide === "bottom" ? PathArc.Clockwise : PathArc.Counterclockwise
}
// Body edge to first convex corner
PathLine {
x: {
switch (root.barSide) {
case "left":
return root._bw - root._sr;
case "right":
return root._sr;
default:
return root._totalW - root._cr;
}
}
y: {
switch (root.barSide) {
case "bottom":
return root._sr;
case "left":
return root._cr;
case "right":
return root._cr + root._bh;
default:
return root._totalH - root._sr;
}
}
}
// Convex arc 1
PathArc {
relativeX: {
switch (root.barSide) {
case "left":
return root._sr;
case "right":
return -root._sr;
default:
return -root._sr;
}
}
relativeY: {
switch (root.barSide) {
case "bottom":
return -root._sr;
case "left":
return root._sr;
case "right":
return -root._sr;
default:
return root._sr;
}
}
radiusX: root._sr
radiusY: root._sr
direction: root.barSide === "bottom" ? PathArc.Counterclockwise : PathArc.Clockwise
}
// Far edge
PathLine {
x: {
switch (root.barSide) {
case "left":
return root._bw;
case "right":
return 0;
default:
return root._cr + root._sr;
}
}
y: {
switch (root.barSide) {
case "bottom":
return 0;
case "left":
return root._cr + root._bh - root._sr;
case "right":
return root._cr + root._sr;
default:
return root._totalH;
}
}
}
// Convex arc 2
PathArc {
relativeX: {
switch (root.barSide) {
case "left":
return -root._sr;
case "right":
return root._sr;
default:
return -root._sr;
}
}
relativeY: {
switch (root.barSide) {
case "bottom":
return root._sr;
case "left":
return root._sr;
case "right":
return -root._sr;
default:
return -root._sr;
}
}
radiusX: root._sr
radiusY: root._sr
direction: root.barSide === "bottom" ? PathArc.Counterclockwise : PathArc.Clockwise
}
// Body edge to second concave arc
PathLine {
x: {
switch (root.barSide) {
case "left":
return root._cr;
case "right":
return root._bw - root._cr;
default:
return root._cr;
}
}
y: {
switch (root.barSide) {
case "bottom":
return root._totalH - root._cr;
case "left":
return root._cr + root._bh;
case "right":
return root._cr;
default:
return root._cr;
}
}
}
// Concave arc 2
PathArc {
relativeX: {
switch (root.barSide) {
case "left":
return -root._cr;
case "right":
return root._cr;
default:
return -root._cr;
}
}
relativeY: {
switch (root.barSide) {
case "bottom":
return root._cr;
case "left":
return root._cr;
case "right":
return -root._cr;
default:
return -root._cr;
}
}
radiusX: root._cr
radiusY: root._cr
direction: root.barSide === "bottom" ? PathArc.Clockwise : PathArc.Counterclockwise
}
}
}
}
+105 -657
View File
@@ -20,10 +20,10 @@ Item {
property string triggerSection: "" property string triggerSection: ""
property string positioning: "center" property string positioning: "center"
property int animationDuration: Theme.popoutAnimationDuration property int animationDuration: Theme.popoutAnimationDuration
property real animationScaleCollapsed: Theme.effectScaleCollapsed property real animationScaleCollapsed: 0.96
property real animationOffset: Theme.effectAnimOffset property real animationOffset: Theme.spacingL
property list<real> animationEnterCurve: Theme.variantPopoutEnterCurve property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
property list<real> animationExitCurve: Theme.variantPopoutExitCurve property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
property bool suspendShadowWhileResizing: false property bool suspendShadowWhileResizing: false
property bool shouldBeVisible: false property bool shouldBeVisible: false
property var customKeyboardFocus: null property var customKeyboardFocus: null
@@ -34,10 +34,6 @@ Item {
property bool _resizeActive: false property bool _resizeActive: false
property real _surfaceMarginLeft: 0 property real _surfaceMarginLeft: 0
property real _surfaceW: 0 property real _surfaceW: 0
property string _chromeClaimId: ""
property int _connectedChromeSerial: 0
property real _chromeAnimTravelX: 1
property real _chromeAnimTravelY: 1
property real storedBarThickness: Theme.barHeight - 4 property real storedBarThickness: Theme.barHeight - 4
property real storedBarSpacing: 4 property real storedBarSpacing: 4
@@ -51,8 +47,6 @@ Item {
property var screen: null property var screen: null
readonly property real effectiveBarThickness: { readonly property real effectiveBarThickness: {
if (Theme.isConnectedEffect)
return Math.max(0, storedBarThickness);
const padding = storedBarConfig ? (storedBarConfig.innerPadding !== undefined ? storedBarConfig.innerPadding : 4) : 4; const padding = storedBarConfig ? (storedBarConfig.innerPadding !== undefined ? storedBarConfig.innerPadding : 4) : 4;
return Math.max(26 + padding * 0.6, Theme.barHeight - 4 - (8 - padding)) + storedBarSpacing; return Math.max(26 + padding * 0.6, Theme.barHeight - 4 - (8 - padding)) + storedBarSpacing;
} }
@@ -74,14 +68,12 @@ Item {
readonly property real barWidth: barBounds.width readonly property real barWidth: barBounds.width
readonly property real barHeight: barBounds.height readonly property real barHeight: barBounds.height
readonly property real barWingSize: barBounds.wingSize readonly property real barWingSize: barBounds.wingSize
readonly property bool effectiveSurfaceBlurEnabled: Theme.connectedSurfaceBlurEnabled
signal opened signal opened
signal popoutClosed signal popoutClosed
signal backgroundClicked signal backgroundClicked
property var _lastOpenedScreen: null property var _lastOpenedScreen: null
property bool isClosing: false
property int effectiveBarPosition: 0 property int effectiveBarPosition: 0
property real effectiveBarBottomGap: 0 property real effectiveBarBottomGap: 0
@@ -155,156 +147,7 @@ Item {
setBarContext(pos, bottomGap); setBarContext(pos, bottomGap);
} }
function _nextChromeClaimId() {
_connectedChromeSerial += 1;
return layerNamespace + ":" + _connectedChromeSerial + ":" + (new Date()).getTime();
}
function _captureChromeAnimTravel() {
_chromeAnimTravelX = Math.max(1, Math.abs(contentContainer.offsetX));
_chromeAnimTravelY = Math.max(1, Math.abs(contentContainer.offsetY));
}
function _connectedChromeAnimX() {
const barSide = contentContainer.connectedBarSide;
if (barSide !== "left" && barSide !== "right")
return contentContainer.animX;
const extent = Math.max(0, root.alignedWidth);
const progress = Math.min(1, Math.abs(contentContainer.animX) / Math.max(1, _chromeAnimTravelX));
const offset = Theme.snap(extent * progress, root.dpr);
return contentContainer.animX < 0 ? -offset : offset;
}
function _connectedChromeAnimY() {
const barSide = contentContainer.connectedBarSide;
if (barSide !== "top" && barSide !== "bottom")
return contentContainer.animY;
const extent = Math.max(0, root.alignedHeight);
const progress = Math.min(1, Math.abs(contentContainer.animY) / Math.max(1, _chromeAnimTravelY));
const offset = Theme.snap(extent * progress, root.dpr);
return contentContainer.animY < 0 ? -offset : offset;
}
function _connectedChromeState(visibleOverride) {
const visible = visibleOverride !== undefined ? !!visibleOverride : contentWindow.visible;
return {
"visible": visible,
"barSide": contentContainer.connectedBarSide,
"bodyX": root.alignedX,
"bodyY": root.alignedY,
"bodyW": root.alignedWidth,
"bodyH": root.alignedHeight,
"animX": _connectedChromeAnimX(),
"animY": _connectedChromeAnimY(),
"screen": root.screen ? root.screen.name : ""
};
}
function _publishConnectedChromeState(forceClaim, visibleOverride) {
if (!root.frameOwnsConnectedChrome || !root.screen || !_chromeClaimId)
return;
const state = _connectedChromeState(visibleOverride);
if (forceClaim || !ConnectedModeState.hasPopoutOwner(_chromeClaimId)) {
ConnectedModeState.claimPopout(_chromeClaimId, state);
} else {
ConnectedModeState.updatePopout(_chromeClaimId, state);
}
}
function _releaseConnectedChromeState() {
if (_chromeClaimId)
ConnectedModeState.releasePopout(_chromeClaimId);
_chromeClaimId = "";
}
// Exposed animation state for ConnectedModeState
readonly property real contentAnimX: contentContainer.animX
readonly property real contentAnimY: contentContainer.animY
property bool _fullSyncPending: false
// ConnectedModeState sync
function _syncPopoutChromeState() {
if (!root.frameOwnsConnectedChrome) {
_releaseConnectedChromeState();
return;
}
if (!root.screen) {
_releaseConnectedChromeState();
return;
}
if (!contentWindow.visible && !shouldBeVisible)
return;
if (!_chromeClaimId)
_chromeClaimId = _nextChromeClaimId();
_publishConnectedChromeState(contentWindow.visible && !ConnectedModeState.hasPopoutOwner(_chromeClaimId));
}
function _syncPopoutAnim(axis) {
if (!root.frameOwnsConnectedChrome || !_chromeClaimId)
return;
if (!contentWindow.visible && !shouldBeVisible)
return;
const barSide = contentContainer.connectedBarSide;
const syncX = axis === "x" && (barSide === "left" || barSide === "right");
const syncY = axis === "y" && (barSide === "top" || barSide === "bottom");
if (!syncX && !syncY)
return;
ConnectedModeState.setPopoutAnim(_chromeClaimId, syncX ? _connectedChromeAnimX() : undefined, syncY ? _connectedChromeAnimY() : undefined);
}
function _flushFullSync() {
_fullSyncPending = false;
_syncPopoutChromeState();
}
function _queueFullSync() {
if (_fullSyncPending)
return;
_fullSyncPending = true;
Qt.callLater(root._flushFullSync);
}
onAlignedXChanged: _queueFullSync()
onAlignedYChanged: _queueFullSync()
onAlignedWidthChanged: _queueFullSync()
onContentAnimXChanged: _syncPopoutAnim("x")
onContentAnimYChanged: _syncPopoutAnim("y")
onScreenChanged: _syncPopoutChromeState()
onEffectiveBarPositionChanged: _syncPopoutChromeState()
Connections {
target: contentWindow
function onVisibleChanged() {
if (contentWindow.visible)
root._publishConnectedChromeState(true);
else
root._releaseConnectedChromeState();
}
}
Connections {
target: SettingsData
function onConnectedFrameModeActiveChanged() {
if (root.frameOwnsConnectedChrome) {
if (contentWindow.visible || root.shouldBeVisible) {
if (!root._chromeClaimId)
root._chromeClaimId = root._nextChromeClaimId();
root._publishConnectedChromeState(true);
}
} else {
root._releaseConnectedChromeState();
}
}
}
readonly property bool useBackgroundWindow: !CompositorService.isHyprland || CompositorService.useHyprlandFocusGrab readonly property bool useBackgroundWindow: !CompositorService.isHyprland || CompositorService.useHyprlandFocusGrab
readonly property bool frameOwnsConnectedChrome: SettingsData.connectedFrameModeActive
&& !!root.screen
&& SettingsData.isScreenInPreferences(root.screen, SettingsData.frameScreenPreferences)
function updateSurfacePosition() { function updateSurfacePosition() {
if (useBackgroundWindow && shouldBeVisible) { if (useBackgroundWindow && shouldBeVisible) {
@@ -313,15 +156,10 @@ Item {
} }
} }
property bool animationsEnabled: true
function open() { function open() {
if (!screen) if (!screen)
return; return;
closeTimer.stop(); closeTimer.stop();
isClosing = false;
animationsEnabled = false;
_primeContent = true;
// Snapshot mask geometry // Snapshot mask geometry
_frozenMaskX = maskX; _frozenMaskX = maskX;
@@ -336,30 +174,12 @@ Item {
} }
_lastOpenedScreen = screen; _lastOpenedScreen = screen;
if (contentContainer) { shouldBeVisible = true;
contentContainer.animX = Theme.snap(contentContainer.offsetX, root.dpr);
contentContainer.animY = Theme.snap(contentContainer.offsetY, root.dpr);
contentContainer.scaleValue = root.animationScaleCollapsed;
_captureChromeAnimTravel();
}
if (root.frameOwnsConnectedChrome) {
_chromeClaimId = _nextChromeClaimId();
_publishConnectedChromeState(true, true);
} else {
_chromeClaimId = "";
}
if (useBackgroundWindow) { if (useBackgroundWindow) {
_surfaceMarginLeft = alignedX - shadowBuffer; _surfaceMarginLeft = alignedX - shadowBuffer;
_surfaceW = alignedWidth + shadowBuffer * 2; _surfaceW = alignedWidth + shadowBuffer * 2;
backgroundWindow.visible = true;
} }
contentWindow.visible = true;
Qt.callLater(() => { Qt.callLater(() => {
animationsEnabled = true;
shouldBeVisible = true;
if (shouldBeVisible && screen) { if (shouldBeVisible && screen) {
if (useBackgroundWindow) if (useBackgroundWindow)
backgroundWindow.visible = true; backgroundWindow.visible = true;
@@ -371,7 +191,6 @@ Item {
} }
function close() { function close() {
isClosing = true;
shouldBeVisible = false; shouldBeVisible = false;
_primeContent = false; _primeContent = false;
PopoutManager.popoutChanged(); PopoutManager.popoutChanged();
@@ -403,10 +222,9 @@ Item {
Timer { Timer {
id: closeTimer id: closeTimer
interval: Theme.variantCloseInterval(animationDuration) interval: animationDuration
onTriggered: { onTriggered: {
if (!shouldBeVisible) { if (!shouldBeVisible) {
isClosing = false;
contentWindow.visible = false; contentWindow.visible = false;
if (useBackgroundWindow) if (useBackgroundWindow)
backgroundWindow.visible = false; backgroundWindow.visible = false;
@@ -416,80 +234,19 @@ Item {
} }
} }
Component.onDestruction: _releaseConnectedChromeState()
readonly property real screenWidth: screen ? screen.width : 0 readonly property real screenWidth: screen ? screen.width : 0
readonly property real screenHeight: screen ? screen.height : 0 readonly property real screenHeight: screen ? screen.height : 0
readonly property real dpr: screen ? screen.devicePixelRatio : 1 readonly property real dpr: screen ? screen.devicePixelRatio : 1
readonly property real frameInset: {
if (!SettingsData.frameEnabled)
return 0;
const ft = SettingsData.frameThickness;
const fr = SettingsData.frameRounding;
const ccr = Theme.connectedCornerRadius;
if (Theme.isConnectedEffect)
return Math.max(ft * 4, ft + ccr * 2);
const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true;
const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 6;
const gap = useAutoGaps ? Math.max(6, storedBarSpacing) : manualGapValue;
return Math.max(ft + gap, fr);
}
readonly property var shadowLevel: Theme.elevationLevel3 readonly property var shadowLevel: Theme.elevationLevel3
readonly property real shadowFallbackOffset: 6 readonly property real shadowFallbackOffset: 6
readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.popoutElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, effectiveShadowDirection, shadowFallbackOffset, 8, 16) : 0 readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.popoutElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, effectiveShadowDirection, shadowFallbackOffset, 8, 16) : 0
readonly property real shadowMotionPadding: { readonly property real shadowMotionPadding: Math.max(0, animationOffset)
if (Theme.isConnectedEffect)
return Math.max(storedBarSpacing + Theme.connectedCornerRadius + 4, 40);
if (Theme.isDirectionalEffect) {
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode !== 0)
return 16; // Slide Behind and Roll Out do not add animationOffset, enabling strict Wayland clipping.
return Math.max(0, animationOffset) + 16;
}
if (Theme.isDepthEffect)
return Math.max(0, animationOffset) + 8;
return Math.max(0, animationOffset);
}
readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr) readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
readonly property real alignedWidth: Theme.px(popupWidth, dpr) readonly property real alignedWidth: Theme.px(popupWidth, dpr)
readonly property real alignedHeight: Theme.px(popupHeight, dpr) readonly property real alignedHeight: Theme.px(popupHeight, dpr)
readonly property real connectedAnchorX: {
if (!Theme.isConnectedEffect)
return triggerX;
switch (effectiveBarPosition) {
case SettingsData.Position.Left:
return barX + barWidth;
case SettingsData.Position.Right:
return barX;
default:
return triggerX;
}
}
readonly property real connectedAnchorY: {
if (!Theme.isConnectedEffect)
return triggerY;
switch (effectiveBarPosition) {
case SettingsData.Position.Top:
return barY + barHeight;
case SettingsData.Position.Bottom:
return barY;
default:
return triggerY;
}
}
function adjacentBarClearance(exclusion) {
if (exclusion <= 0)
return 0;
if (!Theme.isConnectedEffect)
return exclusion;
// In a shared frame corner, the adjacent connected bar already occupies
// one rounded-corner radius before the popout's own connector begins.
return exclusion + Theme.connectedCornerRadius * 2;
}
onAlignedHeightChanged: { onAlignedHeightChanged: {
_syncPopoutChromeState();
if (!suspendShadowWhileResizing || !shouldBeVisible) if (!suspendShadowWhileResizing || !shouldBeVisible)
return; return;
_resizeActive = true; _resizeActive = true;
@@ -512,22 +269,17 @@ Item {
readonly property real alignedX: Theme.snap((() => { readonly property real alignedX: Theme.snap((() => {
const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true; const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true;
const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 4; const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 4;
const rawPopupGap = useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue; const popupGap = useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue;
const popupGap = Theme.isConnectedEffect ? 0 : rawPopupGap;
const edgeGap = Math.max(popupGap, frameInset);
const anchorX = Theme.isConnectedEffect ? connectedAnchorX : triggerX;
switch (effectiveBarPosition) { switch (effectiveBarPosition) {
case SettingsData.Position.Left: case SettingsData.Position.Left:
// bar on left: left side is bar-adjacent (popupGap), right side is frame-perpendicular (edgeGap) return Math.max(popupGap, Math.min(screenWidth - popupWidth - popupGap, triggerX));
return Math.max(popupGap, Math.min(screenWidth - popupWidth - edgeGap, anchorX));
case SettingsData.Position.Right: case SettingsData.Position.Right:
// bar on right: right side is bar-adjacent (popupGap), left side is frame-perpendicular (edgeGap) return Math.max(popupGap, Math.min(screenWidth - popupWidth - popupGap, triggerX - popupWidth));
return Math.max(edgeGap, Math.min(screenWidth - popupWidth - popupGap, anchorX - popupWidth));
default: default:
const rawX = triggerX + (triggerWidth / 2) - (popupWidth / 2); const rawX = triggerX + (triggerWidth / 2) - (popupWidth / 2);
const minX = Math.max(edgeGap, adjacentBarClearance(adjacentBarInfo.leftBar)); const minX = adjacentBarInfo.leftBar > 0 ? adjacentBarInfo.leftBar : popupGap;
const maxX = screenWidth - popupWidth - Math.max(edgeGap, adjacentBarClearance(adjacentBarInfo.rightBar)); const maxX = screenWidth - popupWidth - (adjacentBarInfo.rightBar > 0 ? adjacentBarInfo.rightBar : popupGap);
return Math.max(minX, Math.min(maxX, rawX)); return Math.max(minX, Math.min(maxX, rawX));
} }
})(), dpr) })(), dpr)
@@ -535,22 +287,17 @@ Item {
readonly property real alignedY: Theme.snap((() => { readonly property real alignedY: Theme.snap((() => {
const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true; const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true;
const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 4; const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 4;
const rawPopupGap = useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue; const popupGap = useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue;
const popupGap = Theme.isConnectedEffect ? 0 : rawPopupGap;
const edgeGap = Math.max(popupGap, frameInset);
const anchorY = Theme.isConnectedEffect ? connectedAnchorY : triggerY;
switch (effectiveBarPosition) { switch (effectiveBarPosition) {
case SettingsData.Position.Bottom: case SettingsData.Position.Bottom:
// bar on bottom: bottom side is bar-adjacent (popupGap), top side is frame-perpendicular (edgeGap) return Math.max(popupGap, Math.min(screenHeight - popupHeight - popupGap, triggerY - popupHeight));
return Math.max(edgeGap, Math.min(screenHeight - popupHeight - popupGap, anchorY - popupHeight));
case SettingsData.Position.Top: case SettingsData.Position.Top:
// bar on top: top side is bar-adjacent (popupGap), bottom side is frame-perpendicular (edgeGap) return Math.max(popupGap, Math.min(screenHeight - popupHeight - popupGap, triggerY));
return Math.max(popupGap, Math.min(screenHeight - popupHeight - edgeGap, anchorY));
default: default:
const rawY = triggerY - (popupHeight / 2); const rawY = triggerY - (popupHeight / 2);
const minY = Math.max(edgeGap, adjacentBarClearance(adjacentBarInfo.topBar)); const minY = adjacentBarInfo.topBar > 0 ? adjacentBarInfo.topBar : popupGap;
const maxY = screenHeight - popupHeight - Math.max(edgeGap, adjacentBarClearance(adjacentBarInfo.bottomBar)); const maxY = screenHeight - popupHeight - (adjacentBarInfo.bottomBar > 0 ? adjacentBarInfo.bottomBar : popupGap);
return Math.max(minY, Math.min(maxY, rawY)); return Math.max(minY, Math.min(maxY, rawY));
} }
})(), dpr) })(), dpr)
@@ -606,10 +353,6 @@ Item {
mask: Region { mask: Region {
item: maskRect item: maskRect
Region {
item: contentExclusionRect
intersection: Intersection.Subtract
}
} }
Rectangle { Rectangle {
@@ -618,70 +361,26 @@ Item {
color: "transparent" color: "transparent"
x: root._frozenMaskX x: root._frozenMaskX
y: root._frozenMaskY y: root._frozenMaskY
width: (backgroundWindow.visible && backgroundInteractive) ? root._frozenMaskWidth : 0 width: (shouldBeVisible && backgroundInteractive) ? root._frozenMaskWidth : 0
height: (backgroundWindow.visible && backgroundInteractive) ? root._frozenMaskHeight : 0 height: (shouldBeVisible && backgroundInteractive) ? root._frozenMaskHeight : 0
} }
Item { MouseArea {
id: contentExclusionRect
visible: false
x: root.alignedX
y: root.alignedY
width: root.alignedWidth
height: root.alignedHeight
}
Item {
id: outsideClickCatcher
x: root._frozenMaskX x: root._frozenMaskX
y: root._frozenMaskY y: root._frozenMaskY
width: root._frozenMaskWidth width: root._frozenMaskWidth
height: root._frozenMaskHeight height: root._frozenMaskHeight
enabled: root.shouldBeVisible && root.backgroundInteractive hoverEnabled: false
enabled: shouldBeVisible && backgroundInteractive
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: mouse => {
const clickX = mouse.x + root._frozenMaskX;
const clickY = mouse.y + root._frozenMaskY;
const outsideContent = clickX < root.alignedX || clickX > root.alignedX + root.alignedWidth || clickY < root.alignedY || clickY > root.alignedY + root.alignedHeight;
readonly property real contentLeft: Math.max(0, root.alignedX - x) if (!outsideContent)
readonly property real contentTop: Math.max(0, root.alignedY - y) return;
readonly property real contentRight: Math.min(width, contentLeft + root.alignedWidth) backgroundClicked();
readonly property real contentBottom: Math.min(height, contentTop + root.alignedHeight)
MouseArea {
x: 0
y: 0
width: outsideClickCatcher.width
height: Math.max(0, outsideClickCatcher.contentTop)
enabled: parent.enabled
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: root.backgroundClicked()
}
MouseArea {
x: 0
y: outsideClickCatcher.contentBottom
width: outsideClickCatcher.width
height: Math.max(0, outsideClickCatcher.height - outsideClickCatcher.contentBottom)
enabled: parent.enabled
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: root.backgroundClicked()
}
MouseArea {
x: 0
y: outsideClickCatcher.contentTop
width: Math.max(0, outsideClickCatcher.contentLeft)
height: Math.max(0, outsideClickCatcher.contentBottom - outsideClickCatcher.contentTop)
enabled: parent.enabled
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: root.backgroundClicked()
}
MouseArea {
x: outsideClickCatcher.contentRight
y: outsideClickCatcher.contentTop
width: Math.max(0, outsideClickCatcher.width - outsideClickCatcher.contentRight)
height: Math.max(0, outsideClickCatcher.contentBottom - outsideClickCatcher.contentTop)
enabled: parent.enabled
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: root.backgroundClicked()
} }
} }
@@ -702,21 +401,12 @@ Item {
WindowBlur { WindowBlur {
id: popoutBlur id: popoutBlur
targetWindow: contentWindow targetWindow: contentWindow
blurEnabled: root.effectiveSurfaceBlurEnabled && !root.frameOwnsConnectedChrome
readonly property real s: Math.min(1, contentContainer.scaleValue) readonly property real s: Math.min(1, contentContainer.scaleValue)
readonly property bool trackBlurFromBarEdge: Theme.isConnectedEffect || (typeof SettingsData !== "undefined" && Theme.isDirectionalEffect && SettingsData.directionalAnimationMode !== 2) blurX: contentContainer.x + contentContainer.width * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr)
blurY: contentContainer.y + contentContainer.height * (1 - s) * 0.5 + Theme.snap(contentContainer.animY, root.dpr)
// Directional popouts clip to the bar edge, so the blur needs to grow from blurWidth: (shouldBeVisible && contentWrapper.opacity > 0) ? contentContainer.width * s : 0
// that same edge instead of translating through the bar before settling. blurHeight: (shouldBeVisible && contentWrapper.opacity > 0) ? contentContainer.height * s : 0
readonly property real _dyClamp: (contentContainer.barTop || contentContainer.barBottom) ? Math.max(-contentContainer.height, Math.min(contentContainer.animY, contentContainer.height)) : 0 blurRadius: Theme.cornerRadius
readonly property real _dxClamp: (contentContainer.barLeft || contentContainer.barRight) ? Math.max(-contentContainer.width, Math.min(contentContainer.animX, contentContainer.width)) : 0
blurX: trackBlurFromBarEdge ? contentContainer.x + (contentContainer.barRight ? _dxClamp : 0) : contentContainer.x + contentContainer.width * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr) - contentContainer.horizontalConnectorExtent * s
blurY: trackBlurFromBarEdge ? contentContainer.y + (contentContainer.barBottom ? _dyClamp : 0) : contentContainer.y + contentContainer.height * (1 - s) * 0.5 + Theme.snap(contentContainer.animY, root.dpr) - contentContainer.verticalConnectorExtent * s
blurWidth: (shouldBeVisible && contentWrapper.opacity > 0) ? (trackBlurFromBarEdge ? Math.max(0, contentContainer.width - Math.abs(_dxClamp)) : (contentContainer.width + contentContainer.horizontalConnectorExtent * 2) * s) : 0
blurHeight: (shouldBeVisible && contentWrapper.opacity > 0) ? (trackBlurFromBarEdge ? Math.max(0, contentContainer.height - Math.abs(_dyClamp)) : (contentContainer.height + contentContainer.verticalConnectorExtent * 2) * s) : 0
blurRadius: Theme.isConnectedEffect ? Theme.connectedCornerRadius : Theme.connectedSurfaceRadius
} }
WlrLayershell.namespace: root.layerNamespace WlrLayershell.namespace: root.layerNamespace
@@ -746,6 +436,7 @@ Item {
} }
readonly property bool _fullHeight: useBackgroundWindow && root.fullHeightSurface readonly property bool _fullHeight: useBackgroundWindow && root.fullHeightSurface
anchors { anchors {
left: true left: true
top: true top: true
@@ -771,10 +462,10 @@ Item {
Item { Item {
id: contentMaskRect id: contentMaskRect
visible: false visible: false
x: contentContainer.x - contentContainer.horizontalConnectorExtent x: contentContainer.x
y: contentContainer.y - contentContainer.verticalConnectorExtent y: contentContainer.y
width: root.alignedWidth + contentContainer.horizontalConnectorExtent * 2 width: shouldBeVisible ? root.alignedWidth : 0
height: root.alignedHeight + contentContainer.verticalConnectorExtent * 2 height: shouldBeVisible ? root.alignedHeight : 0
} }
MouseArea { MouseArea {
@@ -803,354 +494,111 @@ Item {
readonly property bool barBottom: effectiveBarPosition === SettingsData.Position.Bottom readonly property bool barBottom: effectiveBarPosition === SettingsData.Position.Bottom
readonly property bool barLeft: effectiveBarPosition === SettingsData.Position.Left readonly property bool barLeft: effectiveBarPosition === SettingsData.Position.Left
readonly property bool barRight: effectiveBarPosition === SettingsData.Position.Right readonly property bool barRight: effectiveBarPosition === SettingsData.Position.Right
readonly property string connectedBarSide: barTop ? "top" : (barBottom ? "bottom" : (barLeft ? "left" : "right")) readonly property real offsetX: barLeft ? root.animationOffset : (barRight ? -root.animationOffset : 0)
readonly property real surfaceRadius: Theme.connectedSurfaceRadius readonly property real offsetY: barBottom ? -root.animationOffset : (barTop ? root.animationOffset : 0)
readonly property color surfaceColor: Theme.popupLayerColor(Theme.surfaceContainer)
readonly property color surfaceBorderColor: Theme.isConnectedEffect ? "transparent" : (BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium)
readonly property real surfaceBorderWidth: Theme.isConnectedEffect ? 0 : BlurService.borderWidth
readonly property real surfaceTopLeftRadius: Theme.isConnectedEffect && (barTop || barLeft) ? 0 : surfaceRadius
readonly property real surfaceTopRightRadius: Theme.isConnectedEffect && (barTop || barRight) ? 0 : surfaceRadius
readonly property real surfaceBottomLeftRadius: Theme.isConnectedEffect && (barBottom || barLeft) ? 0 : surfaceRadius
readonly property real surfaceBottomRightRadius: Theme.isConnectedEffect && (barBottom || barRight) ? 0 : surfaceRadius
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
readonly property real directionalTravelX: Math.max(root.animationOffset, root.alignedWidth + Theme.spacingL)
readonly property real directionalTravelY: Math.max(root.animationOffset, root.alignedHeight + Theme.spacingL)
readonly property real depthTravel: Math.max(root.animationOffset * 0.7, 28)
readonly property real sectionTilt: (triggerSection === "left" ? -1 : (triggerSection === "right" ? 1 : 0))
readonly property real horizontalConnectorExtent: Theme.isConnectedEffect && (barTop || barBottom) ? Theme.connectedCornerRadius : 0
readonly property real verticalConnectorExtent: Theme.isConnectedEffect && (barLeft || barRight) ? Theme.connectedCornerRadius : 0
function connectorWidth(spacing) {
return (barTop || barBottom) ? Theme.connectedCornerRadius : (spacing + Theme.connectedCornerRadius);
}
function connectorHeight(spacing) {
return (barTop || barBottom) ? (spacing + Theme.connectedCornerRadius) : Theme.connectedCornerRadius;
}
function connectorSeamX(baseX, bodyWidth, placement) {
if (barTop || barBottom)
return placement === "left" ? baseX : baseX + bodyWidth;
return barLeft ? baseX : baseX + bodyWidth;
}
function connectorSeamY(baseY, bodyHeight, placement) {
if (barTop)
return baseY;
if (barBottom)
return baseY + bodyHeight;
return placement === "left" ? baseY : baseY + bodyHeight;
}
function connectorX(baseX, bodyWidth, placement, spacing) {
const seamX = connectorSeamX(baseX, bodyWidth, placement);
const width = connectorWidth(spacing);
if (barTop || barBottom)
return placement === "left" ? seamX - width : seamX;
return barLeft ? seamX : seamX - width;
}
function connectorY(baseY, bodyHeight, placement, spacing) {
const seamY = connectorSeamY(baseY, bodyHeight, placement);
const height = connectorHeight(spacing);
if (barTop)
return seamY;
if (barBottom)
return seamY - height;
return placement === "left" ? seamY - height : seamY;
}
readonly property real offsetX: {
if (directionalEffect) {
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2)
return 0;
if (barLeft)
return -directionalTravelX;
if (barRight)
return directionalTravelX;
if (barTop || barBottom)
return 0;
return sectionTilt * directionalTravelX * 0.2;
}
if (depthEffect) {
if (barLeft)
return -depthTravel;
if (barRight)
return depthTravel;
if (barTop || barBottom)
return 0;
return sectionTilt * depthTravel * 0.2;
}
return barLeft ? root.animationOffset : (barRight ? -root.animationOffset : 0);
}
readonly property real offsetY: {
if (directionalEffect) {
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2)
return 0;
if (barBottom)
return directionalTravelY;
if (barTop)
return -directionalTravelY;
if (barLeft || barRight)
return 0;
return directionalTravelY;
}
if (depthEffect) {
if (barBottom)
return depthTravel;
if (barTop)
return -depthTravel;
if (barLeft || barRight)
return 0;
return depthTravel;
}
return barBottom ? -root.animationOffset : (barTop ? root.animationOffset : 0);
}
property real animX: 0 property real animX: 0
property real animY: 0 property real animY: 0
property real scaleValue: root.animationScaleCollapsed
readonly property real computedScaleCollapsed: (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) ? 0.0 : root.animationScaleCollapsed onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
property real scaleValue: computedScaleCollapsed onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
Component.onCompleted: {
animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr);
animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr);
scaleValue = root.shouldBeVisible ? 1.0 : computedScaleCollapsed;
root._captureChromeAnimTravel();
}
onOffsetXChanged: {
if (!root.shouldBeVisible)
animX = Theme.snap(offsetX, root.dpr);
}
onOffsetYChanged: {
if (!root.shouldBeVisible)
animY = Theme.snap(offsetY, root.dpr);
}
Connections { Connections {
target: root target: root
function onShouldBeVisibleChanged() { function onShouldBeVisibleChanged() {
root._captureChromeAnimTravel();
contentContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetX, root.dpr); contentContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetX, root.dpr);
contentContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetY, root.dpr); contentContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetY, root.dpr);
contentContainer.scaleValue = root.shouldBeVisible ? 1.0 : contentContainer.computedScaleCollapsed; contentContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed;
} }
} }
Behavior on animX { Behavior on animX {
enabled: root.animationsEnabled
NumberAnimation { NumberAnimation {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible) duration: root.animationDuration
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
Behavior on animY { Behavior on animY {
enabled: root.animationsEnabled
NumberAnimation { NumberAnimation {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible) duration: root.animationDuration
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
Behavior on scaleValue { Behavior on scaleValue {
enabled: root.animationsEnabled
NumberAnimation { NumberAnimation {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible) duration: root.animationDuration
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
ElevationShadow {
id: shadowSource
width: parent.width
height: parent.height
opacity: contentWrapper.opacity
scale: contentWrapper.scale
x: contentWrapper.x
y: contentWrapper.y
level: root.shadowLevel
direction: root.effectiveShadowDirection
fallbackOffset: root.shadowFallbackOffset
targetRadius: Theme.cornerRadius
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !(root.suspendShadowWhileResizing && root._resizeActive)
}
Item { Item {
id: directionalClipMask id: contentWrapper
anchors.centerIn: parent
width: parent.width
height: parent.height
opacity: shouldBeVisible ? 1 : 0
visible: opacity > 0
scale: contentContainer.scaleValue
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
readonly property bool shouldClip: (Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0) || Theme.isConnectedEffect layer.enabled: contentWrapper.opacity < 1
readonly property real clipOversize: 1000 layer.smooth: false
readonly property real connectedClipAllowance: { layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0)
if (!Theme.isConnectedEffect) return 0;
if (root.frameOwnsConnectedChrome) return 0; Behavior on opacity {
return -Theme.connectedCornerRadius; NumberAnimation {
duration: animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
} }
clip: shouldClip Loader {
id: contentLoader
// Bound the clipping strictly to the bar side, allowing massive overflow on the other 3 sides for shadows anchors.fill: parent
x: shouldClip ? (contentContainer.barLeft ? -connectedClipAllowance : -clipOversize) : 0 active: root._primeContent || shouldBeVisible || contentWindow.visible
y: shouldClip ? (contentContainer.barTop ? -connectedClipAllowance : -clipOversize) : 0 asynchronous: false
width: {
if (!shouldClip)
return parent.width;
if (contentContainer.barLeft)
return parent.width + connectedClipAllowance + clipOversize;
if (contentContainer.barRight)
return parent.width + clipOversize + connectedClipAllowance;
return parent.width + clipOversize * 2;
}
height: {
if (!shouldClip)
return parent.height;
if (contentContainer.barTop)
return parent.height + connectedClipAllowance + clipOversize;
if (contentContainer.barBottom)
return parent.height + clipOversize + connectedClipAllowance;
return parent.height + clipOversize * 2;
} }
}
Item { Rectangle {
id: aligner width: parent.width
readonly property real baseWidth: contentContainer.width height: parent.height
readonly property real baseHeight: contentContainer.height x: contentWrapper.x
readonly property bool isRollOut: typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect y: contentWrapper.y
opacity: contentWrapper.opacity
x: (directionalClipMask.x !== 0 ? -directionalClipMask.x : 0) + (isRollOut && contentContainer.barRight ? baseWidth * (1 - contentContainer.scaleValue) : 0) scale: contentWrapper.scale
y: (directionalClipMask.y !== 0 ? -directionalClipMask.y : 0) + (isRollOut && contentContainer.barBottom ? baseHeight * (1 - contentContainer.scaleValue) : 0) visible: contentWrapper.visible
width: isRollOut && (contentContainer.barLeft || contentContainer.barRight) ? Math.max(0, baseWidth * contentContainer.scaleValue) : baseWidth radius: Theme.cornerRadius
height: isRollOut && (contentContainer.barTop || contentContainer.barBottom) ? Math.max(0, baseHeight * contentContainer.scaleValue) : baseHeight color: "transparent"
border.color: BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium
clip: isRollOut border.width: BlurService.borderWidth
z: 100
Item { }
id: unrollCounteract }
x: aligner.isRollOut && contentContainer.barRight ? -(aligner.baseWidth * (1 - contentContainer.scaleValue)) : 0
y: aligner.isRollOut && contentContainer.barBottom ? -(aligner.baseHeight * (1 - contentContainer.scaleValue)) : 0
width: aligner.baseWidth
height: aligner.baseHeight
ElevationShadow {
id: shadowSource
readonly property real connectorExtent: Theme.isConnectedEffect ? Theme.connectedCornerRadius : 0
readonly property real extraLeft: Theme.isConnectedEffect && (contentContainer.barTop || contentContainer.barBottom) ? connectorExtent : 0
readonly property real extraRight: Theme.isConnectedEffect && (contentContainer.barTop || contentContainer.barBottom) ? connectorExtent : 0
readonly property real extraTop: Theme.isConnectedEffect && (contentContainer.barLeft || contentContainer.barRight) ? connectorExtent : 0
readonly property real extraBottom: Theme.isConnectedEffect && (contentContainer.barLeft || contentContainer.barRight) ? connectorExtent : 0
readonly property real bodyX: extraLeft
readonly property real bodyY: extraTop
readonly property real bodyWidth: parent.width
readonly property real bodyHeight: parent.height
width: parent.width + extraLeft + extraRight
height: parent.height + extraTop + extraBottom
opacity: contentWrapper.opacity
scale: contentWrapper.scale
x: contentWrapper.x - extraLeft
y: contentWrapper.y - extraTop
level: root.shadowLevel
direction: root.effectiveShadowDirection
fallbackOffset: root.shadowFallbackOffset
targetRadius: contentContainer.surfaceRadius
topLeftRadius: contentContainer.surfaceTopLeftRadius
topRightRadius: contentContainer.surfaceTopRightRadius
bottomLeftRadius: contentContainer.surfaceBottomLeftRadius
bottomRightRadius: contentContainer.surfaceBottomRightRadius
targetColor: contentContainer.surfaceColor
borderColor: contentContainer.surfaceBorderColor
borderWidth: contentContainer.surfaceBorderWidth
useCustomSource: Theme.isConnectedEffect
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !(root.suspendShadowWhileResizing && root._resizeActive) && !root.frameOwnsConnectedChrome
Item {
anchors.fill: parent
visible: Theme.isConnectedEffect && !root.frameOwnsConnectedChrome
clip: false
Rectangle {
x: shadowSource.bodyX
y: shadowSource.bodyY
width: shadowSource.bodyWidth
height: shadowSource.bodyHeight
topLeftRadius: contentContainer.surfaceTopLeftRadius
topRightRadius: contentContainer.surfaceTopRightRadius
bottomLeftRadius: contentContainer.surfaceBottomLeftRadius
bottomRightRadius: contentContainer.surfaceBottomRightRadius
color: contentContainer.surfaceColor
}
ConnectedCorner {
visible: Theme.isConnectedEffect
barSide: contentContainer.connectedBarSide
placement: "left"
spacing: 0
connectorRadius: Theme.connectedCornerRadius
color: contentContainer.surfaceColor
dpr: root.dpr
x: Theme.snap(contentContainer.connectorX(shadowSource.bodyX, shadowSource.bodyWidth, placement, spacing), root.dpr)
y: Theme.snap(contentContainer.connectorY(shadowSource.bodyY, shadowSource.bodyHeight, placement, spacing), root.dpr)
}
ConnectedCorner {
visible: Theme.isConnectedEffect
barSide: contentContainer.connectedBarSide
placement: "right"
spacing: 0
connectorRadius: Theme.connectedCornerRadius
color: contentContainer.surfaceColor
dpr: root.dpr
x: Theme.snap(contentContainer.connectorX(shadowSource.bodyX, shadowSource.bodyWidth, placement, spacing), root.dpr)
y: Theme.snap(contentContainer.connectorY(shadowSource.bodyY, shadowSource.bodyHeight, placement, spacing), root.dpr)
}
}
}
Item {
id: contentWrapper
width: parent.width
height: parent.height
opacity: Theme.isDirectionalEffect ? 1 : (shouldBeVisible ? 1 : 0)
visible: opacity > 0
scale: aligner.isRollOut ? 1.0 : contentContainer.scaleValue
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - scale) * 0.5, root.dpr)
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - scale) * 0.5, root.dpr)
layer.enabled: contentWrapper.opacity < 1
layer.smooth: false
layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0)
Behavior on opacity {
enabled: !Theme.isDirectionalEffect
NumberAnimation {
duration: Math.round(Theme.variantDuration(animationDuration, shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Item {
anchors.fill: parent
clip: false
visible: !Theme.isConnectedEffect
Rectangle {
anchors.fill: parent
antialiasing: true
topLeftRadius: contentContainer.surfaceTopLeftRadius
topRightRadius: contentContainer.surfaceTopRightRadius
bottomLeftRadius: contentContainer.surfaceBottomLeftRadius
bottomRightRadius: contentContainer.surfaceBottomRightRadius
color: contentContainer.surfaceColor
border.color: contentContainer.surfaceBorderColor
border.width: contentContainer.surfaceBorderWidth
}
}
Loader {
id: contentLoader
anchors.fill: parent
active: root._primeContent || shouldBeVisible || contentWindow.visible
asynchronous: false
}
} // closes contentWrapper
} // closes unrollCounteract
} // closes aligner
} // closes directionalClipMask
} // closes contentContainer
Item { Item {
id: focusHelper id: focusHelper
+81 -161
View File
@@ -8,122 +8,13 @@ Item {
id: root id: root
property MprisPlayer activePlayer property MprisPlayer activePlayer
property real seekPreviewRatio: -1 property real value: {
readonly property real playerValue: { if (!activePlayer || activePlayer.length <= 0) return 0
if (!activePlayer || activePlayer.length <= 0) const pos = (activePlayer.position || 0) % Math.max(1, activePlayer.length)
return 0; const calculatedRatio = pos / activePlayer.length
const pos = (activePlayer.position || 0) % Math.max(1, activePlayer.length); return Math.max(0, Math.min(1, calculatedRatio))
const calculatedRatio = pos / activePlayer.length;
return Math.max(0, Math.min(1, calculatedRatio));
} }
property real value: seekPreviewRatio >= 0 ? seekPreviewRatio : playerValue
property bool isSeeking: false property bool isSeeking: false
property bool isDraggingSeek: false
property real committedSeekRatio: -1
property int previewSettleChecksRemaining: 0
property real dragThreshold: 4
property int holdIndicatorDelay: 180
function clampRatio(ratio) {
return Math.max(0, Math.min(1, ratio));
}
function ratioForPosition(position) {
if (!activePlayer || activePlayer.length <= 0)
return 0;
return clampRatio(position / activePlayer.length);
}
function positionForRatio(ratio) {
if (!activePlayer || activePlayer.length <= 0)
return 0;
const rawPosition = clampRatio(ratio) * activePlayer.length;
return Math.min(rawPosition, activePlayer.length * 0.99);
}
function updatePreviewFromMouse(mouseX, width) {
if (!activePlayer || activePlayer.length <= 0 || width <= 0)
return;
seekPreviewRatio = clampRatio(mouseX / width);
}
function clearCommittedSeekPreview() {
previewSettleTimer.stop();
committedSeekRatio = -1;
previewSettleChecksRemaining = 0;
if (!isSeeking)
seekPreviewRatio = -1;
}
function beginCommittedSeekPreview(position) {
seekPreviewRatio = ratioForPosition(position);
committedSeekRatio = seekPreviewRatio;
previewSettleChecksRemaining = 15;
previewSettleTimer.restart();
}
function handleSeekPressed(mouse, width, mouseArea, holdTimer) {
isSeeking = true;
isDraggingSeek = false;
mouseArea.pressX = mouse.x;
clearCommittedSeekPreview();
holdTimer.restart();
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
updatePreviewFromMouse(mouse.x, width);
mouseArea.pendingSeekPosition = positionForRatio(seekPreviewRatio);
}
}
function handleSeekReleased(mouseArea, holdTimer) {
holdTimer.stop();
isSeeking = false;
isDraggingSeek = false;
if (mouseArea.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
const clamped = Math.min(mouseArea.pendingSeekPosition, activePlayer.length * 0.99);
activePlayer.position = clamped;
mouseArea.pendingSeekPosition = -1;
beginCommittedSeekPreview(clamped);
} else {
seekPreviewRatio = -1;
}
}
function handleSeekPositionChanged(mouse, width, mouseArea) {
if (mouseArea.pressed && isSeeking && activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
if (!isDraggingSeek && Math.abs(mouse.x - mouseArea.pressX) >= dragThreshold)
isDraggingSeek = true;
updatePreviewFromMouse(mouse.x, width);
mouseArea.pendingSeekPosition = positionForRatio(seekPreviewRatio);
}
}
function handleSeekCanceled(mouseArea, holdTimer) {
holdTimer.stop();
isSeeking = false;
isDraggingSeek = false;
mouseArea.pendingSeekPosition = -1;
clearCommittedSeekPreview();
}
Timer {
id: previewSettleTimer
interval: 80
repeat: true
onTriggered: {
if (root.isSeeking || root.committedSeekRatio < 0) {
stop();
return;
}
const previewSettled = Math.abs(root.playerValue - root.committedSeekRatio) <= 0.0015;
if (previewSettled || root.previewSettleChecksRemaining <= 0) {
root.clearCommittedSeekPreview();
return;
}
root.previewSettleChecksRemaining -= 1;
}
}
implicitHeight: 20 implicitHeight: 20
@@ -138,35 +29,58 @@ Item {
M3WaveProgress { M3WaveProgress {
value: root.value value: root.value
actualValue: root.playerValue
showActualPlaybackState: root.isSeeking
actualProgressColor: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.45)
isPlaying: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing isPlaying: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
MouseArea { MouseArea {
id: waveMouseArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0 enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0
property real pendingSeekPosition: -1 property real pendingSeekPosition: -1
property real pressX: 0
Timer { Timer {
id: waveHoldIndicatorTimer id: waveSeekDebounceTimer
interval: root.holdIndicatorDelay interval: 150
repeat: false
onTriggered: { onTriggered: {
if (parent.pressed && root.isSeeking) if (parent.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
root.isDraggingSeek = true; const clamped = Math.min(parent.pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
parent.pendingSeekPosition = -1
}
} }
} }
onPressed: mouse => root.handleSeekPressed(mouse, parent.width, waveMouseArea, waveHoldIndicatorTimer) onPressed: (mouse) => {
onReleased: root.handleSeekReleased(waveMouseArea, waveHoldIndicatorTimer) root.isSeeking = true
onPositionChanged: mouse => root.handleSeekPositionChanged(mouse, parent.width, waveMouseArea) if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
onCanceled: root.handleSeekCanceled(waveMouseArea, waveHoldIndicatorTimer) const r = Math.max(0, Math.min(1, mouse.x / parent.width))
pendingSeekPosition = r * activePlayer.length
waveSeekDebounceTimer.restart()
}
}
onReleased: {
root.isSeeking = false
waveSeekDebounceTimer.stop()
if (pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
pendingSeekPosition = -1
}
}
onPositionChanged: (mouse) => {
if (pressed && root.isSeeking && activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
pendingSeekPosition = r * activePlayer.length
waveSeekDebounceTimer.restart()
}
}
onClicked: (mouse) => {
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
activePlayer.position = r * activePlayer.length
}
}
} }
} }
} }
@@ -179,7 +93,6 @@ Item {
property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40) property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40)
property color fillColor: Theme.primary property color fillColor: Theme.primary
property color playheadColor: Theme.primary property color playheadColor: Theme.primary
property color actualProgressColor: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.45)
readonly property real midY: height / 2 readonly property real midY: height / 2
Rectangle { Rectangle {
@@ -197,22 +110,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color: parent.fillColor color: parent.fillColor
radius: height / 2 radius: height / 2
Behavior on width { Behavior on width { NumberAnimation { duration: 80 } }
NumberAnimation {
duration: 80
}
}
}
Rectangle {
visible: root.isDraggingSeek
width: 2
height: Math.max(parent.lineWidth + 4, 10)
radius: width / 2
color: parent.actualProgressColor
x: Math.max(0, Math.min(parent.width, parent.width * root.playerValue)) - width / 2
y: parent.midY - height / 2
z: 2
} }
Rectangle { Rectangle {
@@ -224,37 +122,59 @@ Item {
x: Math.max(0, Math.min(parent.width, parent.width * root.value)) - width / 2 x: Math.max(0, Math.min(parent.width, parent.width * root.value)) - width / 2
y: parent.midY - height / 2 y: parent.midY - height / 2
z: 3 z: 3
Behavior on x { Behavior on x { NumberAnimation { duration: 80 } }
NumberAnimation {
duration: 80
}
}
} }
MouseArea { MouseArea {
id: flatMouseArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0 enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0
property real pendingSeekPosition: -1 property real pendingSeekPosition: -1
property real pressX: 0
Timer { Timer {
id: flatHoldIndicatorTimer id: flatSeekDebounceTimer
interval: root.holdIndicatorDelay interval: 150
repeat: false
onTriggered: { onTriggered: {
if (parent.pressed && root.isSeeking) if (parent.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
root.isDraggingSeek = true; const clamped = Math.min(parent.pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
parent.pendingSeekPosition = -1
}
} }
} }
onPressed: mouse => root.handleSeekPressed(mouse, parent.width, flatMouseArea, flatHoldIndicatorTimer) onPressed: (mouse) => {
onReleased: root.handleSeekReleased(flatMouseArea, flatHoldIndicatorTimer) root.isSeeking = true
onPositionChanged: mouse => root.handleSeekPositionChanged(mouse, parent.width, flatMouseArea) if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
onCanceled: root.handleSeekCanceled(flatMouseArea, flatHoldIndicatorTimer) const r = Math.max(0, Math.min(1, mouse.x / parent.width))
pendingSeekPosition = r * activePlayer.length
flatSeekDebounceTimer.restart()
}
}
onReleased: {
root.isSeeking = false
flatSeekDebounceTimer.stop()
if (pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
pendingSeekPosition = -1
}
}
onPositionChanged: (mouse) => {
if (pressed && root.isSeeking && activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
pendingSeekPosition = r * activePlayer.length
flatSeekDebounceTimer.restart()
}
}
onClicked: (mouse) => {
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
activePlayer.position = r * activePlayer.length
}
}
} }
} }
} }
+5 -75
View File
@@ -6,8 +6,6 @@ Item {
id: root id: root
property real value: 0 property real value: 0
property real actualValue: value
property bool showActualPlaybackState: false
property real lineWidth: 2 property real lineWidth: 2
property real wavelength: 20 property real wavelength: 20
property real amp: 1.6 property real amp: 1.6
@@ -17,7 +15,6 @@ Item {
property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40) property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40)
property color fillColor: Theme.primary property color fillColor: Theme.primary
property color playheadColor: Theme.primary property color playheadColor: Theme.primary
property color actualProgressColor: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.45)
property real dpr: (root.window ? root.window.devicePixelRatio : 1) property real dpr: (root.window ? root.window.devicePixelRatio : 1)
function snap(v) { function snap(v) {
@@ -25,12 +22,7 @@ Item {
} }
readonly property real playX: snap(root.width * root.value) readonly property real playX: snap(root.width * root.value)
readonly property real actualX: snap(root.width * root.actualValue)
readonly property real midY: snap(height / 2) readonly property real midY: snap(height / 2)
readonly property bool previewAhead: root.showActualPlaybackState && root.value > root.actualValue
readonly property bool previewBehind: root.showActualPlaybackState && root.value < root.actualValue
readonly property real previewGapStartX: Math.min(root.playX, root.actualX)
readonly property real previewGapEndX: Math.max(root.playX, root.actualX)
Behavior on currentAmp { Behavior on currentAmp {
NumberAnimation { NumberAnimation {
@@ -73,9 +65,7 @@ Item {
readonly property real startX: snap(root.lineWidth / 2) readonly property real startX: snap(root.lineWidth / 2)
readonly property real aaBias: (0.25 / root.dpr) readonly property real aaBias: (0.25 / root.dpr)
readonly property real endX: root.previewAhead ? Math.max(startX, Math.min(root.actualX - aaBias, width)) : Math.max(startX, Math.min(root.playX - startX - aaBias, width)) readonly property real endX: Math.max(startX, Math.min(root.playX - startX - aaBias, width))
readonly property real gapStartX: root.previewAhead ? Math.max(startX, Math.min(root.actualX + aaBias, width)) : Math.max(startX, Math.min(root.playX + playhead.width / 2, width))
readonly property real gapEndX: root.previewAhead ? Math.max(gapStartX, Math.min(root.playX - playhead.width / 2 - aaBias, width)) : Math.max(gapStartX, Math.min(root.actualX - aaBias, width))
Rectangle { Rectangle {
id: mask id: mask
@@ -110,37 +100,6 @@ Item {
} }
} }
Rectangle {
id: actualMask
anchors.top: parent.top
anchors.bottom: parent.bottom
x: waveClip.gapStartX
width: Math.max(0, waveClip.gapEndX - waveClip.gapStartX)
color: "transparent"
clip: true
visible: (root.previewBehind || root.previewAhead) && width > 0
Shape {
anchors.top: parent.top
anchors.bottom: parent.bottom
width: root.width + 4 * root.wavelength
antialiasing: true
preferredRendererType: Shape.CurveRenderer
x: waveOffsetX
ShapePath {
strokeColor: root.actualProgressColor
strokeWidth: snap(root.lineWidth)
capStyle: ShapePath.RoundCap
joinStyle: ShapePath.RoundJoin
fillColor: "transparent"
PathSvg {
path: waveSvg.path
}
}
}
}
Rectangle { Rectangle {
id: startCap id: startCap
width: snap(root.lineWidth) width: snap(root.lineWidth)
@@ -148,7 +107,7 @@ Item {
radius: width / 2 radius: width / 2
color: root.fillColor color: root.fillColor
x: waveClip.startX - width / 2 x: waveClip.startX - width / 2
y: waveY(waveClip.startX) - height / 2 y: root.midY - height / 2 + root.currentAmp * Math.sin((waveClip.startX / root.wavelength) * 2 * Math.PI + root.phase)
visible: waveClip.endX > waveClip.startX visible: waveClip.endX > waveClip.startX
z: 2 z: 2
} }
@@ -160,34 +119,10 @@ Item {
radius: width / 2 radius: width / 2
color: root.fillColor color: root.fillColor
x: waveClip.endX - width / 2 x: waveClip.endX - width / 2
y: waveY(waveClip.endX) - height / 2 y: root.midY - height / 2 + root.currentAmp * Math.sin((waveClip.endX / root.wavelength) * 2 * Math.PI + root.phase)
visible: waveClip.endX > waveClip.startX visible: waveClip.endX > waveClip.startX
z: 2 z: 2
} }
Rectangle {
id: actualEndCap
width: snap(root.lineWidth)
height: snap(root.lineWidth)
radius: width / 2
color: root.actualProgressColor
x: waveClip.gapEndX - width / 2
y: waveY(waveClip.gapEndX) - height / 2
visible: (root.previewBehind || root.previewAhead) && actualMask.width > 0
z: 2
}
Rectangle {
id: actualMarker
width: 2
height: Math.max(root.lineWidth + 4, 10)
radius: width / 2
color: root.actualProgressColor
x: root.actualX - width / 2
y: root.midY - height / 2
visible: root.showActualPlaybackState
z: 2
}
} }
Rectangle { Rectangle {
@@ -206,10 +141,6 @@ Item {
let r = a % m; let r = a % m;
return r < 0 ? r + m : r; return r < 0 ? r + m : r;
} }
function waveY(x, amplitude = root.currentAmp, phaseOffset = root.phase) {
return root.midY + amplitude * Math.sin((x / root.wavelength) * 2 * Math.PI + phaseOffset);
}
readonly property real waveOffsetX: -wrapMod(phase / k, wavelength) readonly property real waveOffsetX: -wrapMod(phase / k, wavelength)
FrameAnimation { FrameAnimation {
@@ -217,9 +148,8 @@ Item {
onTriggered: { onTriggered: {
if (root.isPlaying) if (root.isPlaying)
root.phase += 0.03 * frameTime * 60; root.phase += 0.03 * frameTime * 60;
startCap.y = waveY(waveClip.startX) - startCap.height / 2; startCap.y = root.midY - startCap.height / 2 + root.currentAmp * Math.sin((waveClip.startX / root.wavelength) * 2 * Math.PI + root.phase);
endCap.y = waveY(waveClip.endX) - endCap.height / 2; endCap.y = root.midY - endCap.height / 2 + root.currentAmp * Math.sin((waveClip.endX / root.wavelength) * 2 * Math.PI + root.phase);
actualEndCap.y = waveY(waveClip.gapEndX) - actualEndCap.height / 2;
} }
} }
+1 -5
View File
@@ -1,5 +1,4 @@
import QtQuick import QtQuick
import qs.Common
import qs.Services import qs.Services
Item { Item {
@@ -9,7 +8,6 @@ Item {
required property var targetWindow required property var targetWindow
property var blurItem: null property var blurItem: null
property bool blurEnabled: Theme.connectedSurfaceBlurEnabled
property real blurX: 0 property real blurX: 0
property real blurY: 0 property real blurY: 0
property real blurWidth: 0 property real blurWidth: 0
@@ -19,7 +17,7 @@ Item {
property var _region: null property var _region: null
function _apply() { function _apply() {
if (!blurEnabled || !BlurService.enabled || !targetWindow) { if (!BlurService.enabled || !targetWindow) {
_cleanup(); _cleanup();
return; return;
} }
@@ -45,8 +43,6 @@ Item {
_region = null; _region = null;
} }
onBlurEnabledChanged: _apply()
Connections { Connections {
target: BlurService target: BlurService
function onEnabledChanged() { function onEnabledChanged() {
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