1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-13 07:42:46 -04:00

Compare commits

...

37 Commits

Author SHA1 Message Date
bbedward 3d75a51378 gamma: add ipc call night getTemperature and enrich status function
fixes #1778
2026-04-12 21:46:41 -04:00
bbedward dc4b1529e6 gamma: reset lastAppliedTemp on suspend instead of destroying/recreating
outputs

fixes #2199 , possibly regresses #1235 - but I think the original bug
was lastAppliedTemp being incorrectly set, this allows compositors to
cache last applied gamma

gamma: add a bunch of defensive mechanisms for output changes
related to #2197

gamma: ensure gamma is re-evaluate on resume
fixes #1036
2026-04-12 21:40:39 -04:00
bbedward f61438e11f doctor: fix quickshell regex
fixes #2204
2026-04-11 12:44:58 -04:00
bbedward 8f78163941 dankinstall: workarounds for arch/extra change 2026-04-11 12:24:08 -04:00
mihem f894d338fc feat(running-apps): stronger active app highlight + indicator bar (#2190)
The focused app background used 20% primary opacity which was barely
visible. Increase to 45% to make the active window unambiguous at a glance.
2026-04-11 12:01:21 -04:00
bbedward f2df53afcd colorpicker: re-use Wayland buffer pools 2026-04-09 12:08:43 -04:00
Thomas Kroll 4179fcee83 fix(privacy): detect screen casting on Niri via PipeWire (#2185)
Screen sharing was not detected by PrivacyService on Niri because:

1. Niri creates the screencast as a Stream/Output/Video node, but
   screensharingActive only checked PwNodeType.VideoSource nodes.

2. looksLikeScreencast() only inspected application.name and
   node.name, missing Niri's node which has an empty application.name
   but identifies itself via media.name (niri-screen-cast-src).

Add Stream/Output/Video to the checked media classes and include
media.name in the screencast heuristic. Also add a forward-compatible
check for NiriService.hasActiveCast for when Niri gains cast tracking
in its IPC.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:50:39 -04:00
Andrey Yugai a0c9af1ee7 feature: persist last active player (#2184) 2026-04-09 11:30:04 -04:00
Thomas Kroll 049266271a fix(system-update): popout first-click and AUR package listing (#2183)
* fix(system-update): open popout on first click

The SystemUpdate widget required two clicks to open its popout.

On the first click, the LazyLoader was activated but popoutTarget
(bound to the loader's item) was still null in the MouseArea handler,
so setTriggerPosition was never called. The popout's open() then
returned early because screen was unset.

Restructure the onClicked handler to call setTriggerPosition directly
on the loaded item (matching the pattern used by Clock, Clipboard, and
other bar widgets) and use PopoutManager.requestPopout() instead of
toggle() for consistent popout management.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(system-update): include AUR packages in update list

When paru or yay is the package manager, the update list only showed
official repo packages (via checkupdates or -Qu) while the upgrade
command (paru/yay -Syu) also processes AUR packages. This mismatch
meant AUR updates appeared as a surprise during the upgrade.

Combine the repo update listing with the AUR helper's -Qua flag so
both official and AUR packages are shown in the popout before the
user triggers the upgrade. The output format is identical for both
sources, so the existing parser works unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 10:49:15 -04:00
Ron Harel 0eabda3164 Improve seek and scrub indicator/ animations in the media controls widget. (#2181) 2026-04-09 10:35:56 -04:00
bbedward 32c063aab8 Revert "qml: cut down on inline components for performance"
This reverts commit f6e590a518.
2026-04-07 15:22:46 -04:00
Ron Harel 37f92677cf Make the system tray overflow popup optional and have the widget expand inline as an alternative. (#2171) 2026-04-07 11:13:32 -04:00
Ron Harel 13e8130858 Add adaptive media width setting. (#2165) 2026-04-07 11:07:36 -04:00
bbedward f6e590a518 qml: cut down on inline components for performance 2026-04-07 10:57:11 -04:00
bbedward 3194fc3fbe core: allow RO commands to run as root 2026-04-06 18:19:17 -04:00
bbedward 3318864ece clipboard: make CLI keep CL item in-memory again 2026-04-06 16:09:10 -04:00
Iris e224417593 feature: add login sound functionality and settings entry (#2155)
* added login sound functionality and settings entry

* Removed debug warning that was accidentally left in

* loginSound is off by default, and fixed toggle not working

* Prevent login sound from playing in the same session

---------

Co-authored-by: Iris <iris@raidev.eu>
2026-04-06 14:11:00 -04:00
Marcus Ramberg 3f7f6c5d2c core(doctor): show all detected terminals (#2163) 2026-04-06 14:09:15 -04:00
bbedward 0b88055742 clipboard: fix reliability of modal/popout 2026-04-06 10:30:39 -04:00
bbedward 2b0826e397 core: migrate to dms-shell arch package 2026-04-06 10:09:55 -04:00
bbedward 7db04c9660 i18n: use comments instead of context, sync 2026-04-06 09:41:45 -04:00
Al- Amin 14d1e1d985 fix: Add match rule for new version of Gnome Calculator app ID (#2157) 2026-04-06 09:11:26 -04:00
Dimariqe 903ab1e61d fix: add TrayRecoveryService with bidirectional SNI dedup (Go server) (#2137)
* fix: add TrayRecoveryService with bidirectional SNI dedup (Go server)

Add TrayRecoveryService manager that re-registers lost tray icons after
resume from suspend via native DBus scanning in the Go server.

The service resolves every registered SNI item (both well-known names and
:1.xxx connection IDs) to a canonical connection ID, building a unified
registeredConnIDs set before either scan section runs. This prevents
duplicates in both directions:

- If an app registered via well-known name, the connection-ID section
  skips its :1.xxx entry.
- If an app registered via connection ID, the well-known-name section
  skips its well-known name (checked through registeredConnIDs).
- After successfully registering via well-known name, registeredConnIDs
  is updated immediately so the connection-ID section won't probe the
  same app in the same run.

A startup scan (3 s delay) covers the common case where the DMS process
is killed during suspend and restarted by systemd (Type=dbus), so the
loginctl PrepareForSleep watcher alone is not sufficient. The startup
scan is harmless on a normal boot — it finds all items already registered
and exits early.

Go port of quickshell commit 1470aa3.

* fix: 'interface{}' can be replaced by 'any'

* TrayRecoveryService: Remove objPath parameter from registerSNI
2026-04-06 09:03:36 -04:00
Walid Salah 5982655539 Make focused app widget only show focused app on the current screen (#2152) 2026-04-05 10:53:18 -04:00
Aaron Tulino 1021a210cf Change power profile by scrolling battery (#2142)
Scrolling up "increases" the profile (Power Saver -> Balanced -> Performance). Supports touchpad.
2026-04-03 12:04:00 -04:00
bbedward e34edb15bb i18n: sync 2026-04-03 11:56:00 -04:00
dev 61ee5f4336 Update CpuMonitor.qml to reserve enough widget space for the widest number "100" instead of "88" (#2135)
My bar kept shifting around by a few pixels every time I hit 100% cpu usage
2026-04-02 15:26:56 -04:00
Phil Jackson ce2a92ec27 feat: rewind to track start on previous when past 8 seconds (#2136)
* feat: rewind to track start on previous when past 8 seconds

Adds MprisController.previousOrRewind() which rewinds the current track
to position 0 if more than 8 seconds in (with canSeek check), and falls
back to previous() otherwise — matching traditional media player behaviour.

All previous() call sites across Media.qml, MediaPlayerTab.qml,
MediaOverviewCard.qml, LockScreenContent.qml and DMSShellIPC.qml
are updated to use the new shared function.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: poll position in MprisController for previousOrRewind accuracy

Without a polling timer, activePlayer.position is never updated in
contexts that don't display a seekbar (e.g. the DankBar widget), causing
the position > 8 check in previousOrRewind() to always see 0 and fall
through to previous() instead of rewinding.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 15:26:36 -04:00
Al- Amin 66ce79b9bf fix:update resizeactive binding to include height to make it work (#2126) 2026-04-01 09:19:01 -04:00
Al- Amin 30dd640314 fix:add window rule for the new version of Gnome Calculator (#2125) 2026-04-01 09:18:56 -04:00
bbedward 28f9aabcd9 screenshot: fix scaling of global coordinate space when using all
screens
2026-03-31 15:13:10 -04:00
bbedward 3d9bd73336 launcher: some polishes for blur 2026-03-31 11:04:18 -04:00
bbedward 3497d5f523 blur: stylize control center for blur mode 2026-03-31 09:42:08 -04:00
bbedward 8ef1d95e65 popout: fix inconsistent transparency 2026-03-31 09:06:48 -04:00
bbedward e9aeb9ac60 blur: add probe to check compositor for ext-bg-effect 2026-03-30 15:18:44 -04:00
bbedward fb02f7294d workspace: fix mouse area to edges
fixes #2108
2026-03-30 15:16:17 -04:00
bbedward f15d49d80a blur: add blur support with ext-bg-effect 2026-03-30 11:52:35 -04:00
114 changed files with 24469 additions and 24884 deletions
+40
View File
@@ -0,0 +1,40 @@
package main
import (
"fmt"
"os"
"github.com/AvengeMedia/DankMaterialShell/core/internal/blur"
"github.com/spf13/cobra"
)
var blurCmd = &cobra.Command{
Use: "blur",
Short: "Background blur utilities",
}
var blurCheckCmd = &cobra.Command{
Use: "check",
Short: "Check if the compositor supports background blur (ext-background-effect-v1)",
Args: cobra.NoArgs,
Run: runBlurCheck,
}
func init() {
blurCmd.AddCommand(blurCheckCmd)
}
func runBlurCheck(cmd *cobra.Command, args []string) {
supported, err := blur.ProbeSupport()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
switch supported {
case true:
fmt.Println("supported")
default:
fmt.Println("unsupported")
}
}
+1
View File
@@ -525,5 +525,6 @@ func getCommonCommands() []*cobra.Command {
configCmd, configCmd,
dlCmd, dlCmd,
randrCmd, randrCmd,
blurCmd,
} }
} }
+8 -4
View File
@@ -82,7 +82,7 @@ func (ds *DoctorStatus) OKCount() int {
} }
var ( var (
quickshellVersionRegex = regexp.MustCompile(`quickshell (\d+\.\d+\.\d+)`) quickshellVersionRegex = regexp.MustCompile(`(?i)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,10 +820,14 @@ 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"}
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 { terminals = slices.DeleteFunc(terminals, func(t string) bool {
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], "", optionalFeaturesURL}) return !utils.CommandExists(t)
})
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, or alacritty", optionalFeaturesURL}) results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, foot or alacritty", optionalFeaturesURL})
} }
networkResult, err := network.DetectNetworkStack() networkResult, err := network.DetectNetworkStack()
+28 -3
View File
@@ -109,16 +109,41 @@ func updateArchLinux() error {
} }
var packageName string var packageName string
if isArchPackageInstalled("dms-shell-bin") { var isAUR bool
packageName = "dms-shell-bin" if isArchPackageInstalled("dms-shell") {
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: Neither dms-shell-bin nor dms-shell-git package found.") fmt.Println("Info: No dms-shell 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
+4 -1
View File
@@ -5,6 +5,7 @@ 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"
) )
@@ -30,7 +31,9 @@ func init() {
} }
func main() { func main() {
if os.Geteuid() == 0 { clipboard.MaybeServeAndExit()
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.")
} }
+4 -1
View File
@@ -5,6 +5,7 @@ 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"
) )
@@ -27,7 +28,9 @@ func init() {
} }
func main() { func main() {
if os.Geteuid() == 0 { clipboard.MaybeServeAndExit()
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,6 +7,22 @@ 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()
+35
View File
@@ -0,0 +1,35 @@
package blur
import (
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
client "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
const extBackgroundEffectInterface = "ext_background_effect_manager_v1"
func ProbeSupport() (bool, error) {
display, err := client.Connect("")
if err != nil {
return false, err
}
defer display.Context().Close()
registry, err := display.GetRegistry()
if err != nil {
return false, err
}
found := false
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
switch e.Interface {
case extBackgroundEffectInterface:
found = true
}
})
if err := wlhelpers.Roundtrip(display, display.Context()); err != nil {
return false, err
}
return found, nil
}
+118 -83
View File
@@ -1,7 +1,6 @@
package clipboard package clipboard
import ( import (
"bytes"
"fmt" "fmt"
"io" "io"
"os" "os"
@@ -13,66 +12,142 @@ 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 CopyReader(bytes.NewReader(data), mimeType, false, false) return copyForkCached(data, mimeType, 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 copyServeWithWriter(func(writer io.Writer) error { return serveClipboard(data, mimeType, pasteOnce)
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 CopyReader(bytes.NewReader(data), mimeType, foreground, pasteOnce) return copyForkCached(data, mimeType, 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 {
return copyFork(data, mimeType, pasteOnce) buf, err := io.ReadAll(data)
if err != nil {
return fmt.Errorf("read source: %w", err)
}
return serveClipboard(buf, mimeType, pasteOnce)
} }
return copyServeReader(data, mimeType, pasteOnce) return copyFork(data, mimeType, pasteOnce)
} }
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error { func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd {
args := []string{os.Args[0], "cl", "copy", "--foreground"} cmd := exec.Command(os.Args[0])
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(), "DMS_CLIP_FORKED=1") cmd.Env = append(os.Environ(),
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
if err := cmd.Start(); err != nil { return waitReady(cmd)
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)
} }
@@ -83,50 +158,22 @@ 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 var buf [1]byte
if _, err := stdout.Read(buf[:]); err != nil { if _, err := stdout.Read(buf[:]); err != nil {
return fmt.Errorf("waiting for clipboard ready: %w", err) return fmt.Errorf("waiting for clipboard ready: %w", err)
}
return nil
} }
return nil
} }
func signalReady() { func signalReady() {
if os.Getenv("DMS_CLIP_FORKED") == "" { if os.Getenv(envServe) == "" {
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{}
@@ -147,7 +194,7 @@ func createClipboardCacheFile() (*os.File, error) {
return os.CreateTemp("", "dms-clipboard-*") return os.CreateTemp("", "dms-clipboard-*")
} }
func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOnce bool) error { func serveClipboard(data []byte, 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)
@@ -189,12 +236,10 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
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")
} }
@@ -233,18 +278,12 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
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) {
defer syscall.Close(e.Fd) _ = syscall.SetNonblock(e.Fd, false)
file := os.NewFile(uintptr(e.Fd), "pipe") file := os.NewFile(uintptr(e.Fd), "pipe")
defer file.Close() defer file.Close()
if err := writeTo(file); err != nil { _, _ = file.Write(data)
select {
case sendErr <- err:
default:
}
}
select { select {
case pasted <- struct{}{}: case pasted <- struct{}{}:
default: default:
@@ -266,8 +305,6 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
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
@@ -521,12 +558,10 @@ 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")
} }
@@ -554,12 +589,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) {
defer syscall.Close(e.Fd) _ = syscall.SetNonblock(e.Fd, false)
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 {
+53 -50
View File
@@ -39,11 +39,10 @@ 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
wlPool *client.ShmPool wlPools [2]*client.ShmPool
wlBuffer *client.Buffer wlBuffers [2]*client.Buffer
bufferBusy bool slotBusy [2]bool
oldPool *client.ShmPool needsRedraw bool
oldBuffer *client.Buffer
scopyBuffer *client.Buffer scopyBuffer *client.Buffer
configured bool configured bool
hidden bool hidden bool
@@ -136,6 +135,7 @@ func (p *Picker) Run() (*Color, error) {
break break
} }
p.flushRedraws()
p.checkDone() p.checkDone()
} }
@@ -164,6 +164,15 @@ 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 {
@@ -507,47 +516,45 @@ 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
if ls.hidden { switch {
case ls.hidden:
renderBuf = ls.state.RedrawScreenOnly() renderBuf = ls.state.RedrawScreenOnly()
} else { default:
renderBuf = ls.state.Redraw() renderBuf = ls.state.Redraw()
} }
if renderBuf == nil { if renderBuf == nil {
return return
} }
if ls.oldBuffer != nil { ls.needsRedraw = false
ls.oldBuffer.Destroy()
ls.oldBuffer = nil if ls.wlPools[slot] == nil {
} pool, err := p.shm.CreatePool(renderBuf.Fd(), int32(renderBuf.Size()))
if ls.oldPool != nil { if err != nil {
ls.oldPool.Destroy() return
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.oldPool = ls.wlPool ls.slotBusy[slot] = true
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 {
@@ -566,7 +573,7 @@ func (p *Picker) redrawSurface(ls *LayerSurface) {
} }
_ = ls.wlSurface.SetBufferScale(bufferScale) _ = ls.wlSurface.SetBufferScale(bufferScale)
} }
_ = ls.wlSurface.Attach(wlBuffer, 0, 0) _ = ls.wlSurface.Attach(ls.wlBuffers[slot], 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()
@@ -634,7 +641,7 @@ func (p *Picker) setupPointerHandlers() {
} }
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY) p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
p.redrawSurface(p.activeSurface) p.activeSurface.needsRedraw = true
}) })
p.pointer.SetLeaveHandler(func(e client.PointerLeaveEvent) { p.pointer.SetLeaveHandler(func(e client.PointerLeaveEvent) {
@@ -655,7 +662,7 @@ func (p *Picker) setupPointerHandlers() {
return return
} }
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY) p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
p.redrawSurface(p.activeSurface) p.activeSurface.needsRedraw = true
}) })
p.pointer.SetButtonHandler(func(e client.PointerButtonEvent) { p.pointer.SetButtonHandler(func(e client.PointerButtonEvent) {
@@ -679,17 +686,13 @@ func (p *Picker) cleanup() {
if ls.scopyBuffer != nil { if ls.scopyBuffer != nil {
ls.scopyBuffer.Destroy() ls.scopyBuffer.Destroy()
} }
if ls.oldBuffer != nil { for i := range ls.wlBuffers {
ls.oldBuffer.Destroy() if ls.wlBuffers[i] != nil {
} ls.wlBuffers[i].Destroy()
if ls.oldPool != nil { }
ls.oldPool.Destroy() if ls.wlPools[i] != nil {
} 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,6 +274,12 @@ 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
@@ -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% bind = SUPER CTRL, F, resizeactive, exact 100% 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,6 +94,7 @@ 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,6 +224,7 @@ 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$"#
+47 -51
View File
@@ -242,11 +242,7 @@ func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMap
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR} return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
} }
if a.packageInstalled("dms-shell-bin") { return PackageMapping{Name: "dms-shell", Repository: RepoTypeSystem}
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 {
@@ -328,6 +324,13 @@ 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{
@@ -445,6 +448,37 @@ 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
@@ -453,6 +487,9 @@ 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{
@@ -540,7 +577,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" || pkg == "dms-shell-bin" { if pkg == "dms-shell-git" {
dmsShell = append(dmsShell, pkg) dmsShell = append(dmsShell, pkg)
} else { } else {
isDep := false isDep := false
@@ -621,7 +658,7 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
} }
} }
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" { if pkg == "dms-shell-git" {
srcinfoPath := filepath.Join(packageDir, ".SRCINFO") srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
depsToRemove := []string{ depsToRemove := []string{
"depends = quickshell", "depends = quickshell",
@@ -644,15 +681,7 @@ 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),
@@ -739,42 +768,9 @@ 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
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" { matches, _ := filepath.Glob(filepath.Join(packageDir, "*.pkg.tar*"))
// For DMS split packages, install base package files = matches
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)
+29 -13
View File
@@ -444,20 +444,21 @@ func GetFocusedMonitor() string {
type outputInfo struct { type outputInfo struct {
x, y int32 x, y int32
scale float64
transform int32 transform int32
} }
func getOutputInfo(outputName string) (*outputInfo, bool) { func getAllOutputInfos() map[string]*outputInfo {
display, err := client.Connect("") display, err := client.Connect("")
if err != nil { if err != nil {
return nil, false return nil
} }
ctx := display.Context() ctx := display.Context()
defer ctx.Close() defer ctx.Close()
registry, err := display.GetRegistry() registry, err := display.GetRegistry()
if err != nil { if err != nil {
return nil, false return nil
} }
var outputManager *wlr_output_management.ZwlrOutputManagerV1 var outputManager *wlr_output_management.ZwlrOutputManagerV1
@@ -476,16 +477,17 @@ func getOutputInfo(outputName string) (*outputInfo, bool) {
}) })
if err := wlhelpers.Roundtrip(display, ctx); err != nil { if err := wlhelpers.Roundtrip(display, ctx); err != nil {
return nil, false return nil
} }
if outputManager == nil { if outputManager == nil {
return nil, false return nil
} }
type headState struct { type headState struct {
name string name string
x, y int32 x, y int32
scale float64
transform int32 transform int32
} }
heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState) heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState)
@@ -501,6 +503,9 @@ func getOutputInfo(outputName string) (*outputInfo, bool) {
state.x = pe.X state.x = pe.X
state.y = pe.Y state.y = pe.Y
}) })
e.Head.SetScaleHandler(func(se wlr_output_management.ZwlrOutputHeadV1ScaleEvent) {
state.scale = se.Scale
})
e.Head.SetTransformHandler(func(te wlr_output_management.ZwlrOutputHeadV1TransformEvent) { e.Head.SetTransformHandler(func(te wlr_output_management.ZwlrOutputHeadV1TransformEvent) {
state.transform = te.Transform state.transform = te.Transform
}) })
@@ -511,21 +516,32 @@ func getOutputInfo(outputName string) (*outputInfo, bool) {
for !done { for !done {
if err := ctx.Dispatch(); err != nil { if err := ctx.Dispatch(); err != nil {
return nil, false return nil
} }
} }
result := make(map[string]*outputInfo, len(heads))
for _, state := range heads { for _, state := range heads {
if state.name == outputName { if state.name == "" {
return &outputInfo{ continue
x: state.x, }
y: state.y, result[state.name] = &outputInfo{
transform: state.transform, x: state.x,
}, true y: state.y,
scale: state.scale,
transform: state.transform,
} }
} }
return result
}
return nil, false func getOutputInfo(outputName string) (*outputInfo, bool) {
infos := getAllOutputInfos()
if infos == nil {
return nil, false
}
info, ok := infos[outputName]
return info, ok
} }
func getDWLActiveWindow() (*WindowGeometry, error) { func getDWLActiveWindow() (*WindowGeometry, error) {
+96 -70
View File
@@ -2,6 +2,7 @@ package screenshot
import ( import (
"fmt" "fmt"
"math"
"sync" "sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
@@ -304,22 +305,20 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
if len(outputs) == 0 { if len(outputs) == 0 {
return nil, fmt.Errorf("no outputs available") return nil, fmt.Errorf("no outputs available")
} }
if len(outputs) == 1 { if len(outputs) == 1 {
return s.captureWholeOutput(outputs[0]) return s.captureWholeOutput(outputs[0])
} }
// Capture all outputs first to get actual buffer sizes wlrInfos := getAllOutputInfos()
type capturedOutput struct {
output *WaylandOutput
result *CaptureResult
physX int
physY int
}
captured := make([]capturedOutput, 0, len(outputs))
var minX, minY, maxX, maxY int type pendingOutput struct {
first := true result *CaptureResult
logX float64
logY float64
scale float64
}
var pending []pendingOutput
maxScale := 1.0
for _, output := range outputs { for _, output := range outputs {
result, err := s.captureWholeOutput(output) result, err := s.captureWholeOutput(output)
@@ -328,50 +327,74 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
continue continue
} }
outX, outY := output.x, output.y logX, logY := float64(output.x), float64(output.y)
scale := float64(output.scale) scale := float64(output.scale)
switch DetectCompositor() { switch DetectCompositor() {
case CompositorHyprland: case CompositorHyprland:
if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok { if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok {
outX, outY = hx, hy logX, logY = float64(hx), float64(hy)
} }
if s := GetHyprlandMonitorScale(output.name); s > 0 { if hs := GetHyprlandMonitorScale(output.name); hs > 0 {
scale = s scale = hs
} }
case CompositorDWL: default:
if info, ok := getOutputInfo(output.name); ok { if wlrInfos != nil {
outX, outY = info.x, info.y if info, ok := wlrInfos[output.name]; ok {
logX, logY = float64(info.x), float64(info.y)
if info.scale > 0 {
scale = info.scale
}
}
} }
} }
if scale <= 0 { if scale <= 0 {
scale = 1.0 scale = 1.0
} }
physX := int(float64(outX) * scale) pending = append(pending, pendingOutput{result: result, logX: logX, logY: logY, scale: scale})
physY := int(float64(outY) * scale) if scale > maxScale {
maxScale = scale
}
}
captured = append(captured, capturedOutput{ if len(pending) == 0 {
output: output, return nil, fmt.Errorf("failed to capture any outputs")
result: result, }
physX: physX, if len(pending) == 1 {
physY: physY, return pending[0].result, nil
}) }
right := physX + result.Buffer.Width type layoutEntry struct {
bottom := physY + result.Buffer.Height result *CaptureResult
canvasX int
canvasY int
canvasW int
canvasH int
}
entries := make([]layoutEntry, len(pending))
var minX, minY, maxX, maxY int
if first { for i, p := range pending {
minX, minY = physX, physY cx := int(math.Round(p.logX * maxScale))
maxX, maxY = right, bottom cy := int(math.Round(p.logY * maxScale))
first = false cw := int(math.Round(float64(p.result.Buffer.Width) * maxScale / p.scale))
ch := int(math.Round(float64(p.result.Buffer.Height) * maxScale / p.scale))
entries[i] = layoutEntry{result: p.result, canvasX: cx, canvasY: cy, canvasW: cw, canvasH: ch}
right := cx + cw
bottom := cy + ch
if i == 0 {
minX, minY, maxX, maxY = cx, cy, right, bottom
continue continue
} }
if cx < minX {
if physX < minX { minX = cx
minX = physX
} }
if physY < minY { if cy < minY {
minY = physY minY = cy
} }
if right > maxX { if right > maxX {
maxX = right maxX = right
@@ -381,35 +404,26 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
} }
} }
if len(captured) == 0 {
return nil, fmt.Errorf("failed to capture any outputs")
}
if len(captured) == 1 {
return captured[0].result, nil
}
totalW := maxX - minX totalW := maxX - minX
totalH := maxY - minY totalH := maxY - minY
composite, err := CreateShmBuffer(totalW, totalH, totalW*4)
compositeStride := totalW * 4
composite, err := CreateShmBuffer(totalW, totalH, compositeStride)
if err != nil { if err != nil {
for _, c := range captured { for _, e := range entries {
c.result.Buffer.Close() e.result.Buffer.Close()
} }
return nil, fmt.Errorf("create composite buffer: %w", err) return nil, fmt.Errorf("create composite buffer: %w", err)
} }
composite.Clear() composite.Clear()
var format uint32 var format uint32
for _, c := range captured { for _, e := range entries {
if format == 0 { if format == 0 {
format = c.result.Format format = e.result.Format
} }
s.blitBuffer(composite, c.result.Buffer, c.physX-minX, c.physY-minY, c.result.YInverted) s.blitBufferScaled(composite, e.result.Buffer,
c.result.Buffer.Close() e.canvasX-minX, e.canvasY-minY, e.canvasW, e.canvasH,
e.result.YInverted)
e.result.Buffer.Close()
} }
return &CaptureResult{ return &CaptureResult{
@@ -419,32 +433,44 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
}, nil }, nil
} }
func (s *Screenshoter) blitBuffer(dst, src *ShmBuffer, dstX, dstY int, yInverted bool) { func (s *Screenshoter) blitBufferScaled(dst, src *ShmBuffer, dstX, dstY, dstW, dstH int, yInverted bool) {
if dstW <= 0 || dstH <= 0 {
return
}
srcData := src.Data() srcData := src.Data()
dstData := dst.Data() dstData := dst.Data()
for srcY := 0; srcY < src.Height; srcY++ { for dy := 0; dy < dstH; dy++ {
actualSrcY := srcY canvasY := dstY + dy
if yInverted { if canvasY < 0 || canvasY >= dst.Height {
actualSrcY = src.Height - 1 - srcY
}
dy := dstY + srcY
if dy < 0 || dy >= dst.Height {
continue continue
} }
srcRowOff := actualSrcY * src.Stride srcY := dy * src.Height / dstH
dstRowOff := dy * dst.Stride if yInverted {
srcY = src.Height - 1 - srcY
}
if srcY < 0 || srcY >= src.Height {
continue
}
for srcX := 0; srcX < src.Width; srcX++ { srcRowOff := srcY * src.Stride
dx := dstX + srcX dstRowOff := canvasY * dst.Stride
if dx < 0 || dx >= dst.Width {
for dx := 0; dx < dstW; dx++ {
canvasX := dstX + dx
if canvasX < 0 || canvasX >= dst.Width {
continue
}
srcX := dx * src.Width / dstW
if srcX >= src.Width {
continue continue
} }
si := srcRowOff + srcX*4 si := srcRowOff + srcX*4
di := dstRowOff + dx*4 di := dstRowOff + canvasX*4
if si+3 >= len(srcData) || di+3 >= len(dstData) { if si+3 >= len(srcData) || di+3 >= len(dstData) {
continue continue
+29
View File
@@ -31,6 +31,7 @@ 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"
@@ -72,6 +73,7 @@ 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
@@ -394,6 +396,18 @@ 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 {
@@ -1325,6 +1339,9 @@ 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()
} }
@@ -1610,6 +1627,18 @@ 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
@@ -0,0 +1,93 @@
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")
}
@@ -0,0 +1,262 @@
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
}
+178 -48
View File
@@ -3,8 +3,11 @@ package wayland
import ( import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"errors"
"fmt" "fmt"
"io"
"os" "os"
"slices"
"syscall" "syscall"
"time" "time"
@@ -73,7 +76,10 @@ 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)
if err := m.setupOutputControls(m.availableOutputs, gammaMgr); err != nil { m.availOutputsMu.RLock()
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
} }
@@ -170,6 +176,7 @@ 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)
@@ -204,6 +211,11 @@ 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)
@@ -288,14 +300,28 @@ 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)
}) })
}) })
@@ -303,12 +329,75 @@ 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
} }
@@ -329,26 +418,37 @@ func (m *Manager) recreateOutputControl(out *outputState) error {
enabled := m.config.Enabled enabled := m.config.Enabled
m.configMutex.RUnlock() m.configMutex.RUnlock()
if !enabled || !m.controlsInitialized { switch {
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
} }
@@ -358,6 +458,13 @@ 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
@@ -690,11 +797,12 @@ func (m *Manager) applyGamma(temp int) {
gamma := m.config.Gamma gamma := m.config.Gamma
m.configMutex.RUnlock() m.configMutex.RUnlock()
if !m.controlsInitialized { switch {
case m.connectionDead.Load():
return return
} case !m.controlsInitialized:
return
if m.lastAppliedTemp == temp && m.lastAppliedGamma == gamma { case m.lastAppliedTemp == temp && m.lastAppliedGamma == gamma:
return return
} }
@@ -714,7 +822,14 @@ func (m *Manager) applyGamma(temp int) {
var jobs []job var jobs []job
for _, out := range outs { for _, out := range outs {
if out.failed || out.rampSize == 0 { switch {
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)
@@ -732,18 +847,16 @@ func (m *Manager) applyGamma(temp int) {
} }
for _, j := range jobs { for _, j := range jobs {
if err := m.setGammaBytes(j.out, j.data); err != nil { err := m.setGammaBytes(j.out, j.data)
log.Warnf("gamma: failed to set output %d: %v", j.out.id, err) if err == nil {
j.out.failed = true continue
j.out.rampSize = 0 }
outID := j.out.id log.Warnf("gamma: failed to set output %d: %v", j.out.id, err)
time.AfterFunc(300*time.Millisecond, func() { j.out.failed = true
m.post(func() { j.out.rampSize = 0
if out, ok := m.outputs.Load(outID); ok && out.failed { if isConnectionDeadErr(err) {
m.recreateOutputControl(out) m.markConnectionDead(err)
} return
})
})
} }
} }
@@ -752,6 +865,14 @@ 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
@@ -774,7 +895,6 @@ 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)
} }
@@ -882,10 +1002,10 @@ func (m *Manager) dbusMonitor() {
} }
func (m *Manager) handleDBusSignal(sig *dbus.Signal) { func (m *Manager) handleDBusSignal(sig *dbus.Signal) {
if sig.Name != "org.freedesktop.login1.Manager.PrepareForSleep" { switch {
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)
@@ -899,27 +1019,34 @@ func (m *Manager) handleDBusSignal(sig *dbus.Signal) {
return return
} }
time.AfterFunc(500*time.Millisecond, func() { time.AfterFunc(500*time.Millisecond, func() {
m.post(func() { m.post(m.handleResume)
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{}{}:
@@ -1058,7 +1185,10 @@ 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)
if err := m.setupOutputControls(m.availableOutputs, gammaMgr); err != nil { m.availOutputsMu.RLock()
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,6 +3,7 @@ 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"
@@ -71,9 +72,11 @@ 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-bin" return "dms-shell"
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"
+2
View File
@@ -124,6 +124,8 @@ 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: []
+10
View File
@@ -186,6 +186,14 @@ Singleton {
onPopoutElevationEnabledChanged: saveSettings() onPopoutElevationEnabledChanged: saveSettings()
property bool barElevationEnabled: true property bool barElevationEnabled: true
onBarElevationEnabledChanged: saveSettings() onBarElevationEnabledChanged: saveSettings()
property bool blurEnabled: false
onBlurEnabledChanged: saveSettings()
property string blurBorderColor: "outline"
onBlurBorderColorChanged: saveSettings()
property string blurBorderCustomColor: "#ffffff"
onBlurBorderCustomColorChanged: saveSettings()
property real blurBorderOpacity: 1.0
onBlurBorderOpacityChanged: saveSettings()
property string wallpaperFillMode: "Fill" property string wallpaperFillMode: "Fill"
property bool blurredWallpaperLayer: false property bool blurredWallpaperLayer: false
property bool blurWallpaperOnOverview: false property bool blurWallpaperOnOverview: false
@@ -293,6 +301,7 @@ 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
@@ -426,6 +435,7 @@ 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
@@ -75,6 +75,8 @@ var SPEC = {
vpnLastConnected: { def: "" }, vpnLastConnected: { def: "" },
lastPlayerIdentity: { def: "" },
deviceMaxVolumes: { def: {} }, deviceMaxVolumes: { def: {} },
hiddenOutputDeviceNames: { def: [] }, hiddenOutputDeviceNames: { def: [] },
hiddenInputDeviceNames: { def: [] }, hiddenInputDeviceNames: { def: [] },
@@ -58,6 +58,10 @@ var SPEC = {
modalElevationEnabled: { def: true }, modalElevationEnabled: { def: true },
popoutElevationEnabled: { def: true }, popoutElevationEnabled: { def: true },
barElevationEnabled: { def: true }, barElevationEnabled: { def: true },
blurEnabled: { def: false },
blurBorderColor: { def: "outline" },
blurBorderCustomColor: { def: "#ffffff" },
blurBorderOpacity: { def: 1.0, coerce: percentToUnit },
wallpaperFillMode: { def: "Fill" }, wallpaperFillMode: { def: "Fill" },
blurredWallpaperLayer: { def: false }, blurredWallpaperLayer: { def: false },
blurWallpaperOnOverview: { def: false }, blurWallpaperOnOverview: { def: false },
@@ -136,6 +140,7 @@ 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 },
@@ -238,6 +243,7 @@ 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 },
+12
View File
@@ -221,10 +221,22 @@ 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 {
+1 -3
View File
@@ -369,9 +369,7 @@ Item {
} }
function previous(): void { function previous(): void {
if (MprisController.activePlayer && MprisController.activePlayer.canGoPrevious) { MprisController.previousOrRewind();
MprisController.activePlayer.previous();
}
} }
function next(): void { function next(): void {
@@ -122,7 +122,7 @@ Item {
} }
StyledText { StyledText {
text: I18n.tr("No recent clipboard entries found") text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No recent clipboard entries found") : I18n.tr("Connecting to clipboard service…")
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: I18n.tr("No saved clipboard entries") text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No saved clipboard entries") : I18n.tr("Connecting to clipboard service…")
anchors.centerIn: parent anchors.centerIn: parent
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
@@ -60,15 +60,12 @@ 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(); if (clipboardAvailable)
ClipboardService.refresh();
keyboardController.reset(); keyboardController.reset();
Qt.callLater(function () { Qt.callLater(function () {
@@ -50,14 +50,11 @@ 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(); if (clipboardAvailable)
ClipboardService.refresh();
keyboardController.reset(); keyboardController.reset();
Qt.callLater(function () { Qt.callLater(function () {
@@ -122,10 +119,10 @@ DankPopout {
onBackgroundClicked: hide() onBackgroundClicked: hide()
onShouldBeVisibleChanged: { onShouldBeVisibleChanged: {
if (!shouldBeVisible) { if (!shouldBeVisible)
return; return;
} if (clipboardAvailable)
ClipboardService.refresh(); ClipboardService.refresh();
keyboardController.reset(); keyboardController.reset();
Qt.callLater(function () { Qt.callLater(function () {
if (contentLoader.item?.searchField) { if (contentLoader.item?.searchField) {
+36 -2
View File
@@ -3,6 +3,7 @@ import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets
Item { Item {
id: root id: root
@@ -30,7 +31,7 @@ Item {
property real animationOffset: Theme.spacingL property real animationOffset: Theme.spacingL
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
property color backgroundColor: Theme.surfaceContainer property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
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
@@ -59,11 +60,25 @@ Item {
function open() { function open() {
closeTimer.stop(); closeTimer.stop();
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();
}
}
function _finishOpen() {
ModalManager.openModal(root); ModalManager.openModal(root);
shouldBeVisible = true; shouldBeVisible = true;
if (!useSingleWindow) if (!useSingleWindow)
@@ -215,6 +230,16 @@ Item {
visible: false visible: false
color: "transparent" color: "transparent"
WindowBlur {
targetWindow: contentWindow
readonly property real s: Math.min(1, modalContainer.scaleValue)
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr)
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
blurWidth: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.width * s : 0
blurHeight: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.height * s : 0
blurRadius: root.cornerRadius
}
WlrLayershell.namespace: root.layerNamespace WlrLayershell.namespace: root.layerNamespace
WlrLayershell.layer: { WlrLayershell.layer: {
if (root.useOverlayLayer) if (root.useOverlayLayer)
@@ -393,6 +418,15 @@ Item {
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 {
anchors.fill: parent
radius: root.cornerRadius
color: "transparent"
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
z: 100
}
FocusScope { FocusScope {
anchors.fill: parent anchors.fill: parent
focus: root.shouldBeVisible focus: root.shouldBeVisible
+1 -1
View File
@@ -132,7 +132,7 @@ DankModal {
modalWidth: 680 modalWidth: 680
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 680 modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 680
backgroundColor: Theme.surfaceContainer backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
cornerRadius: Theme.cornerRadius cornerRadius: Theme.cornerRadius
borderColor: Theme.outlineMedium borderColor: Theme.outlineMedium
borderWidth: 1 borderWidth: 1
@@ -4,6 +4,7 @@ import Quickshell.Wayland
import Quickshell.Hyprland import Quickshell.Hyprland
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets
Item { Item {
id: root id: root
@@ -134,40 +135,47 @@ Item {
} }
} }
function show() { function _finishShow(query, mode) {
closeCleanupTimer.stop(); spotlightOpen = true;
isClosing = false; isClosing = false;
openedFromOverview = false; openedFromOverview = false;
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen)
launcherWindow.screen = focusedScreen;
spotlightOpen = true;
keyboardActive = true; keyboardActive = true;
ModalManager.openModal(root); ModalManager.openModal(root);
if (useHyprlandFocusGrab) if (useHyprlandFocusGrab)
focusGrab.active = true; focusGrab.active = true;
_ensureContentLoadedAndInitialize("", ""); _ensureContentLoadedAndInitialize(query || "", mode || "");
}
function show() {
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) {
closeCleanupTimer.stop(); closeCleanupTimer.stop();
isClosing = false;
openedFromOverview = false;
var focusedScreen = CompositorService.getFocusedScreen(); var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen) if (focusedScreen && launcherWindow.screen !== focusedScreen) {
spotlightOpen = false;
isClosing = false;
launcherWindow.screen = focusedScreen; launcherWindow.screen = focusedScreen;
Qt.callLater(() => root._finishShow(query, ""));
return;
}
spotlightOpen = true; _finishShow(query, "");
keyboardActive = true;
ModalManager.openModal(root);
if (useHyprlandFocusGrab)
focusGrab.active = true;
_ensureContentLoadedAndInitialize(query, "");
} }
function hide() { function hide() {
@@ -191,14 +199,20 @@ Item {
function showWithMode(mode) { function showWithMode(mode) {
closeCleanupTimer.stop(); 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; isClosing = false;
openedFromOverview = false; openedFromOverview = false;
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen)
launcherWindow.screen = focusedScreen;
spotlightOpen = true;
keyboardActive = true; keyboardActive = true;
ModalManager.openModal(root); ModalManager.openModal(root);
if (useHyprlandFocusGrab) if (useHyprlandFocusGrab)
@@ -295,6 +309,16 @@ Item {
color: "transparent" color: "transparent"
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore
WindowBlur {
targetWindow: launcherWindow
readonly property real s: Math.min(1, modalContainer.scale)
blurX: root.modalX + root.modalWidth * (1 - s) * 0.5
blurY: root.modalY + root.modalHeight * (1 - s) * 0.5
blurWidth: (contentVisible && modalContainer.opacity > 0) ? root.modalWidth * s : 0
blurHeight: (contentVisible && modalContainer.opacity > 0) ? root.modalHeight * s : 0
blurRadius: root.cornerRadius
}
WlrLayershell.namespace: "dms:spotlight" WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: { WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) { switch (Quickshell.env("DMS_MODAL_LAYER")) {
@@ -428,6 +452,14 @@ Item {
event.accepted = true; event.accepted = true;
} }
} }
Rectangle {
anchors.fill: parent
radius: root.cornerRadius
color: "transparent"
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
}
} }
} }
} }
@@ -311,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
@@ -737,8 +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: root.parentModal?.isClosing ? 0 : 1
ResultsList { ResultsList {
id: resultsList id: resultsList
anchors.fill: parent anchors.fill: parent
@@ -324,6 +324,8 @@ Item {
height: 24 height: 24
z: 100 z: 100
visible: { visible: {
if (BlurService.enabled)
return false;
if (mainListView.contentHeight <= mainListView.height) if (mainListView.contentHeight <= mainListView.height)
return false; return false;
var atBottom = mainListView.contentY >= mainListView.contentHeight - mainListView.height + mainListView.originY - 5; var atBottom = mainListView.contentY >= mainListView.contentHeight - mainListView.height + mainListView.originY - 5;
@@ -449,7 +451,7 @@ Item {
case "apps": case "apps":
return "apps"; return "apps";
default: default:
return root.controller?.searchQuery?.length > 0 ? "search_off" : "search"; return "search_off";
} }
} }
} }
@@ -485,9 +487,9 @@ Item {
case "plugins": case "plugins":
return hasQuery ? I18n.tr("No plugin results") : I18n.tr("Browse or search plugins"); return hasQuery ? I18n.tr("No plugin results") : I18n.tr("Browse or search plugins");
case "apps": case "apps":
return hasQuery ? I18n.tr("No apps found") : I18n.tr("Type to search apps"); return I18n.tr("No apps found");
default: default:
return hasQuery ? I18n.tr("No results found") : I18n.tr("Type to search"); return I18n.tr("No results found");
} }
} }
} }
@@ -133,7 +133,7 @@ DankPopout {
QtObject { QtObject {
id: modalAdapter id: modalAdapter
property bool spotlightOpen: appDrawerPopout.shouldBeVisible property bool spotlightOpen: appDrawerPopout.shouldBeVisible
property bool isClosing: false readonly property bool isClosing: !appDrawerPopout.shouldBeVisible
function hide() { function hide() {
appDrawerPopout.close(); appDrawerPopout.close();
@@ -34,7 +34,7 @@ PluginComponent {
id: detailRoot id: detailRoot
implicitHeight: detailColumn.implicitHeight + Theme.spacingM * 2 implicitHeight: detailColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
DankActionButton { DankActionButton {
anchors.top: parent.top anchors.top: parent.top
@@ -252,7 +252,7 @@ PluginComponent {
width: parent ? parent.width : 300 width: parent ? parent.width : 300
height: 50 height: 50
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceContainerHighest color: Theme.surfaceLight
border.width: 1 border.width: 1
border.color: Theme.outlineLight border.color: Theme.outlineLight
opacity: 1.0 opacity: 1.0
@@ -33,7 +33,7 @@ Row {
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle { background: Rectangle {
color: Theme.surfaceContainer color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
border.color: Theme.primarySelected border.color: Theme.primarySelected
border.width: 0 border.width: 0
radius: Theme.cornerRadius radius: Theme.cornerRadius
@@ -207,9 +207,9 @@ Rectangle {
width: parent.width width: parent.width
height: 50 height: 50
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) color: deviceMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
border.color: modelData === AudioService.source ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) border.color: modelData === AudioService.source ? Theme.primary : Theme.outlineLight
border.width: 0 border.width: modelData === AudioService.source ? 2 : 1
Row { Row {
anchors.left: parent.left anchors.left: parent.left
@@ -218,9 +218,9 @@ Rectangle {
width: parent.width width: parent.width
height: 50 height: 50
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) color: deviceMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) border.color: modelData === AudioService.sink ? Theme.primary : Theme.outlineLight
border.width: 0 border.width: modelData === AudioService.sink ? 2 : 1
DankRipple { DankRipple {
id: deviceRipple id: deviceRipple
@@ -397,9 +397,9 @@ Rectangle {
width: parent.width width: parent.width
height: 50 height: 50
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) color: Theme.surfaceLight
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) border.color: modelData === AudioService.sink ? Theme.primary : Theme.outlineLight
border.width: 0 border.width: modelData === AudioService.sink ? 2 : 1
Row { Row {
anchors.left: parent.left anchors.left: parent.left
@@ -129,8 +129,9 @@ Rectangle {
width: (parent.width - Theme.spacingM) / 2 width: (parent.width - Theme.spacingM) / 2
height: 64 height: 64
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) color: Theme.surfaceLight
border.width: 0 border.color: Theme.outlineLight
border.width: 1
Column { Column {
anchors.centerIn: parent anchors.centerIn: parent
@@ -164,8 +165,9 @@ Rectangle {
width: (parent.width - Theme.spacingM) / 2 width: (parent.width - Theme.spacingM) / 2
height: 64 height: 64
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) color: Theme.surfaceLight
border.width: 0 border.color: Theme.outlineLight
border.width: 1
Column { Column {
anchors.centerIn: parent anchors.centerIn: parent
@@ -153,7 +153,7 @@ Item {
width: 320 width: 320
height: contentColumn.implicitHeight + Theme.spacingL * 2 height: contentColumn.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceContainer color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0 border.width: 0
opacity: modalVisible ? 1 : 0 opacity: modalVisible ? 1 : 0
@@ -229,7 +229,6 @@ Rectangle {
width: parent.width width: parent.width
height: 50 height: 50
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.width: 0
Component.onCompleted: { Component.onCompleted: {
if (!isConnected) if (!isConnected)
@@ -243,8 +242,8 @@ Rectangle {
if (isConnecting) if (isConnecting)
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12); return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
if (deviceMouseArea.containsMouse) if (deviceMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08); return Theme.primaryHoverLight;
return Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency); return Theme.surfaceLight;
} }
border.color: { border.color: {
@@ -252,8 +251,9 @@ Rectangle {
return Theme.warning; return Theme.warning;
if (isConnected) if (isConnected)
return Theme.primary; return Theme.primary;
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12); return Theme.outlineLight;
} }
border.width: (isConnecting || isConnected) ? 2 : 1
Row { Row {
anchors.left: parent.left anchors.left: parent.left
@@ -490,9 +490,9 @@ Rectangle {
width: parent.width width: parent.width
height: 50 height: 50
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: availableMouseArea.containsMouse && isInteractive ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) color: availableMouseArea.containsMouse && isInteractive ? Theme.primaryHoverLight : Theme.surfaceLight
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) border.color: Theme.outlineLight
border.width: 0 border.width: 1
opacity: isInteractive ? 1 : 0.6 opacity: isInteractive ? 1 : 0.6
Row { Row {
@@ -79,9 +79,9 @@ Rectangle {
width: parent.width width: parent.width
height: 80 height: 80
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) color: Theme.surfaceLight
border.color: modelData.mount === currentMountPath ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) border.color: modelData.mount === currentMountPath ? Theme.primary : Theme.outlineLight
border.width: modelData.mount === currentMountPath ? 2 : 0 border.width: modelData.mount === currentMountPath ? 2 : 1
Row { Row {
anchors.left: parent.left anchors.left: parent.left
@@ -308,9 +308,9 @@ Rectangle {
width: parent.width width: parent.width
height: wiredContentRow.implicitHeight + Theme.spacingM * 2 height: wiredContentRow.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: wiredNetworkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) color: wiredNetworkMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
border.color: Theme.primary border.color: isActive ? Theme.primary : Theme.outlineLight
border.width: 0 border.width: isActive ? 2 : 1
Row { Row {
id: wiredContentRow id: wiredContentRow
@@ -565,9 +565,9 @@ Rectangle {
width: wifiContent.width width: wifiContent.width
height: wifiContentRow.implicitHeight + Theme.spacingM * 2 height: wifiContentRow.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: networkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) color: networkMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
border.color: wifiDelegate.isConnected ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) border.color: wifiDelegate.isConnected ? Theme.primary : Theme.outlineLight
border.width: 0 border.width: wifiDelegate.isConnected ? 2 : 1
Row { Row {
id: wifiContentRow id: wifiContentRow
@@ -14,6 +14,7 @@ Item {
property real barThickness: 48 property real barThickness: 48
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null
property bool overrideAxisLayout: false property bool overrideAxisLayout: false
property bool forceVerticalLayout: false property bool forceVerticalLayout: false
@@ -357,6 +358,7 @@ Item {
barThickness: root.barThickness barThickness: root.barThickness
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow
isFirst: index === 0 isFirst: index === 0
isLast: index === centerRepeater.count - 1 isLast: index === centerRepeater.count - 1
sectionSpacing: parent.itemSpacing sectionSpacing: parent.itemSpacing
+51 -3
View File
@@ -14,6 +14,8 @@ Item {
required property var rootWindow required property var rootWindow
required property var barConfig required property var barConfig
readonly property var blurBarWindow: barWindow
property var leftWidgetsModel property var leftWidgetsModel
property var centerWidgetsModel property var centerWidgetsModel
property var rightWidgetsModel property var rightWidgetsModel
@@ -408,6 +410,12 @@ Item {
value: topBarContent.barConfig value: topBarContent.barConfig
restoreMode: Binding.RestoreNone restoreMode: Binding.RestoreNone
} }
Binding {
target: hLeftSection
property: "blurBarWindow"
value: topBarContent.blurBarWindow
restoreMode: Binding.RestoreNone
}
RightSection { RightSection {
id: hRightSection id: hRightSection
@@ -434,6 +442,12 @@ Item {
value: topBarContent.barConfig value: topBarContent.barConfig
restoreMode: Binding.RestoreNone restoreMode: Binding.RestoreNone
} }
Binding {
target: hRightSection
property: "blurBarWindow"
value: topBarContent.blurBarWindow
restoreMode: Binding.RestoreNone
}
CenterSection { CenterSection {
id: hCenterSection id: hCenterSection
@@ -460,6 +474,12 @@ Item {
value: topBarContent.barConfig value: topBarContent.barConfig
restoreMode: Binding.RestoreNone restoreMode: Binding.RestoreNone
} }
Binding {
target: hCenterSection
property: "blurBarWindow"
value: topBarContent.blurBarWindow
restoreMode: Binding.RestoreNone
}
} }
Item { Item {
@@ -493,6 +513,12 @@ Item {
value: topBarContent.barConfig value: topBarContent.barConfig
restoreMode: Binding.RestoreNone restoreMode: Binding.RestoreNone
} }
Binding {
target: vLeftSection
property: "blurBarWindow"
value: topBarContent.blurBarWindow
restoreMode: Binding.RestoreNone
}
CenterSection { CenterSection {
id: vCenterSection id: vCenterSection
@@ -520,6 +546,12 @@ Item {
value: topBarContent.barConfig value: topBarContent.barConfig
restoreMode: Binding.RestoreNone restoreMode: Binding.RestoreNone
} }
Binding {
target: vCenterSection
property: "blurBarWindow"
value: topBarContent.blurBarWindow
restoreMode: Binding.RestoreNone
}
RightSection { RightSection {
id: vRightSection id: vRightSection
@@ -548,6 +580,12 @@ Item {
value: topBarContent.barConfig value: topBarContent.barConfig
restoreMode: Binding.RestoreNone restoreMode: Binding.RestoreNone
} }
Binding {
target: vRightSection
property: "blurBarWindow"
value: topBarContent.blurBarWindow
restoreMode: Binding.RestoreNone
}
} }
} }
@@ -931,6 +969,7 @@ 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
@@ -1399,12 +1438,21 @@ 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 (systemUpdateLoader.item && systemUpdateLoader.item.setBarContext) { if (popout.setBarContext) {
systemUpdateLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0); popout.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0);
} }
systemUpdateLoader.item?.toggle(); if (popout.setTriggerPosition) {
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");
} }
} }
} }
+108 -1
View File
@@ -97,6 +97,112 @@ PanelWindow {
} }
} }
property var blurRegion: null
property var _blurWidgetItems: []
function registerBlurWidget(item) {
if (_blurWidgetItems.indexOf(item) >= 0)
return;
_blurWidgetItems = _blurWidgetItems.concat([item]);
_blurRebuildTimer.restart();
}
function unregisterBlurWidget(item) {
const idx = _blurWidgetItems.indexOf(item);
if (idx < 0)
return;
const arr = _blurWidgetItems.slice();
arr.splice(idx, 1);
_blurWidgetItems = arr;
_blurRebuildTimer.restart();
}
Timer {
id: _blurRebuildTimer
interval: 1
onTriggered: barBlur.rebuild()
}
Item {
id: barBlur
visible: false
readonly property bool barHasTransparency: barWindow._backgroundAlpha > 0 && barWindow._backgroundAlpha < 1
function rebuild() {
teardown();
if (!BlurService.enabled || !BlurService.available)
return;
const widgets = barWindow._blurWidgetItems.filter(w => w && w.visible && w.width > 0 && w.height > 0);
const hasBar = barHasTransparency;
if (!hasBar && widgets.length === 0)
return;
const cr = Theme.cornerRadius;
let qml = 'import QtQuick; import Quickshell; Region {';
for (let i = 0; i < widgets.length; i++) {
qml += ` property Item w${i}; Region { item: w${i}; radius: ${cr} }`;
}
qml += '}';
try {
const region = Qt.createQmlObject(qml, barWindow, "BarBlurRegion");
if (hasBar) {
region.x = Qt.binding(() => topBarMouseArea.x + barUnitInset.x + topBarSlide.x);
region.y = Qt.binding(() => topBarMouseArea.y + barUnitInset.y + topBarSlide.y);
region.width = Qt.binding(() => barUnitInset.width);
region.height = Qt.binding(() => barUnitInset.height);
region.radius = Qt.binding(() => barBackground.rt);
}
for (let i = 0; i < widgets.length; i++) {
region[`w${i}`] = widgets[i];
}
barWindow.BackgroundEffect.blurRegion = region;
barWindow.blurRegion = region;
} catch (e) {
console.warn("BarBlur: Failed to create blur region:", e);
}
}
function teardown() {
if (!barWindow.blurRegion)
return;
try {
barWindow.BackgroundEffect.blurRegion = null;
} catch (e) {}
barWindow.blurRegion.destroy();
barWindow.blurRegion = null;
}
onBarHasTransparencyChanged: _blurRebuildTimer.restart()
Connections {
target: BlurService
function onEnabledChanged() {
barBlur.rebuild();
}
}
Connections {
target: topBarSlide
function onXChanged() {
if (barWindow.blurRegion)
barWindow.blurRegion.changed();
}
function onYChanged() {
if (barWindow.blurRegion)
barWindow.blurRegion.changed();
}
}
Component.onCompleted: rebuild()
Component.onDestruction: teardown()
}
WlrLayershell.layer: dBarLayer WlrLayershell.layer: dBarLayer
WlrLayershell.namespace: "dms:bar" WlrLayershell.namespace: "dms:bar"
@@ -711,7 +817,8 @@ PanelWindow {
onHasActivePopoutChanged: evaluateReveal() onHasActivePopoutChanged: evaluateReveal()
function updateActivePopoutState() { function updateActivePopoutState() {
if (!barWindow.screen) return; if (!barWindow.screen)
return;
const screenName = barWindow.screen.name; const screenName = barWindow.screen.name;
const activePopout = PopoutManager.currentPopoutsByScreen[screenName]; const activePopout = PopoutManager.currentPopoutsByScreen[screenName];
const activeTrayMenu = TrayMenuManager.activeTrayMenus[screenName]; const activeTrayMenu = TrayMenuManager.activeTrayMenus[screenName];
@@ -13,6 +13,7 @@ Item {
property real barThickness: 48 property real barThickness: 48
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null
property bool overrideAxisLayout: false property bool overrideAxisLayout: false
property bool forceVerticalLayout: false property bool forceVerticalLayout: false
@@ -59,6 +60,7 @@ Item {
barThickness: root.barThickness barThickness: root.barThickness
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow
isFirst: index === 0 isFirst: index === 0
isLast: index === rowRepeater.count - 1 isLast: index === rowRepeater.count - 1
sectionSpacing: parent.rowSpacing sectionSpacing: parent.rowSpacing
@@ -103,6 +105,7 @@ Item {
barThickness: root.barThickness barThickness: root.barThickness
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow
isFirst: index === 0 isFirst: index === 0
isLast: index === columnRepeater.count - 1 isLast: index === columnRepeater.count - 1
sectionSpacing: parent.columnSpacing sectionSpacing: parent.columnSpacing
@@ -13,6 +13,7 @@ Item {
property real barThickness: 48 property real barThickness: 48
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null
property bool overrideAxisLayout: false property bool overrideAxisLayout: false
property bool forceVerticalLayout: false property bool forceVerticalLayout: false
@@ -61,6 +62,7 @@ Item {
barThickness: root.barThickness barThickness: root.barThickness
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow
isFirst: index === 0 isFirst: index === 0
isLast: index === rowRepeater.count - 1 isLast: index === rowRepeater.count - 1
sectionSpacing: parent.rowSpacing sectionSpacing: parent.rowSpacing
@@ -105,6 +107,7 @@ Item {
barThickness: root.barThickness barThickness: root.barThickness
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow
isFirst: index === 0 isFirst: index === 0
isLast: index === columnRepeater.count - 1 isLast: index === columnRepeater.count - 1
sectionSpacing: parent.columnSpacing sectionSpacing: parent.columnSpacing
@@ -16,6 +16,7 @@ Loader {
property real barThickness: 48 property real barThickness: 48
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null
property bool isFirst: false property bool isFirst: false
property bool isLast: false property bool isLast: false
property real sectionSpacing: 0 property real sectionSpacing: 0
@@ -92,6 +93,14 @@ Loader {
restoreMode: Binding.RestoreNone restoreMode: Binding.RestoreNone
} }
Binding {
target: root.item
when: root.item && "blurBarWindow" in root.item
property: "blurBarWindow"
value: root.blurBarWindow
restoreMode: Binding.RestoreNone
}
Binding { Binding {
target: root.item target: root.item
when: root.item && "axis" in root.item when: root.item && "axis" in root.item
@@ -630,7 +630,7 @@ BasePill {
if (appItem.isFocused && colorizeEnabled) { if (appItem.isFocused && colorizeEnabled) {
return mouseArea.containsMouse ? Theme.withAlpha(Qt.lighter(appItem.activeOverlayColor, 1.3), 0.4) : Theme.withAlpha(appItem.activeOverlayColor, 0.3); return mouseArea.containsMouse ? Theme.withAlpha(Qt.lighter(appItem.activeOverlayColor, 1.3), 0.4) : Theme.withAlpha(appItem.activeOverlayColor, 0.3);
} }
return mouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"; return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
} }
border.width: dragHandler.dragging ? 2 : 0 border.width: dragHandler.dragging ? 2 : 0
@@ -1,4 +1,5 @@
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
@@ -10,6 +11,8 @@ 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":
@@ -119,5 +122,44 @@ 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");
}
}
} }
} }
@@ -3,6 +3,7 @@ import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Modules.Plugins import qs.Modules.Plugins
import qs.Services
import qs.Widgets import qs.Widgets
BasePill { BasePill {
@@ -93,6 +94,15 @@ BasePill {
PanelWindow { PanelWindow {
id: contextMenuWindow id: contextMenuWindow
WindowBlur {
targetWindow: contextMenuWindow
blurX: menuContainer.x
blurY: menuContainer.y
blurWidth: contextMenuWindow.visible ? menuContainer.width : 0
blurHeight: contextMenuWindow.visible ? menuContainer.height : 0
blurRadius: Theme.cornerRadius
}
WlrLayershell.namespace: "dms:clipboard-context-menu" WlrLayershell.namespace: "dms:clipboard-context-menu"
property bool isVertical: false property bool isVertical: false
@@ -187,8 +197,8 @@ BasePill {
height: Math.max(64, menuColumn.implicitHeight + Theme.spacingS * 2) height: Math.max(64, menuColumn.implicitHeight + Theme.spacingS * 2)
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1 border.width: BlurService.enabled ? BlurService.borderWidth : 1
opacity: contextMenuWindow.visible ? 1 : 0 opacity: contextMenuWindow.visible ? 1 : 0
visible: opacity > 0 visible: opacity > 0
@@ -224,7 +234,7 @@ BasePill {
width: parent.width width: parent.width
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: clearAllArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: clearAllArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
Row { Row {
anchors.fill: parent anchors.fill: parent
@@ -264,7 +274,7 @@ BasePill {
width: parent.width width: parent.width
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: savedItemsArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: savedItemsArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
Row { Row {
anchors.fill: parent anchors.fill: parent
@@ -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: "88%" text: "100%"
} }
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
readonly property Toplevel activeWindow: ToplevelManager.activeToplevel property Toplevel activeWindow: null
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,10 +38,44 @@ 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() {
+116 -55
View File
@@ -19,7 +19,8 @@ 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 int textWidth: { readonly property bool adaptiveWidthEnabled: SettingsData.mediaAdaptiveWidthEnabled
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:
@@ -36,10 +37,7 @@ BasePill {
if (isVerticalOrientation) { if (isVerticalOrientation) {
return widgetThickness - horizontalPadding * 2; return widgetThickness - horizontalPadding * 2;
} }
const controlsWidth = 20 + Theme.spacingXS + 24 + Theme.spacingXS + 20; return 0;
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) {
@@ -99,7 +97,7 @@ BasePill {
if (isMouseWheelY) { if (isMouseWheelY) {
if (deltaY > 0) { if (deltaY > 0) {
activePlayer.previous(); MprisController.previousOrRewind();
} else { } else {
activePlayer.next(); activePlayer.next();
} }
@@ -107,7 +105,7 @@ BasePill {
scrollAccumulatorY += deltaY; scrollAccumulatorY += deltaY;
if (Math.abs(scrollAccumulatorY) >= touchpadThreshold) { if (Math.abs(scrollAccumulatorY) >= touchpadThreshold) {
if (scrollAccumulatorY > 0) { if (scrollAccumulatorY > 0) {
activePlayer.previous(); MprisController.previousOrRewind();
} else { } else {
activePlayer.next(); activePlayer.next();
} }
@@ -119,7 +117,28 @@ BasePill {
content: Component { content: Component {
Item { Item {
implicitWidth: root.playerAvailable ? root.currentContentWidth : 0 id: contentRoot
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
@@ -132,8 +151,9 @@ BasePill {
Behavior on implicitWidth { Behavior on implicitWidth {
NumberAnimation { NumberAnimation {
duration: Theme.shortDuration duration: Theme.mediumDuration
easing.type: Theme.standardEasing easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
} }
} }
@@ -214,7 +234,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) {
activePlayer.previous(); MprisController.previousOrRewind();
} else if (mouse.button === Qt.RightButton) { } else if (mouse.button === Qt.RightButton) {
activePlayer.next(); activePlayer.next();
} }
@@ -269,7 +289,7 @@ BasePill {
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: textWidth width: contentRoot.measuredTextWidth
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;
@@ -278,50 +298,95 @@ BasePill {
clip: true clip: true
color: "transparent" color: "transparent"
StyledText { Behavior on width {
id: mediaText NumberAnimation {
property bool needsScrolling: implicitWidth > textContainer.width && SettingsData.scrollTitleEnabled duration: Theme.mediumDuration
property real scrollOffset: 0 easing.type: Easing.BezierSpline
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();
} }
}
SequentialAnimation { Item {
id: scrollAnimation id: textClip
running: mediaText.needsScrolling && textContainer.visible anchors.fill: parent
loops: Animation.Infinite clip: true
PauseAnimation { StyledText {
duration: 2000 id: mediaText
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();
} }
NumberAnimation { SequentialAnimation {
target: mediaText id: scrollAnimation
property: "scrollOffset" running: mediaText.needsScrolling && textContainer.visible
from: 0 loops: Animation.Infinite
to: mediaText.implicitWidth - textContainer.width + 5
duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60) PauseAnimation {
easing.type: Easing.Linear duration: 2000
}
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
}
} }
PauseAnimation { SequentialAnimation {
duration: 2000 id: textChangeAnimation
}
NumberAnimation { ParallelAnimation {
target: mediaText NumberAnimation {
property: "scrollOffset" target: mediaText
to: 0 property: "opacity"
duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60) from: 0.7
easing.type: Easing.Linear to: 1
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
}
}
} }
} }
} }
@@ -354,7 +419,7 @@ BasePill {
height: 20 height: 20
radius: 10 radius: 10
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color: prevArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: prevArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
visible: root.playerAvailable visible: root.playerAvailable
opacity: (activePlayer && activePlayer.canGoPrevious) ? 1 : 0.3 opacity: (activePlayer && activePlayer.canGoPrevious) ? 1 : 0.3
@@ -370,11 +435,7 @@ BasePill {
anchors.fill: parent anchors.fill: parent
enabled: root.playerAvailable enabled: root.playerAvailable
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: MprisController.previousOrRewind()
if (activePlayer) {
activePlayer.previous();
}
}
} }
} }
@@ -411,7 +472,7 @@ BasePill {
height: 20 height: 20
radius: 10 radius: 10
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color: nextArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: nextArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
visible: playerAvailable visible: playerAvailable
opacity: (activePlayer && activePlayer.canGoNext) ? 1 : 0.3 opacity: (activePlayer && activePlayer.canGoNext) ? 1 : 0.3
@@ -285,7 +285,7 @@ BasePill {
width: parent.width width: parent.width
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: tabArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: tabArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
Row { Row {
anchors.fill: parent anchors.fill: parent
@@ -327,7 +327,7 @@ BasePill {
width: parent.width width: parent.width
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: newNoteArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: newNoteArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
Row { Row {
anchors.fill: parent anchors.fill: parent
@@ -271,9 +271,9 @@ BasePill {
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
if (isFocused) { if (isFocused) {
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.2); return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.45);
} }
return mouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"; return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
} }
// App icon // App icon
@@ -526,9 +526,9 @@ BasePill {
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
if (isFocused) { if (isFocused) {
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.2); return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.45);
} }
return mouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"; return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
} }
IconImage { IconImage {
@@ -738,6 +738,15 @@ BasePill {
sourceComponent: PanelWindow { sourceComponent: PanelWindow {
id: contextMenuWindow id: contextMenuWindow
WindowBlur {
targetWindow: contextMenuWindow
blurX: contextMenuRect.x
blurY: contextMenuRect.y
blurWidth: contextMenuWindow.isVisible ? contextMenuRect.width : 0
blurHeight: contextMenuWindow.isVisible ? contextMenuRect.height : 0
blurRadius: Theme.cornerRadius
}
property var currentWindow: null property var currentWindow: null
property bool isVisible: false property bool isVisible: false
property point anchorPos: Qt.point(0, 0) property point anchorPos: Qt.point(0, 0)
@@ -830,6 +839,7 @@ BasePill {
} }
Rectangle { Rectangle {
id: contextMenuRect
x: { x: {
if (contextMenuWindow.isVertical) { if (contextMenuWindow.isVertical) {
if (contextMenuWindow.edge === "left") { if (contextMenuWindow.edge === "left") {
@@ -858,13 +868,13 @@ BasePill {
height: 32 height: 32
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.width: 1 border.width: BlurService.enabled ? BlurService.borderWidth : 1
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: parent.radius radius: parent.radius
color: closeMouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: closeMouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
} }
StyledText { StyledText {
@@ -16,8 +16,11 @@ 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()) : [];
@@ -40,6 +43,76 @@ 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");
@@ -78,6 +151,13 @@ 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)
@@ -103,6 +183,7 @@ 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
@@ -198,10 +279,11 @@ 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.mainBarItems values: root.displayedMainBarItems
objectProp: "key" objectProp: "key"
} }
@@ -209,29 +291,7 @@ 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: { property string iconSource: root.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 "";
}
width: root.trayItemSize width: root.trayItemSize
height: root.barThickness height: root.barThickness
@@ -287,7 +347,7 @@ BasePill {
height: root.trayItemSize height: root.trayItemSize
anchors.centerIn: parent anchors.centerIn: parent
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: trayItemArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: trayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
border.width: dragHandler.dragging ? 2 : 0 border.width: dragHandler.dragging ? 2 : 0
border.color: Theme.primary border.color: Theme.primary
opacity: dragHandler.dragging ? 0.8 : 1.0 opacity: dragHandler.dragging ? 0.8 : 1.0
@@ -371,7 +431,8 @@ BasePill {
} }
if (!delegateRoot.trayItem.hasMenu) if (!delegateRoot.trayItem.hasMenu)
return; return;
root.menuOpen = false; if (root.useOverflowPopup)
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);
} }
@@ -380,8 +441,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 = index; root.draggedIndex = root.reverseInlineHorizontal ? (root.mainBarItems.length - 1 - index) : index;
root.dropTargetIndex = index; root.dropTargetIndex = root.draggedIndex;
} }
} }
if (!dragHandler.dragging) if (!dragHandler.dragging)
@@ -391,7 +452,8 @@ 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 newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset)); const visualTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
const newTargetIndex = root.reverseInlineHorizontal ? (root.mainBarItems.length - 1 - visualTargetIndex) : visualTargetIndex;
if (newTargetIndex !== root.dropTargetIndex) { if (newTargetIndex !== root.dropTargetIndex) {
root.dropTargetIndex = newTargetIndex; root.dropTargetIndex = newTargetIndex;
} }
@@ -407,7 +469,8 @@ 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;
} }
root.menuOpen = false; if (root.useOverflowPopup)
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);
} }
} }
@@ -425,11 +488,11 @@ BasePill {
height: root.trayItemSize height: root.trayItemSize
anchors.centerIn: parent anchors.centerIn: parent
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: caretArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: caretArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: root.menuOpen ? "expand_less" : "expand_more" name: root.toggleIconName()
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
} }
@@ -451,6 +514,301 @@ 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);
}
}
} }
} }
@@ -459,219 +817,23 @@ 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.mainBarItems values: root.reverseInlineVertical ? [] : root.displayedMainBarItems
objectProp: "key" objectProp: "key"
} }
delegate: verticalMainTrayItemDelegate
}
delegate: Item { Repeater {
id: delegateRoot model: ScriptModel {
property var trayItem: modelData.item values: root.reverseInlineVertical ? root.displayedInlineExpandedItems : []
property string itemKey: modelData.key objectProp: "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 ? 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 {
@@ -685,18 +847,11 @@ BasePill {
height: root.trayItemSize height: root.trayItemSize
anchors.centerIn: parent anchors.centerIn: parent
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: caretAreaVert.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: caretAreaVert.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: { name: root.toggleIconName()
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
} }
@@ -718,12 +873,38 @@ 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
}
} }
} }
PanelWindow { PanelWindow {
id: overflowMenu id: overflowMenu
visible: root.menuOpen
WindowBlur {
targetWindow: overflowMenu
blurX: menuContainer.x
blurY: menuContainer.y
blurWidth: root.menuOpen ? menuContainer.width : 0
blurHeight: root.menuOpen ? menuContainer.height : 0
blurRadius: Theme.cornerRadius
}
visible: root.useOverflowPopup && root.menuOpen
screen: root.parentScreen screen: root.parentScreen
WlrLayershell.layer: WlrLayershell.Top WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.exclusiveZone: -1 WlrLayershell.exclusiveZone: -1
@@ -739,13 +920,14 @@ BasePill {
HyprlandFocusGrab { HyprlandFocusGrab {
windows: [overflowMenu] windows: [overflowMenu]
active: CompositorService.useHyprlandFocusGrab && root.menuOpen active: CompositorService.useHyprlandFocusGrab && root.useOverflowPopup && root.menuOpen
} }
Connections { Connections {
target: PopoutManager target: PopoutManager
function onPopoutOpening() { function onPopoutOpening() {
root.menuOpen = false; if (root.useOverflowPopup)
root.menuOpen = false;
} }
} }
@@ -990,6 +1172,15 @@ BasePill {
layer.samples: 4 layer.samples: 4
} }
Rectangle {
anchors.fill: parent
color: "transparent"
radius: Theme.cornerRadius
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
z: 100
}
Grid { Grid {
id: menuGrid id: menuGrid
anchors.centerIn: parent anchors.centerIn: parent
@@ -1002,35 +1193,12 @@ BasePill {
delegate: Rectangle { delegate: Rectangle {
property var trayItem: modelData property var trayItem: modelData
property string iconSource: { property string iconSource: root.trayIconSourceFor(trayItem)
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
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: itemArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.withAlpha(Theme.surfaceContainer, 0) color: itemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0)
IconImage { IconImage {
id: menuIconImg id: menuIconImg
@@ -1191,6 +1359,15 @@ BasePill {
PanelWindow { PanelWindow {
id: menuWindow id: menuWindow
WindowBlur {
targetWindow: menuWindow
blurX: trayMenuContainer.x
blurY: trayMenuContainer.y
blurWidth: menuRoot.showMenu ? trayMenuContainer.width : 0
blurHeight: menuRoot.showMenu ? trayMenuContainer.height : 0
blurRadius: Theme.cornerRadius
}
WlrLayershell.namespace: "dms:tray-menu-window" WlrLayershell.namespace: "dms:tray-menu-window"
visible: menuRoot.showMenu && (menuRoot.trayItem?.hasMenu ?? false) visible: menuRoot.showMenu && (menuRoot.trayItem?.hasMenu ?? false)
WlrLayershell.layer: WlrLayershell.Top WlrLayershell.layer: WlrLayershell.Top
@@ -1285,7 +1462,8 @@ BasePill {
onVisibleChanged: { onVisibleChanged: {
if (visible) { if (visible) {
updatePosition(); updatePosition();
root.menuOpen = false; if (root.useOverflowPopup)
root.menuOpen = false;
PopoutManager.closeAllPopouts(); PopoutManager.closeAllPopouts();
ModalManager.closeAllModalsExcept(null); ModalManager.closeAllModalsExcept(null);
} }
@@ -1302,7 +1480,7 @@ BasePill {
onClicked: mouse => { onClicked: mouse => {
const clickX = mouse.x + menuWindow.maskX; const clickX = mouse.x + menuWindow.maskX;
const clickY = mouse.y + menuWindow.maskY; const clickY = mouse.y + menuWindow.maskY;
const outsideContent = clickX < menuContainer.x || clickX > menuContainer.x + menuContainer.width || clickY < menuContainer.y || clickY > menuContainer.y + menuContainer.height; const outsideContent = clickX < trayMenuContainer.x || clickX > trayMenuContainer.x + trayMenuContainer.width || clickY < trayMenuContainer.y || clickY > trayMenuContainer.y + trayMenuContainer.height;
if (!outsideContent) if (!outsideContent)
return; return;
@@ -1360,7 +1538,7 @@ BasePill {
} }
Item { Item {
id: menuContainer id: trayMenuContainer
readonly property real rawWidth: Math.min(500, Math.max(250, menuColumn.implicitWidth + Theme.spacingS * 2)) readonly property real rawWidth: Math.min(500, Math.max(250, menuColumn.implicitWidth + Theme.spacingS * 2))
readonly property real rawHeight: Math.max(40, menuColumn.implicitHeight + Theme.spacingS * 2) readonly property real rawHeight: Math.max(40, menuColumn.implicitHeight + Theme.spacingS * 2)
@@ -1438,6 +1616,15 @@ BasePill {
layer.textureMirroring: ShaderEffectSource.MirrorVertically layer.textureMirroring: ShaderEffectSource.MirrorVertically
} }
Rectangle {
anchors.fill: parent
color: "transparent"
radius: Theme.cornerRadius
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
z: 100
}
QsMenuAnchor { QsMenuAnchor {
id: submenuHydrator id: submenuHydrator
anchor.window: menuWindow anchor.window: menuWindow
@@ -1470,7 +1657,7 @@ BasePill {
width: parent.width width: parent.width
height: 28 height: 28
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: visibilityToggleArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.withAlpha(Theme.surfaceContainer, 0) color: visibilityToggleArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0)
StyledText { StyledText {
anchors.left: parent.left anchors.left: parent.left
@@ -1523,7 +1710,7 @@ BasePill {
width: parent.width width: parent.width
height: 28 height: 28
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: backArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.withAlpha(Theme.surfaceContainer, 0) color: backArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0)
Row { Row {
anchors.left: parent.left anchors.left: parent.left
@@ -1574,7 +1761,7 @@ BasePill {
color: { color: {
if (menuEntry?.isSeparator) if (menuEntry?.isSeparator)
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2); return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2);
return itemArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.withAlpha(Theme.surfaceContainer, 0); return itemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0);
} }
MouseArea { MouseArea {
@@ -17,8 +17,49 @@ Item {
property real widgetHeight: 30 property real widgetHeight: 30
property real barThickness: 48 property real barThickness: 48
property var barConfig: null property var barConfig: null
property var blurBarWindow: null
property var hyprlandOverviewLoader: null property var hyprlandOverviewLoader: null
property var parentScreen: null property var parentScreen: null
readonly property real _leftMargin: {
if (isVertical)
return 0;
root.x;
if (!root.parent)
return 0;
const gap = root.mapToItem(null, 0, 0).x;
return (gap > 0 && gap < 30) ? gap + 5 : 0;
}
readonly property real _rightMargin: {
if (isVertical)
return 0;
root.x;
root.width;
if (!root.parent || !blurBarWindow)
return 0;
const gap = blurBarWindow.width - root.mapToItem(null, root.width, 0).x;
return (gap > 0 && gap < 30) ? gap + 5 : 0;
}
readonly property real _topMargin: {
if (!isVertical)
return 0;
root.y;
if (!root.parent)
return 0;
const gap = root.mapToItem(null, 0, 0).y;
return (gap > 0 && gap < 30) ? gap + 5 : 0;
}
readonly property real _bottomMargin: {
if (!isVertical)
return 0;
root.y;
root.height;
if (!root.parent || !blurBarWindow)
return 0;
const gap = blurBarWindow.height - root.mapToItem(null, 0, root.height).y;
return (gap > 0 && gap < 30) ? gap + 5 : 0;
}
property int _desktopEntriesUpdateTrigger: 0 property int _desktopEntriesUpdateTrigger: 0
readonly property var sortedToplevels: { readonly property var sortedToplevels: {
return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName); return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName);
@@ -538,6 +579,60 @@ Item {
}); });
} }
function switchToWorkspaceByModelData(data) {
if (!data)
return;
if (root.useExtWorkspace && (data.id || data.name)) {
ExtWorkspaceService.activateWorkspace(data.id || data.name, data.groupID || "");
return;
}
switch (CompositorService.compositor) {
case "niri":
if (data.idx !== undefined)
NiriService.switchToWorkspace(data.idx);
break;
case "hyprland":
if (data.id)
Hyprland.dispatch(`workspace ${data.id}`);
break;
case "dwl":
if (data.tag !== undefined)
DwlService.switchToTag(root.screenName, data.tag);
break;
case "sway":
case "scroll":
case "miracle":
if (data.num)
try {
I3.dispatch(`workspace number ${data.num}`);
} catch (_) {}
break;
}
}
function findClosestWorkspaceIndex(localX, localY) {
if (workspaceRepeater.count === 0)
return -1;
let closestIdx = -1;
let closestDist = Infinity;
for (let i = 0; i < workspaceRepeater.count; i++) {
const item = workspaceRepeater.itemAt(i);
if (!item)
continue;
const center = item.mapToItem(root, item.width / 2, item.height / 2);
const dist = isVertical ? Math.abs(localY - center.y) : Math.abs(localX - center.x);
if (dist < closestDist) {
closestDist = dist;
closestIdx = i;
}
}
return closestIdx;
}
function switchWorkspace(direction) { function switchWorkspace(direction) {
if (useExtWorkspace) { if (useExtWorkspace) {
const realWorkspaces = getRealWorkspaces(); const realWorkspaces = getRealWorkspaces();
@@ -751,8 +846,15 @@ Item {
} }
MouseArea { MouseArea {
anchors.fill: parent id: edgeMouseArea
acceptedButtons: Qt.RightButton z: -1
x: -root._leftMargin
y: -root._topMargin
width: root.width + root._leftMargin + root._rightMargin
height: root.height + root._topMargin + root._bottomMargin
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
property real touchpadAccumulator: 0 property real touchpadAccumulator: 0
property real mouseAccumulator: 0 property real mouseAccumulator: 0
@@ -765,12 +867,20 @@ Item {
} }
onClicked: mouse => { onClicked: mouse => {
if (mouse.button === Qt.RightButton) { const rootPos = edgeMouseArea.mapToItem(root, mouse.x, mouse.y);
switch (mouse.button) {
case Qt.RightButton:
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
NiriService.toggleOverview(); NiriService.toggleOverview();
} else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) { } else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen; root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen;
} }
break;
case Qt.LeftButton:
const idx = root.findClosestWorkspaceIndex(rootPos.x, rootPos.y);
if (idx >= 0)
root.switchToWorkspaceByModelData(root.workspaceList[idx]);
break;
} }
} }
@@ -1845,5 +1955,27 @@ Item {
if (useExtWorkspace && !DMSService.activeSubscriptions.includes("extworkspace")) { if (useExtWorkspace && !DMSService.activeSubscriptions.includes("extworkspace")) {
DMSService.addSubscription("extworkspace"); DMSService.addSubscription("extworkspace");
} }
_updateBlurRegistration();
}
property bool _blurRegistered: false
readonly property bool _shouldBlur: BlurService.enabled && blurBarWindow && blurBarWindow.registerBlurWidget && !(barConfig?.noBackground ?? false) && root.visible && root.width > 0
on_ShouldBlurChanged: _updateBlurRegistration()
function _updateBlurRegistration() {
if (_shouldBlur && !_blurRegistered) {
blurBarWindow.registerBlurWidget(visualBackground);
_blurRegistered = true;
} else if (!_shouldBlur && _blurRegistered) {
if (blurBarWindow && blurBarWindow.unregisterBlurWidget)
blurBarWindow.unregisterBlurWidget(visualBackground);
_blurRegistered = false;
}
}
Component.onDestruction: {
if (_blurRegistered && blurBarWindow && blurBarWindow.unregisterBlurWidget)
blurBarWindow.unregisterBlurWidget(visualBackground);
} }
} }
@@ -100,7 +100,7 @@ DankPopout {
if (currentPlayer && currentPlayer !== player && currentPlayer.canPause) { if (currentPlayer && currentPlayer !== player && currentPlayer.canPause) {
currentPlayer.pause(); currentPlayer.pause();
} }
MprisController.activePlayer = player; MprisController.setActivePlayer(player);
root.__hideDropdowns(); root.__hideDropdowns();
} }
onDeviceSelected: device => { onDeviceSelected: device => {
+1 -11
View File
@@ -487,17 +487,7 @@ Item {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: MprisController.previousOrRewind()
if (!activePlayer) {
return;
}
if (activePlayer.position > 8 && activePlayer.canSeek) {
activePlayer.position = 0;
} else {
activePlayer.previous();
}
}
} }
} }
} }
@@ -145,14 +145,7 @@ Card {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: MprisController.previousOrRewind()
if (!activePlayer) return
if (activePlayer.position > 8 && activePlayer.canSeek) {
activePlayer.position = 0
} else {
activePlayer.previous()
}
}
} }
} }
+18
View File
@@ -17,6 +17,15 @@ Variants {
delegate: PanelWindow { delegate: PanelWindow {
id: dock id: dock
WindowBlur {
targetWindow: dock
blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x
blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y
blurWidth: dock.hasApps && dock.reveal ? dockBackground.width : 0
blurHeight: dock.hasApps && dock.reveal ? dockBackground.height : 0
blurRadius: Theme.cornerRadius
}
WlrLayershell.namespace: "dms:dock" WlrLayershell.namespace: "dms:dock"
readonly property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right readonly property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
@@ -562,6 +571,15 @@ Variants {
color: Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency) color: Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
} }
Rectangle {
anchors.fill: parent
color: "transparent"
radius: Theme.cornerRadius
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
z: 100
}
} }
Shape { Shape {
+11 -2
View File
@@ -9,6 +9,15 @@ import qs.Widgets
PanelWindow { PanelWindow {
id: root id: root
WindowBlur {
targetWindow: root
blurX: menuContainer.x
blurY: menuContainer.y
blurWidth: root.visible ? menuContainer.width : 0
blurHeight: root.visible ? menuContainer.height : 0
blurRadius: Theme.cornerRadius
}
WlrLayershell.namespace: "dms:dock-context-menu" WlrLayershell.namespace: "dms:dock-context-menu"
property var appData: null property var appData: null
@@ -168,8 +177,8 @@ PanelWindow {
height: menuColumn.implicitHeight + Theme.spacingS * 2 height: menuColumn.implicitHeight + Theme.spacingS * 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1 border.width: BlurService.enabled ? BlurService.borderWidth : 1
opacity: root.visible ? 1 : 0 opacity: root.visible ? 1 : 0
visible: opacity > 0 visible: opacity > 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.activePlayer?.previous() onClicked: MprisController.previousOrRewind()
} }
} }
@@ -1,6 +1,5 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Services.Notifications import Quickshell.Services.Notifications
@@ -11,6 +10,15 @@ import qs.Widgets
PanelWindow { PanelWindow {
id: win id: win
WindowBlur {
targetWindow: win
blurX: content.x + content.cardInset + swipeTx.x + tx.x
blurY: content.y + content.cardInset + swipeTx.y + tx.y
blurWidth: !win._finalized ? Math.max(0, content.width - content.cardInset * 2) : 0
blurHeight: !win._finalized ? Math.max(0, content.height - content.cardInset * 2) : 0
blurRadius: Theme.cornerRadius
}
WlrLayershell.namespace: "dms:notification-popup" WlrLayershell.namespace: "dms:notification-popup"
required property var notificationData required property var notificationData
@@ -436,6 +444,16 @@ PanelWindow {
} }
} }
Rectangle {
anchors.fill: parent
anchors.margins: content.cardInset
radius: Theme.cornerRadius
color: "transparent"
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
z: 100
}
Item { Item {
id: backgroundContainer id: backgroundContainer
anchors.fill: parent anchors.fill: parent
+24 -1
View File
@@ -14,6 +14,7 @@ Item {
property real barThickness: 48 property real barThickness: 48
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null
property alias content: contentLoader.sourceComponent property alias content: contentLoader.sourceComponent
property bool isVerticalOrientation: axis?.isVertical ?? false property bool isVerticalOrientation: axis?.isVertical ?? false
property bool isFirst: false property bool isFirst: false
@@ -106,7 +107,7 @@ Item {
const rawTransparency = (root.barConfig && root.barConfig.widgetTransparency !== undefined) ? root.barConfig.widgetTransparency : 1.0; const rawTransparency = (root.barConfig && root.barConfig.widgetTransparency !== undefined) ? root.barConfig.widgetTransparency : 1.0;
const isHovered = root.enableBackgroundHover && (mouseArea.containsMouse || (root.isHovered || false)); const isHovered = root.enableBackgroundHover && (mouseArea.containsMouse || (root.isHovered || false));
const transparency = isHovered ? Math.max(0.3, rawTransparency) : rawTransparency; const transparency = isHovered ? Math.max(0.3, rawTransparency) : rawTransparency;
const baseColor = isHovered ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor; const baseColor = isHovered ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.widgetBaseBackgroundColor;
if (Theme.widgetBackgroundHasAlpha) { if (Theme.widgetBackgroundHasAlpha) {
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * transparency); return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * transparency);
@@ -169,4 +170,26 @@ Item {
root.wheel(wheelEvent); root.wheel(wheelEvent);
} }
} }
property bool _blurRegistered: false
readonly property bool _shouldBlur: BlurService.enabled && blurBarWindow && blurBarWindow.registerBlurWidget && !(barConfig?.noBackground ?? false) && root.visible && root.width > 0
on_ShouldBlurChanged: _updateBlurRegistration()
function _updateBlurRegistration() {
if (_shouldBlur && !_blurRegistered) {
blurBarWindow.registerBlurWidget(visualContent);
_blurRegistered = true;
} else if (!_shouldBlur && _blurRegistered) {
if (blurBarWindow && blurBarWindow.unregisterBlurWidget)
blurBarWindow.unregisterBlurWidget(visualContent);
_blurRegistered = false;
}
}
Component.onCompleted: _updateBlurRegistration()
Component.onDestruction: {
if (_blurRegistered && blurBarWindow && blurBarWindow.unregisterBlurWidget)
blurBarWindow.unregisterBlurWidget(visualContent);
}
} }
@@ -14,6 +14,7 @@ Item {
property real barThickness: 48 property real barThickness: 48
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null
property string pluginId: "" property string pluginId: ""
property var pluginService: null property var pluginService: null
@@ -182,6 +183,7 @@ Item {
barThickness: root.barThickness barThickness: root.barThickness
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow
content: root.horizontalBarPill content: root.horizontalBarPill
states: State { states: State {
@@ -241,6 +243,7 @@ Item {
barThickness: root.barThickness barThickness: root.barThickness
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow
content: root.verticalBarPill content: root.verticalBarPill
isVerticalOrientation: true isVerticalOrientation: true
@@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import Quickshell import Quickshell
import qs.Common import qs.Common
import qs.Services
import qs.Widgets import qs.Widgets
Popup { Popup {
@@ -186,8 +187,8 @@ Popup {
contentItem: Rectangle { contentItem: Rectangle {
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1 border.width: BlurService.enabled ? BlurService.borderWidth : 1
Item { Item {
id: keyboardHandler id: keyboardHandler
@@ -46,6 +46,13 @@ 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,6 +91,16 @@ 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"]
+80 -1
View File
@@ -125,6 +125,15 @@ Item {
return Theme.warning; return Theme.warning;
} }
function openBlurBorderColorPicker() {
PopoutService.colorPickerModal.selectedColor = SettingsData.blurBorderCustomColor ?? "#ffffff";
PopoutService.colorPickerModal.pickerTitle = I18n.tr("Blur Border Color");
PopoutService.colorPickerModal.onColorSelectedCallback = function (color) {
SettingsData.set("blurBorderCustomColor", color.toString());
};
PopoutService.colorPickerModal.open();
}
function openM3ShadowColorPicker() { function openM3ShadowColorPicker() {
PopoutService.colorPickerModal.selectedColor = SettingsData.m3ElevationCustomColor ?? "#000000"; PopoutService.colorPickerModal.selectedColor = SettingsData.m3ElevationCustomColor ?? "#000000";
PopoutService.colorPickerModal.pickerTitle = I18n.tr("Shadow Color"); PopoutService.colorPickerModal.pickerTitle = I18n.tr("Shadow Color");
@@ -1816,6 +1825,77 @@ Item {
} }
} }
SettingsCard {
tab: "theme"
tags: ["blur", "background", "transparency", "glass", "frosted"]
title: I18n.tr("Background Blur")
settingKey: "blurEnabled"
iconName: "blur_on"
SettingsToggleRow {
tab: "theme"
tags: ["blur", "background", "transparency", "glass", "frosted"]
settingKey: "blurEnabled"
text: I18n.tr("Background Blur")
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")
checked: SettingsData.blurEnabled ?? false
enabled: BlurService.available
onToggled: checked => SettingsData.set("blurEnabled", checked)
}
SettingsDropdownRow {
tab: "theme"
tags: ["blur", "border", "outline", "edge"]
settingKey: "blurBorderColor"
text: I18n.tr("Blur Border Color")
description: I18n.tr("Border color around blurred surfaces")
visible: SettingsData.blurEnabled
options: [I18n.tr("Outline", "blur border color"), I18n.tr("Primary", "blur border color"), I18n.tr("Secondary", "blur border color"), I18n.tr("Text Color", "blur border color"), I18n.tr("Custom", "blur border color")]
currentValue: {
switch (SettingsData.blurBorderColor) {
case "primary":
return I18n.tr("Primary", "blur border color");
case "secondary":
return I18n.tr("Secondary", "blur border color");
case "surfaceText":
return I18n.tr("Text Color", "blur border color");
case "custom":
return I18n.tr("Custom", "blur border color");
default:
return I18n.tr("Outline", "blur border color");
}
}
onValueChanged: value => {
if (value === I18n.tr("Primary", "blur border color")) {
SettingsData.set("blurBorderColor", "primary");
} else if (value === I18n.tr("Secondary", "blur border color")) {
SettingsData.set("blurBorderColor", "secondary");
} else if (value === I18n.tr("Text Color", "blur border color")) {
SettingsData.set("blurBorderColor", "surfaceText");
} else if (value === I18n.tr("Custom", "blur border color")) {
SettingsData.set("blurBorderColor", "custom");
openBlurBorderColorPicker();
} else {
SettingsData.set("blurBorderColor", "outline");
}
}
}
SettingsSliderRow {
tab: "theme"
tags: ["blur", "border", "opacity"]
settingKey: "blurBorderOpacity"
text: I18n.tr("Blur Border Opacity")
visible: SettingsData.blurEnabled
value: Math.round((SettingsData.blurBorderOpacity ?? 1.0) * 100)
minimum: 0
maximum: 100
unit: "%"
defaultValue: 100
onSliderValueChanged: newValue => SettingsData.set("blurBorderOpacity", newValue / 100)
}
}
SettingsCard { SettingsCard {
tab: "theme" tab: "theme"
tags: ["niri", "layout", "gaps", "radius", "window", "border"] tags: ["niri", "layout", "gaps", "radius", "window", "border"]
@@ -2602,7 +2682,6 @@ Item {
onToggled: checked => SettingsData.set("matugenTemplateNeovim", checked) onToggled: checked => SettingsData.set("matugenTemplateNeovim", checked)
} }
SettingsDropdownRow { SettingsDropdownRow {
text: I18n.tr("Dark mode base") text: I18n.tr("Dark mode base")
tab: "theme" tab: "theme"
+3 -1
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"]; 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"];
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,6 +712,8 @@ 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);
}); });
+142 -27
View File
@@ -1,6 +1,5 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts
import qs.Common import qs.Common
import qs.Widgets import qs.Widgets
import qs.Services import qs.Services
@@ -40,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"]; 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"];
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]];
@@ -52,15 +51,14 @@ Column {
height: implicitHeight height: implicitHeight
spacing: Theme.spacingM spacing: Theme.spacingM
RowLayout { Row {
width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingM
DankIcon { DankIcon {
name: root.titleIcon name: root.titleIcon
size: Theme.iconSize size: Theme.iconSize
color: Theme.primary color: Theme.primary
Layout.alignment: Qt.AlignVCenter anchors.verticalCenter: parent.verticalCenter
} }
StyledText { StyledText {
@@ -68,7 +66,7 @@ Column {
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceText color: Theme.surfaceText
Layout.alignment: Qt.AlignVCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
@@ -439,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" visible: modelData.id === "clock" || modelData.id === "focusedWindow" || modelData.id === "keyboard_layout_name" || modelData.id === "appsDock" || modelData.id === "systemTray"
DankActionButton { DankActionButton {
id: compactModeButton id: compactModeButton
@@ -545,6 +543,39 @@ 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
@@ -933,6 +964,88 @@ 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
@@ -981,10 +1094,26 @@ Column {
Repeater { Repeater {
model: [ model: [
{ label: I18n.tr("Percentage"), mode: 0, icon: "percent" }, {
{ label: I18n.tr("Total"), mode: 1, icon: "storage" }, label: I18n.tr("Percentage"),
{ label: I18n.tr("Remaining"), mode: 2, icon: "hourglass_empty" }, mode: 0,
{ label: I18n.tr("Remaining / Total"), mode: 3, icon: "pie_chart" } icon: "percent"
},
{
label: I18n.tr("Total"),
mode: 1,
icon: "storage"
},
{
label: I18n.tr("Remaining"),
mode: 2,
icon: "hourglass_empty"
},
{
label: I18n.tr("Remaining / Total"),
mode: 3,
icon: "pie_chart"
}
] ]
delegate: Rectangle { delegate: Rectangle {
@@ -1316,20 +1445,7 @@ Column {
id: longestControlCenterLabelMetrics id: longestControlCenterLabelMetrics
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
text: { text: {
const labels = [ const labels = [I18n.tr("Network"), I18n.tr("VPN"), I18n.tr("Bluetooth"), I18n.tr("Audio"), I18n.tr("Volume"), I18n.tr("Microphone"), I18n.tr("Microphone Volume"), I18n.tr("Brightness"), I18n.tr("Brightness Value"), I18n.tr("Battery"), I18n.tr("Printer"), I18n.tr("Screen Sharing")];
I18n.tr("Network"),
I18n.tr("VPN"),
I18n.tr("Bluetooth"),
I18n.tr("Audio"),
I18n.tr("Volume"),
I18n.tr("Microphone"),
I18n.tr("Microphone Volume"),
I18n.tr("Brightness"),
I18n.tr("Brightness Value"),
I18n.tr("Battery"),
I18n.tr("Printer"),
I18n.tr("Screen Sharing")
];
let longest = ""; let longest = "";
for (let i = 0; i < labels.length; i++) { for (let i = 0; i < labels.length; i++) {
if (labels[i].length > longest.length) if (labels[i].length > longest.length)
@@ -1340,6 +1456,7 @@ Column {
} }
Repeater { Repeater {
id: groupRepeater
model: controlCenterContextMenu.controlCenterGroups model: controlCenterContextMenu.controlCenterGroups
delegate: Item { delegate: Item {
@@ -1569,8 +1686,6 @@ Column {
} }
} }
} }
id: groupRepeater
} }
} }
} }
+56 -2
View File
@@ -26,6 +26,7 @@ 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
@@ -67,6 +68,16 @@ 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));
@@ -395,7 +406,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; do for event_key in audio-volume-change power-plug power-unplug message message-new-instant desktop-login; do
found=0 found=0
case "$event_key" in case "$event_key" in
@@ -457,7 +468,8 @@ 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 = {
@@ -551,6 +563,10 @@ EOFCONFIG
criticalNotificationSound.destroy(); criticalNotificationSound.destroy();
criticalNotificationSound = null; criticalNotificationSound = null;
} }
if (loginSound) {
loginSound.destroy();
loginSound = null;
}
} }
function createSoundPlayers() { function createSoundPlayers() {
@@ -622,6 +638,19 @@ 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);
} }
@@ -661,6 +690,31 @@ 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();
+113
View File
@@ -0,0 +1,113 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland // ! Import is needed despite what qmlls says
import qs.Common
Singleton {
id: root
property bool quickshellSupported: false
property bool compositorSupported: false
property bool available: quickshellSupported && compositorSupported
readonly property bool enabled: available && (SettingsData.blurEnabled ?? false)
readonly property color borderColor: {
if (!enabled)
return "transparent";
const opacity = SettingsData.blurBorderOpacity ?? 0.5;
switch (SettingsData.blurBorderColor ?? "outline") {
case "primary":
return Theme.withAlpha(Theme.primary, opacity);
case "secondary":
return Theme.withAlpha(Theme.secondary, opacity);
case "surfaceText":
return Theme.withAlpha(Theme.surfaceText, opacity);
case "custom":
return Theme.withAlpha(SettingsData.blurBorderCustomColor ?? "#ffffff", opacity);
default:
return Theme.withAlpha(Theme.outline, opacity);
}
}
readonly property int borderWidth: enabled ? 1 : 0
function hoverColor(baseColor, hoverAlpha) {
if (!enabled)
return baseColor;
return Theme.withAlpha(baseColor, hoverAlpha ?? 0.15);
}
function createBlurRegion(targetWindow) {
if (!available)
return null;
try {
const region = Qt.createQmlObject(`
import Quickshell
Region {}
`, targetWindow, "BlurRegion");
targetWindow.BackgroundEffect.blurRegion = region;
return region;
} catch (e) {
console.warn("BlurService: Failed to create blur region:", e);
return null;
}
}
function reapplyBlurRegion(targetWindow, region) {
if (!region || !available)
return;
try {
targetWindow.BackgroundEffect.blurRegion = region;
region.changed();
} catch (e) {}
}
function destroyBlurRegion(targetWindow, region) {
if (!region)
return;
try {
targetWindow.BackgroundEffect.blurRegion = null;
} catch (e) {}
region.destroy();
}
Process {
id: blurProbe
running: false
command: ["dms", "blur", "check"]
stdout: StdioCollector {
onStreamFinished: {
root.compositorSupported = text.trim() === "supported";
if (root.compositorSupported)
console.info("BlurService: Compositor supports ext-background-effect-v1");
else
console.info("BlurService: Compositor does not support ext-background-effect-v1");
}
}
onExited: exitCode => {
if (exitCode !== 0)
console.warn("BlurService: blur probe failed with code:", exitCode);
}
}
Component.onCompleted: {
try {
const test = Qt.createQmlObject(`
import Quickshell
Region { radius: 0 }
`, root, "BlurAvailabilityTest");
test.destroy();
quickshellSupported = true;
console.info("BlurService: Quickshell blur support available");
blurProbe.running = true;
} catch (e) {
console.info("BlurService: BackgroundEffect not available - blur disabled. Requires a newer version of Quickshell.");
}
}
}
+6
View File
@@ -255,6 +255,12 @@ 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
+14 -1
View File
@@ -819,6 +819,7 @@ Singleton {
if (event.event === "unlock" || event.event === "resume") { if (event.event === "unlock" || event.event === "resume") {
suppressOsd = true; suppressOsd = true;
osdSuppressTimer.restart(); osdSuppressTimer.restart();
evaluateNightMode();
} }
} }
@@ -1050,7 +1051,19 @@ Singleton {
} }
function status(): string { function status(): string {
return root.nightModeEnabled ? "Night mode is enabled" : "Night mode is disabled"; const parts = ["Night mode is " + (root.nightModeEnabled ? "enabled" : "disabled")];
parts.push("Temperature: " + SessionData.nightModeTemperature + "K");
if (SessionData.nightModeAutoEnabled) {
parts.push("Automation: " + SessionData.nightModeAutoMode);
parts.push("Day temperature: " + SessionData.nightModeHighTemperature + "K");
}
if (DisplayService.gammaCurrentTemp > 0)
parts.push("Current: " + DisplayService.gammaCurrentTemp + "K");
return parts.join("\n");
}
function getTemperature(): string {
return SessionData.nightModeTemperature.toString();
} }
function temperature(value: string): string { function temperature(value: string): string {
+66 -1
View File
@@ -4,10 +4,75 @@ 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: availablePlayers.find(p => p.isPlaying) ?? availablePlayers.find(p => p.canControl && p.canPlay) ?? null property MprisPlayer activePlayer: 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();
}
} }
+14 -2
View File
@@ -6,6 +6,7 @@ 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
@@ -58,6 +59,10 @@ 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
} }
@@ -74,6 +79,12 @@ 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()
@@ -110,8 +121,9 @@ 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 combined = appName + " " + nodeName const mediaName = (node.properties && node.properties["media.name"] || "").toLowerCase()
return /xdg-desktop-portal|xdpw|screencast|screen|gnome shell|kwin|obs/.test(combined) const combined = appName + " " + nodeName + " " + mediaName
return /xdg-desktop-portal|xdpw|screencast|screen-cast|screen|gnome shell|kwin|obs|niri/.test(combined)
} }
function getMicrophoneStatus() { function getMicrophoneStatus() {
+4 -1
View File
@@ -231,7 +231,10 @@ Singleton {
return; return;
isChecking = true; isChecking = true;
hasError = false; hasError = false;
if (updChecker.length > 0) { if (pkgManager === "paru" || pkgManager === "yay") {
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);
+1 -1
View File
@@ -89,7 +89,7 @@ Row {
width: Math.max(contentItem.implicitWidth + root.buttonPadding * 2, root.minButtonWidth) + (selected ? 4 : 0) width: Math.max(contentItem.implicitWidth + root.buttonPadding * 2, root.minButtonWidth) + (selected ? 4 : 0)
height: root.buttonHeight height: root.buttonHeight
color: selected ? Theme.buttonBg : Theme.surfaceVariant color: selected ? Theme.buttonBg : Theme.withAlpha(Theme.surfaceVariant, Theme.popupTransparency)
border.color: "transparent" border.color: "transparent"
border.width: 0 border.width: 0
+14 -4
View File
@@ -107,6 +107,16 @@ PanelWindow {
} }
WlrLayershell.exclusiveZone: -1 WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
WindowBlur {
targetWindow: root
blurX: shadowBuffer
blurY: shadowBuffer
blurWidth: shouldBeVisible ? alignedWidth : 0
blurHeight: shouldBeVisible ? alignedHeight : 0
blurRadius: Theme.cornerRadius
}
color: "transparent" color: "transparent"
readonly property real dpr: CompositorService.getScreenScale(screen) readonly property real dpr: CompositorService.getScreenScale(screen)
@@ -256,15 +266,15 @@ PanelWindow {
scale: shouldBeVisible ? 1 : 0.9 scale: shouldBeVisible ? 1 : 0.9
property bool childHovered: false property bool childHovered: false
readonly property real popupSurfaceAlpha: SettingsData.popupTransparency readonly property real popupSurfaceAlpha: Theme.popupTransparency
Rectangle { Rectangle {
id: background id: background
anchors.fill: parent anchors.fill: parent
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, osdContainer.popupSurfaceAlpha) color: Theme.withAlpha(Theme.surfaceContainer, osdContainer.popupSurfaceAlpha)
border.color: Theme.outlineMedium border.color: BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium
border.width: 1 border.width: BlurService.enabled ? BlurService.borderWidth : 1
z: -1 z: -1
} }
@@ -276,7 +286,7 @@ PanelWindow {
level: Theme.elevationLevel3 level: Theme.elevationLevel3
fallbackOffset: 6 fallbackOffset: 6
targetRadius: Theme.cornerRadius targetRadius: Theme.cornerRadius
targetColor: Theme.surfaceContainer targetColor: Theme.withAlpha(Theme.surfaceContainer, osdContainer.popupSurfaceAlpha)
borderColor: Theme.outlineMedium borderColor: Theme.outlineMedium
borderWidth: 1 borderWidth: 1
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
+26 -8
View File
@@ -398,6 +398,17 @@ Item {
visible: false visible: false
color: "transparent" color: "transparent"
WindowBlur {
id: popoutBlur
targetWindow: contentWindow
readonly property real s: Math.min(1, contentContainer.scaleValue)
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)
blurWidth: (shouldBeVisible && contentWrapper.opacity > 0) ? contentContainer.width * s : 0
blurHeight: (shouldBeVisible && contentWrapper.opacity > 0) ? contentContainer.height * s : 0
blurRadius: Theme.cornerRadius
}
WlrLayershell.namespace: root.layerNamespace WlrLayershell.namespace: root.layerNamespace
WlrLayershell.layer: { WlrLayershell.layer: {
switch (Quickshell.env("DMS_POPOUT_LAYER")) { switch (Quickshell.env("DMS_POPOUT_LAYER")) {
@@ -565,14 +576,6 @@ Item {
} }
} }
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
border.color: Theme.outlineMedium
border.width: 0
}
Loader { Loader {
id: contentLoader id: contentLoader
anchors.fill: parent anchors.fill: parent
@@ -580,6 +583,21 @@ Item {
asynchronous: false asynchronous: false
} }
} }
Rectangle {
width: parent.width
height: parent.height
x: contentWrapper.x
y: contentWrapper.y
opacity: contentWrapper.opacity
scale: contentWrapper.scale
visible: contentWrapper.visible
radius: Theme.cornerRadius
color: "transparent"
border.color: BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium
border.width: BlurService.borderWidth
z: 100
}
} }
Item { Item {
+161 -81
View File
@@ -8,13 +8,122 @@ Item {
id: root id: root
property MprisPlayer activePlayer property MprisPlayer activePlayer
property real value: { property real seekPreviewRatio: -1
if (!activePlayer || activePlayer.length <= 0) return 0 readonly property real playerValue: {
const pos = (activePlayer.position || 0) % Math.max(1, activePlayer.length) if (!activePlayer || activePlayer.length <= 0)
const calculatedRatio = pos / activePlayer.length return 0;
return Math.max(0, Math.min(1, calculatedRatio)) const pos = (activePlayer.position || 0) % Math.max(1, activePlayer.length);
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
@@ -29,58 +138,35 @@ 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: waveSeekDebounceTimer id: waveHoldIndicatorTimer
interval: 150 interval: root.holdIndicatorDelay
repeat: false
onTriggered: { onTriggered: {
if (parent.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) { if (parent.pressed && root.isSeeking)
const clamped = Math.min(parent.pendingSeekPosition, activePlayer.length * 0.99) root.isDraggingSeek = true;
activePlayer.position = clamped
parent.pendingSeekPosition = -1
}
} }
} }
onPressed: (mouse) => { onPressed: mouse => root.handleSeekPressed(mouse, parent.width, waveMouseArea, waveHoldIndicatorTimer)
root.isSeeking = true onReleased: root.handleSeekReleased(waveMouseArea, waveHoldIndicatorTimer)
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) { onPositionChanged: mouse => root.handleSeekPositionChanged(mouse, parent.width, waveMouseArea)
const r = Math.max(0, Math.min(1, mouse.x / parent.width)) onCanceled: root.handleSeekCanceled(waveMouseArea, waveHoldIndicatorTimer)
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
}
}
} }
} }
} }
@@ -93,6 +179,7 @@ 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 {
@@ -110,7 +197,22 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color: parent.fillColor color: parent.fillColor
radius: height / 2 radius: height / 2
Behavior on width { NumberAnimation { duration: 80 } } Behavior on width {
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 {
@@ -122,59 +224,37 @@ 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 { NumberAnimation { duration: 80 } } Behavior on x {
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: flatSeekDebounceTimer id: flatHoldIndicatorTimer
interval: 150 interval: root.holdIndicatorDelay
repeat: false
onTriggered: { onTriggered: {
if (parent.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) { if (parent.pressed && root.isSeeking)
const clamped = Math.min(parent.pendingSeekPosition, activePlayer.length * 0.99) root.isDraggingSeek = true;
activePlayer.position = clamped
parent.pendingSeekPosition = -1
}
} }
} }
onPressed: (mouse) => { onPressed: mouse => root.handleSeekPressed(mouse, parent.width, flatMouseArea, flatHoldIndicatorTimer)
root.isSeeking = true onReleased: root.handleSeekReleased(flatMouseArea, flatHoldIndicatorTimer)
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) { onPositionChanged: mouse => root.handleSeekPositionChanged(mouse, parent.width, flatMouseArea)
const r = Math.max(0, Math.min(1, mouse.x / parent.width)) onCanceled: root.handleSeekCanceled(flatMouseArea, flatHoldIndicatorTimer)
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
}
}
} }
} }
} }
+75 -5
View File
@@ -6,6 +6,8 @@ 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
@@ -15,6 +17,7 @@ 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) {
@@ -22,7 +25,12 @@ 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 {
@@ -65,7 +73,9 @@ 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: Math.max(startX, Math.min(root.playX - startX - aaBias, width)) 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 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
@@ -100,6 +110,37 @@ 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)
@@ -107,7 +148,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: root.midY - height / 2 + root.currentAmp * Math.sin((waveClip.startX / root.wavelength) * 2 * Math.PI + root.phase) y: waveY(waveClip.startX) - height / 2
visible: waveClip.endX > waveClip.startX visible: waveClip.endX > waveClip.startX
z: 2 z: 2
} }
@@ -119,10 +160,34 @@ Item {
radius: width / 2 radius: width / 2
color: root.fillColor color: root.fillColor
x: waveClip.endX - width / 2 x: waveClip.endX - width / 2
y: root.midY - height / 2 + root.currentAmp * Math.sin((waveClip.endX / root.wavelength) * 2 * Math.PI + root.phase) y: waveY(waveClip.endX) - height / 2
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 {
@@ -141,6 +206,10 @@ 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 {
@@ -148,8 +217,9 @@ Item {
onTriggered: { onTriggered: {
if (root.isPlaying) if (root.isPlaying)
root.phase += 0.03 * frameTime * 60; root.phase += 0.03 * frameTime * 60;
startCap.y = root.midY - startCap.height / 2 + root.currentAmp * Math.sin((waveClip.startX / root.wavelength) * 2 * Math.PI + root.phase); startCap.y = waveY(waveClip.startX) - startCap.height / 2;
endCap.y = root.midY - endCap.height / 2 + root.currentAmp * Math.sin((waveClip.endX / root.wavelength) * 2 * Math.PI + root.phase); endCap.y = waveY(waveClip.endX) - endCap.height / 2;
actualEndCap.y = waveY(waveClip.gapEndX) - actualEndCap.height / 2;
} }
} }
+4 -2
View File
@@ -238,7 +238,7 @@ Rectangle {
width: fieldContent.width + Theme.spacingM * 2 width: fieldContent.width + Theme.spacingM * 2
height: 32 height: 32
radius: Theme.cornerRadius - 2 radius: Theme.cornerRadius - 2
color: Theme.surfaceContainerHigh color: Theme.surfaceLight
border.width: 1 border.width: 1
border.color: Theme.outlineLight border.color: Theme.outlineLight
@@ -272,7 +272,9 @@ Rectangle {
checked: configData ? (configData.autoconnect || false) : false checked: configData ? (configData.autoconnect || false) : false
visible: !VPNService.configLoading && configData !== null visible: !VPNService.configLoading && configData !== null
onToggled: checked => { onToggled: checked => {
VPNService.updateConfig(profile.uuid, {autoconnect: checked}); VPNService.updateConfig(profile.uuid, {
autoconnect: checked
});
} }
} }
+65
View File
@@ -0,0 +1,65 @@
import QtQuick
import qs.Services
Item {
id: root
visible: false
required property var targetWindow
property var blurItem: null
property real blurX: 0
property real blurY: 0
property real blurWidth: 0
property real blurHeight: 0
property real blurRadius: 0
property var _region: null
function _apply() {
if (!BlurService.enabled || !targetWindow) {
_cleanup();
return;
}
if (!_region)
_region = BlurService.createBlurRegion(targetWindow);
if (!_region)
return;
_region.item = Qt.binding(() => root.blurItem);
_region.x = Qt.binding(() => root.blurX);
_region.y = Qt.binding(() => root.blurY);
_region.width = Qt.binding(() => root.blurWidth);
_region.height = Qt.binding(() => root.blurHeight);
_region.radius = Qt.binding(() => root.blurRadius);
}
function _cleanup() {
if (!_region)
return;
BlurService.destroyBlurRegion(targetWindow, _region);
_region = null;
}
Connections {
target: BlurService
function onEnabledChanged() {
root._apply();
}
}
Connections {
target: root.targetWindow
function onVisibleChanged() {
if (root.targetWindow && root.targetWindow.visible) {
root._region = null;
root._apply();
}
}
}
Component.onCompleted: _apply()
Component.onDestruction: _cleanup()
}
File diff suppressed because it is too large Load Diff
@@ -86,13 +86,13 @@ def create_poeditor_json(translations):
references.append(ref) references.append(ref)
contexts = sorted(data['contexts']) if data['contexts'] else [] contexts = sorted(data['contexts']) if data['contexts'] else []
context_str = " | ".join(contexts) if contexts else term comment = " | ".join(contexts) if contexts else ""
entry = { entry = {
"term": term, "term": term,
"context": context_str, "context": term,
"reference": ", ".join(references), "reference": ", ".join(references),
"comment": "" "comment": comment
} }
poeditor_data.append(entry) poeditor_data.append(entry)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
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