mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-06 12:32:07 -04:00
Compare commits
5 Commits
3d75a51378
...
frame
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1217b25de5 | ||
|
|
e913630f90 | ||
|
|
220bb2708b | ||
|
|
e57ab3e1f3 | ||
|
|
952ab9b753 |
@@ -82,7 +82,7 @@ func (ds *DoctorStatus) OKCount() int {
|
||||
}
|
||||
|
||||
var (
|
||||
quickshellVersionRegex = regexp.MustCompile(`(?i)quickshell (\d+\.\d+\.\d+)`)
|
||||
quickshellVersionRegex = regexp.MustCompile(`quickshell (\d+\.\d+\.\d+)`)
|
||||
hyprlandVersionRegex = regexp.MustCompile(`v?(\d+\.\d+\.\d+)`)
|
||||
niriVersionRegex = regexp.MustCompile(`niri (\d+\.\d+)`)
|
||||
swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`)
|
||||
@@ -820,14 +820,10 @@ func checkOptionalDependencies() []checkResult {
|
||||
results = append(results, checkImageFormatPlugins()...)
|
||||
|
||||
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
|
||||
terminals = slices.DeleteFunc(terminals, func(t string) bool {
|
||||
return !utils.CommandExists(t)
|
||||
})
|
||||
|
||||
if len(terminals) > 0 {
|
||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, strings.Join(terminals, ", "), "", optionalFeaturesURL})
|
||||
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
|
||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], "", optionalFeaturesURL})
|
||||
} else {
|
||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, foot or alacritty", optionalFeaturesURL})
|
||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, or alacritty", optionalFeaturesURL})
|
||||
}
|
||||
|
||||
networkResult, err := network.DetectNetworkStack()
|
||||
|
||||
@@ -109,41 +109,16 @@ func updateArchLinux() error {
|
||||
}
|
||||
|
||||
var packageName string
|
||||
var isAUR bool
|
||||
if isArchPackageInstalled("dms-shell") {
|
||||
packageName = "dms-shell"
|
||||
if isArchPackageInstalled("dms-shell-bin") {
|
||||
packageName = "dms-shell-bin"
|
||||
} else if isArchPackageInstalled("dms-shell-git") {
|
||||
packageName = "dms-shell-git"
|
||||
isAUR = true
|
||||
} else if isArchPackageInstalled("dms-shell-bin") {
|
||||
packageName = "dms-shell-bin"
|
||||
isAUR = true
|
||||
} else {
|
||||
fmt.Println("Info: No dms-shell package found.")
|
||||
fmt.Println("Info: Neither dms-shell-bin nor dms-shell-git package found.")
|
||||
fmt.Println("Info: Falling back to git-based update method...")
|
||||
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 updateCmd *exec.Cmd
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ package main
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
@@ -31,9 +30,7 @@ func init() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
clipboard.MaybeServeAndExit()
|
||||
|
||||
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
|
||||
if os.Geteuid() == 0 {
|
||||
log.Fatal("This program should not be run as root. Exiting.")
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ package main
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
@@ -28,9 +27,7 @@ func init() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
clipboard.MaybeServeAndExit()
|
||||
|
||||
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
|
||||
if os.Geteuid() == 0 {
|
||||
log.Fatal("This program should not be run as root. Exiting.")
|
||||
}
|
||||
|
||||
|
||||
@@ -7,22 +7,6 @@ import (
|
||||
"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 {
|
||||
cmd := exec.Command("pacman", "-Q", packageName)
|
||||
err := cmd.Run()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package clipboard
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -12,142 +13,66 @@ import (
|
||||
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 {
|
||||
return copyForkCached(data, mimeType, false)
|
||||
return CopyReader(bytes.NewReader(data), mimeType, false, false)
|
||||
}
|
||||
|
||||
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
||||
if foreground {
|
||||
return serveClipboard(data, mimeType, pasteOnce)
|
||||
return copyServeWithWriter(func(writer io.Writer) error {
|
||||
total := 0
|
||||
for total < len(data) {
|
||||
n, err := writer.Write(data[total:])
|
||||
total += n
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if total != len(data) {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
return nil
|
||||
}, mimeType, pasteOnce)
|
||||
}
|
||||
return copyForkCached(data, mimeType, pasteOnce)
|
||||
return CopyReader(bytes.NewReader(data), mimeType, foreground, pasteOnce)
|
||||
}
|
||||
|
||||
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
|
||||
if foreground {
|
||||
buf, err := io.ReadAll(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read source: %w", err)
|
||||
}
|
||||
return serveClipboard(buf, mimeType, pasteOnce)
|
||||
if !foreground {
|
||||
return copyFork(data, mimeType, pasteOnce)
|
||||
}
|
||||
return copyFork(data, mimeType, pasteOnce)
|
||||
return copyServeReader(data, mimeType, pasteOnce)
|
||||
}
|
||||
|
||||
func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd {
|
||||
cmd := exec.Command(os.Args[0])
|
||||
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
||||
args := []string{os.Args[0], "cl", "copy", "--foreground"}
|
||||
if pasteOnce {
|
||||
args = append(args, "--paste-once")
|
||||
}
|
||||
args = append(args, "--type", mimeType)
|
||||
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Stderr = nil
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||
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
|
||||
}
|
||||
cmd.Env = append(os.Environ(), "DMS_CLIP_FORKED=1")
|
||||
|
||||
func waitReady(cmd *exec.Cmd) error {
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
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) {
|
||||
case *os.File:
|
||||
cmd.Stdin = src
|
||||
return waitReady(cmd)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("start: %w", err)
|
||||
}
|
||||
|
||||
default:
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
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 {
|
||||
return fmt.Errorf("start: %w", err)
|
||||
}
|
||||
@@ -158,22 +83,50 @@ func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
||||
if err := stdin.Close(); err != nil {
|
||||
return fmt.Errorf("close stdin: %w", err)
|
||||
}
|
||||
|
||||
var buf [1]byte
|
||||
if _, err := stdout.Read(buf[:]); err != nil {
|
||||
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var buf [1]byte
|
||||
if _, err := stdout.Read(buf[:]); err != nil {
|
||||
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func signalReady() {
|
||||
if os.Getenv(envServe) == "" {
|
||||
if os.Getenv("DMS_CLIP_FORKED") == "" {
|
||||
return
|
||||
}
|
||||
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) {
|
||||
preferredDirs := []string{}
|
||||
|
||||
@@ -194,7 +147,7 @@ func createClipboardCacheFile() (*os.File, error) {
|
||||
return os.CreateTemp("", "dms-clipboard-*")
|
||||
}
|
||||
|
||||
func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
||||
func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOnce bool) error {
|
||||
display, err := wlclient.Connect("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("wayland connect: %w", err)
|
||||
@@ -236,10 +189,12 @@ func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
||||
if bindErr != nil {
|
||||
return fmt.Errorf("registry bind: %w", bindErr)
|
||||
}
|
||||
|
||||
if dataControlMgr == nil {
|
||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||
}
|
||||
defer dataControlMgr.Destroy()
|
||||
|
||||
if seat == nil {
|
||||
return fmt.Errorf("no seat available")
|
||||
}
|
||||
@@ -278,12 +233,18 @@ func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
||||
|
||||
cancelled := make(chan struct{})
|
||||
pasted := make(chan struct{}, 1)
|
||||
sendErr := make(chan error, 1)
|
||||
|
||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||
_ = syscall.SetNonblock(e.Fd, false)
|
||||
defer syscall.Close(e.Fd)
|
||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||
defer file.Close()
|
||||
_, _ = file.Write(data)
|
||||
if err := writeTo(file); err != nil {
|
||||
select {
|
||||
case sendErr <- err:
|
||||
default:
|
||||
}
|
||||
}
|
||||
select {
|
||||
case pasted <- struct{}{}:
|
||||
default:
|
||||
@@ -305,6 +266,8 @@ func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
||||
select {
|
||||
case <-cancelled:
|
||||
return nil
|
||||
case err := <-sendErr:
|
||||
return err
|
||||
case <-pasted:
|
||||
if pasteOnce {
|
||||
return nil
|
||||
@@ -558,10 +521,12 @@ func copyMultiServe(offers []Offer, pasteOnce bool) error {
|
||||
if bindErr != nil {
|
||||
return fmt.Errorf("registry bind: %w", bindErr)
|
||||
}
|
||||
|
||||
if dataControlMgr == nil {
|
||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||
}
|
||||
defer dataControlMgr.Destroy()
|
||||
|
||||
if seat == nil {
|
||||
return fmt.Errorf("no seat available")
|
||||
}
|
||||
@@ -589,12 +554,12 @@ func copyMultiServe(offers []Offer, pasteOnce bool) error {
|
||||
pasted := make(chan struct{}, 1)
|
||||
|
||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||
_ = syscall.SetNonblock(e.Fd, false)
|
||||
defer syscall.Close(e.Fd)
|
||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||
defer file.Close()
|
||||
|
||||
if data, ok := offerMap[e.MimeType]; ok {
|
||||
_, _ = file.Write(data)
|
||||
file.Write(data)
|
||||
}
|
||||
|
||||
select {
|
||||
|
||||
@@ -39,10 +39,11 @@ type LayerSurface struct {
|
||||
wlSurface *client.Surface
|
||||
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
|
||||
viewport *wp_viewporter.WpViewport
|
||||
wlPools [2]*client.ShmPool
|
||||
wlBuffers [2]*client.Buffer
|
||||
slotBusy [2]bool
|
||||
needsRedraw bool
|
||||
wlPool *client.ShmPool
|
||||
wlBuffer *client.Buffer
|
||||
bufferBusy bool
|
||||
oldPool *client.ShmPool
|
||||
oldBuffer *client.Buffer
|
||||
scopyBuffer *client.Buffer
|
||||
configured bool
|
||||
hidden bool
|
||||
@@ -135,7 +136,6 @@ func (p *Picker) Run() (*Color, error) {
|
||||
break
|
||||
}
|
||||
|
||||
p.flushRedraws()
|
||||
p.checkDone()
|
||||
}
|
||||
|
||||
@@ -164,15 +164,6 @@ func (p *Picker) checkDone() {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Picker) flushRedraws() {
|
||||
for _, ls := range p.surfaces {
|
||||
if !ls.needsRedraw {
|
||||
continue
|
||||
}
|
||||
p.redrawSurface(ls)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Picker) connect() error {
|
||||
display, err := client.Connect("")
|
||||
if err != nil {
|
||||
@@ -516,45 +507,47 @@ func (p *Picker) captureForSurface(ls *LayerSurface) {
|
||||
}
|
||||
|
||||
func (p *Picker) redrawSurface(ls *LayerSurface) {
|
||||
slot := ls.state.FrontIndex()
|
||||
if ls.slotBusy[slot] {
|
||||
ls.needsRedraw = true
|
||||
return
|
||||
}
|
||||
|
||||
var renderBuf *ShmBuffer
|
||||
switch {
|
||||
case ls.hidden:
|
||||
if ls.hidden {
|
||||
renderBuf = ls.state.RedrawScreenOnly()
|
||||
default:
|
||||
} else {
|
||||
renderBuf = ls.state.Redraw()
|
||||
}
|
||||
if renderBuf == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ls.needsRedraw = false
|
||||
|
||||
if ls.wlPools[slot] == nil {
|
||||
pool, err := p.shm.CreatePool(renderBuf.Fd(), int32(renderBuf.Size()))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
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
|
||||
})
|
||||
if ls.oldBuffer != nil {
|
||||
ls.oldBuffer.Destroy()
|
||||
ls.oldBuffer = nil
|
||||
}
|
||||
if ls.oldPool != nil {
|
||||
ls.oldPool.Destroy()
|
||||
ls.oldPool = nil
|
||||
}
|
||||
|
||||
ls.slotBusy[slot] = true
|
||||
ls.oldPool = ls.wlPool
|
||||
ls.oldBuffer = ls.wlBuffer
|
||||
ls.wlPool = nil
|
||||
ls.wlBuffer = nil
|
||||
|
||||
pool, err := p.shm.CreatePool(renderBuf.Fd(), int32(renderBuf.Size()))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ls.wlPool = pool
|
||||
|
||||
wlBuffer, err := pool.CreateBuffer(0, int32(renderBuf.Width), int32(renderBuf.Height), int32(renderBuf.Stride), uint32(ls.state.ScreenFormat()))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ls.wlBuffer = wlBuffer
|
||||
|
||||
lsRef := ls
|
||||
wlBuffer.SetReleaseHandler(func(e client.BufferReleaseEvent) {
|
||||
lsRef.bufferBusy = false
|
||||
})
|
||||
ls.bufferBusy = true
|
||||
|
||||
logicalW, logicalH := ls.state.LogicalSize()
|
||||
if logicalW == 0 || logicalH == 0 {
|
||||
@@ -573,7 +566,7 @@ func (p *Picker) redrawSurface(ls *LayerSurface) {
|
||||
}
|
||||
_ = ls.wlSurface.SetBufferScale(bufferScale)
|
||||
}
|
||||
_ = ls.wlSurface.Attach(ls.wlBuffers[slot], 0, 0)
|
||||
_ = ls.wlSurface.Attach(wlBuffer, 0, 0)
|
||||
_ = ls.wlSurface.Damage(0, 0, int32(logicalW), int32(logicalH))
|
||||
_ = ls.wlSurface.Commit()
|
||||
|
||||
@@ -641,7 +634,7 @@ func (p *Picker) setupPointerHandlers() {
|
||||
}
|
||||
|
||||
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
|
||||
p.activeSurface.needsRedraw = true
|
||||
p.redrawSurface(p.activeSurface)
|
||||
})
|
||||
|
||||
p.pointer.SetLeaveHandler(func(e client.PointerLeaveEvent) {
|
||||
@@ -662,7 +655,7 @@ func (p *Picker) setupPointerHandlers() {
|
||||
return
|
||||
}
|
||||
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
|
||||
p.activeSurface.needsRedraw = true
|
||||
p.redrawSurface(p.activeSurface)
|
||||
})
|
||||
|
||||
p.pointer.SetButtonHandler(func(e client.PointerButtonEvent) {
|
||||
@@ -686,13 +679,17 @@ func (p *Picker) cleanup() {
|
||||
if ls.scopyBuffer != nil {
|
||||
ls.scopyBuffer.Destroy()
|
||||
}
|
||||
for i := range ls.wlBuffers {
|
||||
if ls.wlBuffers[i] != nil {
|
||||
ls.wlBuffers[i].Destroy()
|
||||
}
|
||||
if ls.wlPools[i] != nil {
|
||||
ls.wlPools[i].Destroy()
|
||||
}
|
||||
if ls.oldBuffer != nil {
|
||||
ls.oldBuffer.Destroy()
|
||||
}
|
||||
if ls.oldPool != nil {
|
||||
ls.oldPool.Destroy()
|
||||
}
|
||||
if ls.wlBuffer != nil {
|
||||
ls.wlBuffer.Destroy()
|
||||
}
|
||||
if ls.wlPool != nil {
|
||||
ls.wlPool.Destroy()
|
||||
}
|
||||
if ls.viewport != nil {
|
||||
ls.viewport.Destroy()
|
||||
|
||||
@@ -274,12 +274,6 @@ func (s *SurfaceState) FrontRenderBuffer() *ShmBuffer {
|
||||
return s.renderBufs[s.front]
|
||||
}
|
||||
|
||||
func (s *SurfaceState) FrontIndex() int {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.front
|
||||
}
|
||||
|
||||
func (s *SurfaceState) SwapBuffers() {
|
||||
s.mu.Lock()
|
||||
s.front ^= 1
|
||||
|
||||
@@ -137,7 +137,7 @@ bind = SUPER, bracketright, layoutmsg, preselect r
|
||||
|
||||
# === Sizing & Layout ===
|
||||
bind = SUPER, R, layoutmsg, togglesplit
|
||||
bind = SUPER CTRL, F, resizeactive, exact 100% 100%
|
||||
bind = SUPER CTRL, F, resizeactive, exact 100%
|
||||
|
||||
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
||||
bindmd = SUPER, mouse:272, Move window, movewindow
|
||||
|
||||
@@ -94,7 +94,6 @@ windowrule = tile on, match:class ^(gnome-control-center)$
|
||||
windowrule = tile on, match:class ^(pavucontrol)$
|
||||
windowrule = tile on, match:class ^(nm-connection-editor)$
|
||||
|
||||
windowrule = float on, match:class ^(org\.gnome\.Calculator)$
|
||||
windowrule = float on, match:class ^(gnome-calculator)$
|
||||
windowrule = float on, match:class ^(galculator)$
|
||||
windowrule = float on, match:class ^(blueman-manager)$
|
||||
|
||||
@@ -224,7 +224,6 @@ window-rule {
|
||||
open-floating false
|
||||
}
|
||||
window-rule {
|
||||
match app-id=r#"^org\.gnome\.Calculator$"#
|
||||
match app-id=r#"^gnome-calculator$"#
|
||||
match app-id=r#"^galculator$"#
|
||||
match app-id=r#"^blueman-manager$"#
|
||||
|
||||
@@ -242,7 +242,11 @@ func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMap
|
||||
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
|
||||
}
|
||||
|
||||
return PackageMapping{Name: "dms-shell", Repository: RepoTypeSystem}
|
||||
if a.packageInstalled("dms-shell-bin") {
|
||||
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
|
||||
}
|
||||
|
||||
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||
@@ -324,13 +328,6 @@ func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []d
|
||||
|
||||
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
|
||||
if len(systemPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
@@ -448,37 +445,6 @@ func (a *ArchDistribution) categorizePackages(dependencies []deps.Dependency, wm
|
||||
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 {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
@@ -487,9 +453,6 @@ func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages [
|
||||
a.log(fmt.Sprintf("Installing system packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
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...)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
@@ -577,7 +540,7 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
|
||||
var dmsShell []string
|
||||
|
||||
for _, pkg := range packages {
|
||||
if pkg == "dms-shell-git" {
|
||||
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
||||
dmsShell = append(dmsShell, pkg)
|
||||
} else {
|
||||
isDep := false
|
||||
@@ -658,7 +621,7 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
||||
}
|
||||
}
|
||||
|
||||
if pkg == "dms-shell-git" {
|
||||
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
||||
srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
|
||||
depsToRemove := []string{
|
||||
"depends = quickshell",
|
||||
@@ -681,7 +644,15 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
||||
}
|
||||
|
||||
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{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: startProgress + 0.3*(endProgress-startProgress),
|
||||
@@ -768,9 +739,42 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
||||
CommandInfo: "sudo pacman -U built-package",
|
||||
}
|
||||
|
||||
// Find .pkg.tar* files - for split packages, install the base and any installed compositor variants
|
||||
var files []string
|
||||
matches, _ := filepath.Glob(filepath.Join(packageDir, "*.pkg.tar*"))
|
||||
files = matches
|
||||
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
||||
// For DMS split packages, install base package
|
||||
pattern := filepath.Join(packageDir, fmt.Sprintf("%s-%s*.pkg.tar*", pkg, "*"))
|
||||
matches, err := filepath.Glob(pattern)
|
||||
if err == nil {
|
||||
for _, match := range matches {
|
||||
basename := filepath.Base(match)
|
||||
// Always include base package
|
||||
if !strings.Contains(basename, "hyprland") && !strings.Contains(basename, "niri") {
|
||||
files = append(files, match)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also update compositor-specific packages if they're installed
|
||||
if strings.HasSuffix(pkg, "-git") {
|
||||
if a.packageInstalled("dms-shell-hyprland-git") {
|
||||
hyprlandPattern := filepath.Join(packageDir, "dms-shell-hyprland-git-*.pkg.tar*")
|
||||
if hyprlandMatches, err := filepath.Glob(hyprlandPattern); err == nil && len(hyprlandMatches) > 0 {
|
||||
files = append(files, hyprlandMatches[0])
|
||||
}
|
||||
}
|
||||
if a.packageInstalled("dms-shell-niri-git") {
|
||||
niriPattern := filepath.Join(packageDir, "dms-shell-niri-git-*.pkg.tar*")
|
||||
if niriMatches, err := filepath.Glob(niriPattern); err == nil && len(niriMatches) > 0 {
|
||||
files = append(files, niriMatches[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For other packages, install all built packages
|
||||
matches, _ := filepath.Glob(filepath.Join(packageDir, "*.pkg.tar*"))
|
||||
files = matches
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return fmt.Errorf("no package files found after building %s", pkg)
|
||||
|
||||
@@ -31,7 +31,6 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
||||
"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/wlcontext"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput"
|
||||
@@ -73,7 +72,6 @@ var clipboardManager *clipboard.Manager
|
||||
var dbusManager *serverDbus.Manager
|
||||
var wlContext *wlcontext.SharedContext
|
||||
var themeModeManager *thememode.Manager
|
||||
var trayRecoveryManager *trayrecovery.Manager
|
||||
var locationManager *location.Manager
|
||||
var geoClientInstance geolocation.Client
|
||||
|
||||
@@ -396,18 +394,6 @@ func InitializeThemeModeManager() error {
|
||||
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 {
|
||||
manager, err := location.NewManager(geoClient)
|
||||
if err != nil {
|
||||
@@ -1339,9 +1325,6 @@ func cleanupManagers() {
|
||||
if themeModeManager != nil {
|
||||
themeModeManager.Close()
|
||||
}
|
||||
if trayRecoveryManager != nil {
|
||||
trayRecoveryManager.Close()
|
||||
}
|
||||
if wlContext != nil {
|
||||
wlContext.Close()
|
||||
}
|
||||
@@ -1627,18 +1610,6 @@ func Start(printDocs bool) error {
|
||||
}()
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-loginctlReady
|
||||
if loginctlManager == nil {
|
||||
return
|
||||
}
|
||||
if err := InitializeTrayRecoveryManager(); err != nil {
|
||||
log.Warnf("TrayRecovery manager unavailable: %v", err)
|
||||
} else {
|
||||
trayRecoveryManager.WatchLoginctl(loginctlManager)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
geoClient := geolocation.NewClient()
|
||||
geoClientInstance = geoClient
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
package trayrecovery
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
const resumeDelay = 3 * time.Second
|
||||
|
||||
type Manager struct {
|
||||
conn *dbus.Conn
|
||||
stopChan chan struct{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewManager() (*Manager, error) {
|
||||
conn, err := dbus.ConnectSessionBus()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to session bus: %w", err)
|
||||
}
|
||||
|
||||
m := &Manager{
|
||||
conn: conn,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
// Run a startup scan after a delay — covers the case where the process
|
||||
// was killed during suspend and restarted by systemd (Type=dbus).
|
||||
// The fresh process never sees the PrepareForSleep true→false transition,
|
||||
// so the loginctl watcher alone is not enough.
|
||||
go m.scheduleRecovery()
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// WatchLoginctl subscribes to loginctl session state changes and triggers
|
||||
// tray recovery after resume from suspend (PrepareForSleep false transition).
|
||||
// This handles the case where the process survives suspend.
|
||||
func (m *Manager) WatchLoginctl(lm *loginctl.Manager) {
|
||||
ch := lm.Subscribe("tray-recovery")
|
||||
m.wg.Add(1)
|
||||
go func() {
|
||||
defer m.wg.Done()
|
||||
defer lm.Unsubscribe("tray-recovery")
|
||||
|
||||
wasSleeping := false
|
||||
for {
|
||||
select {
|
||||
case <-m.stopChan:
|
||||
return
|
||||
case state, ok := <-ch:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if state.PreparingForSleep {
|
||||
wasSleeping = true
|
||||
continue
|
||||
}
|
||||
if wasSleeping {
|
||||
wasSleeping = false
|
||||
go m.scheduleRecovery()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (m *Manager) scheduleRecovery() {
|
||||
select {
|
||||
case <-time.After(resumeDelay):
|
||||
m.recoverTrayItems()
|
||||
case <-m.stopChan:
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Close() {
|
||||
select {
|
||||
case <-m.stopChan:
|
||||
return
|
||||
default:
|
||||
close(m.stopChan)
|
||||
}
|
||||
m.wg.Wait()
|
||||
if m.conn != nil {
|
||||
m.conn.Close()
|
||||
}
|
||||
log.Info("TrayRecovery manager closed")
|
||||
}
|
||||
@@ -1,262 +0,0 @@
|
||||
package trayrecovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
sniWatcherDest = "org.kde.StatusNotifierWatcher"
|
||||
sniWatcherPath = "/StatusNotifierWatcher"
|
||||
sniWatcherIface = "org.kde.StatusNotifierWatcher"
|
||||
sniItemIface = "org.kde.StatusNotifierItem"
|
||||
dbusIface = "org.freedesktop.DBus"
|
||||
propsIface = "org.freedesktop.DBus.Properties"
|
||||
probeTimeout = 300 * time.Millisecond
|
||||
connProbeTimeout = 150 * time.Millisecond
|
||||
batchSize = 30
|
||||
)
|
||||
|
||||
var excludedPrefixes = []string{
|
||||
"org.freedesktop.",
|
||||
"org.gnome.",
|
||||
"org.kde.StatusNotifier",
|
||||
"com.canonical.AppMenu",
|
||||
"org.mpris.",
|
||||
"org.pipewire.",
|
||||
"org.pulseaudio",
|
||||
"fi.epitaph",
|
||||
"quickshell",
|
||||
"org.kde.quickshell",
|
||||
}
|
||||
|
||||
func (m *Manager) recoverTrayItems() {
|
||||
registeredItems := m.getRegisteredItems()
|
||||
allNames := m.getDBusNames()
|
||||
if allNames == nil {
|
||||
return
|
||||
}
|
||||
|
||||
registeredConnIDs := m.buildRegisteredConnIDs(registeredItems)
|
||||
|
||||
count := len(registeredItems)
|
||||
log.Infof("TrayRecoveryService: scanning DBus for unregistered SNI items (%d already registered)...", count)
|
||||
|
||||
m.scanWellKnownNames(allNames, registeredItems, registeredConnIDs)
|
||||
m.scanConnectionIDs(allNames, registeredItems, registeredConnIDs)
|
||||
}
|
||||
|
||||
func (m *Manager) getRegisteredItems() []string {
|
||||
obj := m.conn.Object(sniWatcherDest, sniWatcherPath)
|
||||
variant, err := obj.GetProperty(sniWatcherIface + ".RegisteredStatusNotifierItems")
|
||||
if err != nil {
|
||||
log.Warnf("TrayRecoveryService: failed to get registered items: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
switch v := variant.Value().(type) {
|
||||
case []string:
|
||||
return v
|
||||
case []any:
|
||||
items := make([]string, 0, len(v))
|
||||
for _, elem := range v {
|
||||
if s, ok := elem.(string); ok {
|
||||
items = append(items, s)
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) getDBusNames() []string {
|
||||
var names []string
|
||||
err := m.conn.BusObject().Call(dbusIface+".ListNames", 0).Store(&names)
|
||||
if err != nil {
|
||||
log.Warnf("TrayRecoveryService: failed to list bus names: %v", err)
|
||||
return nil
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func (m *Manager) getNameOwner(name string) string {
|
||||
var owner string
|
||||
err := m.conn.BusObject().Call(dbusIface+".GetNameOwner", 0, name).Store(&owner)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return owner
|
||||
}
|
||||
|
||||
// buildRegisteredConnIDs resolves every registered SNI item (well-known name
|
||||
// or :1.xxx connection ID) to a canonical connection ID. This prevents
|
||||
// duplicates in both directions.
|
||||
func (m *Manager) buildRegisteredConnIDs(registeredItems []string) map[string]bool {
|
||||
connIDs := make(map[string]bool, len(registeredItems))
|
||||
for _, item := range registeredItems {
|
||||
name := extractName(item)
|
||||
if strings.HasPrefix(name, ":1.") {
|
||||
connIDs[name] = true
|
||||
} else {
|
||||
owner := m.getNameOwner(name)
|
||||
if owner != "" {
|
||||
connIDs[owner] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return connIDs
|
||||
}
|
||||
|
||||
// scanWellKnownNames probes well-known names (e.g. DinoX, nm-applet) for
|
||||
// unregistered SNI items and re-registers them.
|
||||
func (m *Manager) scanWellKnownNames(allNames []string, registeredItems []string, registeredConnIDs map[string]bool) {
|
||||
registeredRaw := strings.Join(registeredItems, "\n")
|
||||
|
||||
for _, name := range allNames {
|
||||
if strings.HasPrefix(name, ":") {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(registeredRaw, name) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip if this name's connection ID is already in the registered set
|
||||
// (handles the case where the app registered via connection ID instead)
|
||||
connForName := m.getNameOwner(name)
|
||||
if connForName != "" && registeredConnIDs[connForName] {
|
||||
continue
|
||||
}
|
||||
|
||||
if isExcludedName(name) {
|
||||
continue
|
||||
}
|
||||
|
||||
short := shortName(name)
|
||||
objectPaths := []string{
|
||||
"/StatusNotifierItem",
|
||||
"/org/ayatana/NotificationItem/" + short,
|
||||
}
|
||||
|
||||
for _, objPath := range objectPaths {
|
||||
if m.probeSNI(name, objPath, probeTimeout) {
|
||||
m.registerSNI(name)
|
||||
// Update set so the connection-ID section won't double-register this app
|
||||
if connForName != "" {
|
||||
registeredConnIDs[connForName] = true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// scanConnectionIDs probes all :1.xxx connections in parallel for unregistered
|
||||
// SNI items (e.g. Vesktop, Electron apps). Most non-SNI connections return an
|
||||
// error instantly, so this is fast.
|
||||
func (m *Manager) scanConnectionIDs(allNames []string, registeredItems []string, registeredConnIDs map[string]bool) {
|
||||
registeredRaw := strings.Join(registeredItems, "\n")
|
||||
registeredLower := strings.ToLower(registeredRaw)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
sem := make(chan struct{}, batchSize)
|
||||
|
||||
for _, name := range allNames {
|
||||
if !strings.HasPrefix(name, ":1.") {
|
||||
continue
|
||||
}
|
||||
if registeredConnIDs[name] {
|
||||
continue
|
||||
}
|
||||
|
||||
sem <- struct{}{}
|
||||
wg.Add(1)
|
||||
go func(conn string) {
|
||||
defer wg.Done()
|
||||
defer func() { <-sem }()
|
||||
|
||||
sniID := m.getSNIId(conn, connProbeTimeout)
|
||||
if sniID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip if an item with the same Id is already registered (case-insensitive)
|
||||
if strings.Contains(registeredLower, strings.ToLower(sniID)) {
|
||||
return
|
||||
}
|
||||
|
||||
m.registerSNI(conn)
|
||||
log.Infof("TrayRecovery: re-registered %s (Id: %s)", conn, sniID)
|
||||
}(name)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (m *Manager) probeSNI(dest, path string, timeout time.Duration) bool {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
obj := m.conn.Object(dest, dbus.ObjectPath(path))
|
||||
var props map[string]dbus.Variant
|
||||
err := obj.CallWithContext(ctx, propsIface+".GetAll", 0, sniItemIface).Store(&props)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
_, hasID := props["Id"]
|
||||
return hasID
|
||||
}
|
||||
|
||||
func (m *Manager) getSNIId(dest string, timeout time.Duration) string {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
obj := m.conn.Object(dest, "/StatusNotifierItem")
|
||||
var variant dbus.Variant
|
||||
err := obj.CallWithContext(ctx, propsIface+".Get", 0, sniItemIface, "Id").Store(&variant)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
id, _ := variant.Value().(string)
|
||||
return id
|
||||
}
|
||||
|
||||
func (m *Manager) registerSNI(name string) {
|
||||
obj := m.conn.Object(sniWatcherDest, sniWatcherPath)
|
||||
call := obj.Call(sniWatcherIface+".RegisterStatusNotifierItem", 0, name)
|
||||
if call.Err != nil {
|
||||
log.Warnf("TrayRecovery: failed to register %s: %v", name, call.Err)
|
||||
return
|
||||
}
|
||||
log.Infof("TrayRecovery: re-registered %s", name)
|
||||
}
|
||||
|
||||
func extractName(item string) string {
|
||||
if idx := strings.IndexByte(item, '/'); idx != -1 {
|
||||
return item[:idx]
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
func shortName(name string) string {
|
||||
parts := strings.Split(name, ".")
|
||||
if len(parts) > 0 {
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func isExcludedName(name string) bool {
|
||||
for _, prefix := range excludedPrefixes {
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -3,11 +3,8 @@ package wayland
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"slices"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -76,10 +73,7 @@ func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error
|
||||
m.post(func() {
|
||||
log.Info("Gamma control enabled at startup")
|
||||
gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
|
||||
m.availOutputsMu.RLock()
|
||||
outs := slices.Clone(m.availableOutputs)
|
||||
m.availOutputsMu.RUnlock()
|
||||
if err := m.setupOutputControls(outs, gammaMgr); err != nil {
|
||||
if err := m.setupOutputControls(m.availableOutputs, gammaMgr); err != nil {
|
||||
log.Errorf("Failed to initialize gamma controls: %v", err)
|
||||
return
|
||||
}
|
||||
@@ -176,7 +170,6 @@ func (m *Manager) setupRegistry() error {
|
||||
})
|
||||
if gammaMgr != nil {
|
||||
outputs = append(outputs, output)
|
||||
m.addAvailableOutput(output)
|
||||
}
|
||||
m.outputRegNames.Store(outputID, e.Name)
|
||||
|
||||
@@ -211,11 +204,6 @@ func (m *Manager) setupRegistry() error {
|
||||
}
|
||||
if foundOut.gammaControl != nil {
|
||||
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)
|
||||
|
||||
@@ -300,28 +288,14 @@ func (m *Manager) setupControlHandlers(state *outputState, control *wlr_gamma_co
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if ctrl, ok := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1); ok && ctrl != nil && !ctrl.IsZombie() {
|
||||
ctrl.Destroy()
|
||||
}
|
||||
out.gammaControl = nil
|
||||
out.failed = true
|
||||
out.rampSize = 0
|
||||
out.retryCount++
|
||||
out.lastFailTime = time.Now()
|
||||
|
||||
if !m.outputStillValid(out) {
|
||||
return
|
||||
}
|
||||
|
||||
backoff := time.Duration(300<<uint(min(out.retryCount-1, 4))) * time.Millisecond
|
||||
time.AfterFunc(backoff, func() {
|
||||
m.post(func() {
|
||||
if !m.outputStillValid(out) {
|
||||
return
|
||||
}
|
||||
if _, stillTracked := m.outputs.Load(outputID); !stillTracked {
|
||||
return
|
||||
}
|
||||
m.recreateOutputControl(out)
|
||||
})
|
||||
})
|
||||
@@ -329,75 +303,12 @@ func (m *Manager) setupControlHandlers(state *outputState, control *wlr_gamma_co
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Manager) addAvailableOutput(o *wlclient.Output) {
|
||||
if o == nil {
|
||||
return
|
||||
}
|
||||
m.availOutputsMu.Lock()
|
||||
defer m.availOutputsMu.Unlock()
|
||||
if slices.Contains(m.availableOutputs, o) {
|
||||
return
|
||||
}
|
||||
m.availableOutputs = append(m.availableOutputs, o)
|
||||
}
|
||||
|
||||
func (m *Manager) removeAvailableOutput(o *wlclient.Output) {
|
||||
if o == nil {
|
||||
return
|
||||
}
|
||||
m.availOutputsMu.Lock()
|
||||
defer m.availOutputsMu.Unlock()
|
||||
m.availableOutputs = slices.DeleteFunc(m.availableOutputs, func(existing *wlclient.Output) bool {
|
||||
return existing == o
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Manager) outputStillValid(out *outputState) bool {
|
||||
switch {
|
||||
case out == nil:
|
||||
return false
|
||||
case out.output == nil:
|
||||
return false
|
||||
case out.output.IsZombie():
|
||||
return false
|
||||
}
|
||||
m.availOutputsMu.RLock()
|
||||
defer m.availOutputsMu.RUnlock()
|
||||
return slices.Contains(m.availableOutputs, out.output)
|
||||
}
|
||||
|
||||
func isConnectionDeadErr(err error) bool {
|
||||
switch {
|
||||
case err == nil:
|
||||
return false
|
||||
case errors.Is(err, syscall.EPIPE):
|
||||
return true
|
||||
case errors.Is(err, syscall.ECONNRESET):
|
||||
return true
|
||||
case errors.Is(err, syscall.EBADF):
|
||||
return true
|
||||
case errors.Is(err, io.EOF):
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Manager) addOutputControl(output *wlclient.Output) error {
|
||||
switch {
|
||||
case m.connectionDead.Load():
|
||||
return nil
|
||||
case output == nil || output.IsZombie():
|
||||
return nil
|
||||
}
|
||||
|
||||
outputID := output.ID()
|
||||
gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
|
||||
|
||||
control, err := gammaMgr.GetGammaControl(output)
|
||||
if err != nil {
|
||||
if isConnectionDeadErr(err) {
|
||||
m.markConnectionDead(err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -418,37 +329,26 @@ func (m *Manager) recreateOutputControl(out *outputState) error {
|
||||
enabled := m.config.Enabled
|
||||
m.configMutex.RUnlock()
|
||||
|
||||
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):
|
||||
if !enabled || !m.controlsInitialized {
|
||||
return nil
|
||||
}
|
||||
if _, ok := m.outputs.Load(out.id); !ok {
|
||||
return nil
|
||||
}
|
||||
if out.isVirtual {
|
||||
return nil
|
||||
}
|
||||
if out.retryCount >= 10 {
|
||||
return nil
|
||||
}
|
||||
|
||||
gammaMgr, ok := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
|
||||
if !ok {
|
||||
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)
|
||||
if err != nil {
|
||||
if isConnectionDeadErr(err) {
|
||||
m.markConnectionDead(err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -458,13 +358,6 @@ func (m *Manager) recreateOutputControl(out *outputState) error {
|
||||
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) {
|
||||
m.configMutex.RLock()
|
||||
config := m.config
|
||||
@@ -797,12 +690,11 @@ func (m *Manager) applyGamma(temp int) {
|
||||
gamma := m.config.Gamma
|
||||
m.configMutex.RUnlock()
|
||||
|
||||
switch {
|
||||
case m.connectionDead.Load():
|
||||
if !m.controlsInitialized {
|
||||
return
|
||||
case !m.controlsInitialized:
|
||||
return
|
||||
case m.lastAppliedTemp == temp && m.lastAppliedGamma == gamma:
|
||||
}
|
||||
|
||||
if m.lastAppliedTemp == temp && m.lastAppliedGamma == gamma {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -822,14 +714,7 @@ func (m *Manager) applyGamma(temp int) {
|
||||
var jobs []job
|
||||
|
||||
for _, out := range outs {
|
||||
switch {
|
||||
case out.failed:
|
||||
continue
|
||||
case out.rampSize == 0:
|
||||
continue
|
||||
case out.gammaControl == nil:
|
||||
continue
|
||||
case !m.outputStillValid(out):
|
||||
if out.failed || out.rampSize == 0 {
|
||||
continue
|
||||
}
|
||||
ramp := GenerateGammaRamp(out.rampSize, temp, gamma)
|
||||
@@ -847,16 +732,18 @@ func (m *Manager) applyGamma(temp int) {
|
||||
}
|
||||
|
||||
for _, j := range jobs {
|
||||
err := m.setGammaBytes(j.out, j.data)
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
log.Warnf("gamma: failed to set output %d: %v", j.out.id, err)
|
||||
j.out.failed = true
|
||||
j.out.rampSize = 0
|
||||
if isConnectionDeadErr(err) {
|
||||
m.markConnectionDead(err)
|
||||
return
|
||||
if err := m.setGammaBytes(j.out, j.data); err != nil {
|
||||
log.Warnf("gamma: failed to set output %d: %v", j.out.id, err)
|
||||
j.out.failed = true
|
||||
j.out.rampSize = 0
|
||||
outID := j.out.id
|
||||
time.AfterFunc(300*time.Millisecond, func() {
|
||||
m.post(func() {
|
||||
if out, ok := m.outputs.Load(outID); ok && out.failed {
|
||||
m.recreateOutputControl(out)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -865,14 +752,6 @@ func (m *Manager) applyGamma(temp int) {
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -895,6 +774,7 @@ func (m *Manager) setGammaBytes(out *outputState, data []byte) error {
|
||||
}
|
||||
syscall.Seek(fd, 0, 0)
|
||||
|
||||
ctrl := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1)
|
||||
return ctrl.SetGamma(fd)
|
||||
}
|
||||
|
||||
@@ -1002,10 +882,10 @@ func (m *Manager) dbusMonitor() {
|
||||
}
|
||||
|
||||
func (m *Manager) handleDBusSignal(sig *dbus.Signal) {
|
||||
switch {
|
||||
case sig.Name != "org.freedesktop.login1.Manager.PrepareForSleep":
|
||||
if sig.Name != "org.freedesktop.login1.Manager.PrepareForSleep" {
|
||||
return
|
||||
case len(sig.Body) == 0:
|
||||
}
|
||||
if len(sig.Body) == 0 {
|
||||
return
|
||||
}
|
||||
preparing, ok := sig.Body[0].(bool)
|
||||
@@ -1019,34 +899,27 @@ func (m *Manager) handleDBusSignal(sig *dbus.Signal) {
|
||||
return
|
||||
}
|
||||
time.AfterFunc(500*time.Millisecond, func() {
|
||||
m.post(m.handleResume)
|
||||
m.post(func() {
|
||||
m.configMutex.RLock()
|
||||
stillEnabled := m.config.Enabled
|
||||
m.configMutex.RUnlock()
|
||||
if !stillEnabled || !m.controlsInitialized {
|
||||
return
|
||||
}
|
||||
m.outputs.Range(func(_ uint32, out *outputState) bool {
|
||||
if out.gammaControl != nil {
|
||||
out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1).Destroy()
|
||||
out.gammaControl = nil
|
||||
}
|
||||
out.retryCount = 0
|
||||
out.failed = false
|
||||
m.recreateOutputControl(out)
|
||||
return true
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Manager) handleResume() {
|
||||
m.configMutex.RLock()
|
||||
stillEnabled := m.config.Enabled
|
||||
m.configMutex.RUnlock()
|
||||
|
||||
switch {
|
||||
case !stillEnabled:
|
||||
return
|
||||
case !m.controlsInitialized:
|
||||
return
|
||||
case m.connectionDead.Load():
|
||||
return
|
||||
}
|
||||
|
||||
// Compositors (Niri, Hyprland, wlroots-based) re-apply the cached gamma
|
||||
// ramp to DRM on resume; gamma_control objects stay valid. We just need
|
||||
// to force a resend so the schedule catches up with the current time of
|
||||
// day — the original #1235 regression was caused by lastAppliedTemp
|
||||
// matching and the send being skipped.
|
||||
m.recalcSchedule(time.Now())
|
||||
m.lastAppliedTemp = 0
|
||||
m.applyCurrentTemp("resume")
|
||||
}
|
||||
|
||||
func (m *Manager) triggerUpdate() {
|
||||
select {
|
||||
case m.updateTrigger <- struct{}{}:
|
||||
@@ -1185,10 +1058,7 @@ func (m *Manager) SetEnabled(enabled bool) {
|
||||
case enabled && !m.controlsInitialized:
|
||||
m.post(func() {
|
||||
gammaMgr := m.gammaControl.(*wlr_gamma_control.ZwlrGammaControlManagerV1)
|
||||
m.availOutputsMu.RLock()
|
||||
outs := slices.Clone(m.availableOutputs)
|
||||
m.availOutputsMu.RUnlock()
|
||||
if err := m.setupOutputControls(outs, gammaMgr); err != nil {
|
||||
if err := m.setupOutputControls(m.availableOutputs, gammaMgr); err != nil {
|
||||
log.Errorf("gamma: failed to create controls: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package wayland
|
||||
import (
|
||||
"math"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||
@@ -72,11 +71,9 @@ type Manager struct {
|
||||
registry *wlclient.Registry
|
||||
gammaControl any
|
||||
availableOutputs []*wlclient.Output
|
||||
availOutputsMu sync.RWMutex
|
||||
outputRegNames syncmap.Map[uint32, uint32]
|
||||
outputs syncmap.Map[uint32, *outputState]
|
||||
controlsInitialized bool
|
||||
connectionDead atomic.Bool
|
||||
|
||||
cmdq chan cmd
|
||||
alive bool
|
||||
|
||||
@@ -139,7 +139,7 @@ func dmsPackageName(distroID string, dependencies []deps.Dependency) string {
|
||||
if isGit {
|
||||
return "dms-shell-git"
|
||||
}
|
||||
return "dms-shell"
|
||||
return "dms-shell-bin"
|
||||
case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE:
|
||||
if isGit {
|
||||
return "dms-git"
|
||||
|
||||
@@ -124,8 +124,6 @@ Singleton {
|
||||
|
||||
property string vpnLastConnected: ""
|
||||
|
||||
property string lastPlayerIdentity: ""
|
||||
|
||||
property var deviceMaxVolumes: ({})
|
||||
property var hiddenOutputDeviceNames: []
|
||||
property var hiddenInputDeviceNames: []
|
||||
|
||||
@@ -14,7 +14,7 @@ import "settings/SettingsStore.js" as Store
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property int settingsConfigVersion: 5
|
||||
readonly property int settingsConfigVersion: 11
|
||||
|
||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
||||
|
||||
@@ -186,6 +186,7 @@ Singleton {
|
||||
onPopoutElevationEnabledChanged: saveSettings()
|
||||
property bool barElevationEnabled: true
|
||||
onBarElevationEnabledChanged: saveSettings()
|
||||
|
||||
property bool blurEnabled: false
|
||||
onBlurEnabledChanged: saveSettings()
|
||||
property string blurBorderColor: "outline"
|
||||
@@ -198,6 +199,33 @@ Singleton {
|
||||
property bool blurredWallpaperLayer: false
|
||||
property bool blurWallpaperOnOverview: false
|
||||
|
||||
property bool frameEnabled: false
|
||||
onFrameEnabledChanged: saveSettings()
|
||||
property real frameThickness: 16
|
||||
onFrameThicknessChanged: saveSettings()
|
||||
property real frameRounding: 23
|
||||
onFrameRoundingChanged: saveSettings()
|
||||
property string frameColor: ""
|
||||
onFrameColorChanged: saveSettings()
|
||||
property real frameOpacity: 1.0
|
||||
onFrameOpacityChanged: saveSettings()
|
||||
property var frameScreenPreferences: ["all"]
|
||||
onFrameScreenPreferencesChanged: saveSettings()
|
||||
property real frameBarSize: 40
|
||||
onFrameBarSizeChanged: saveSettings()
|
||||
property bool frameShowOnOverview: false
|
||||
onFrameShowOnOverviewChanged: saveSettings()
|
||||
property bool frameBlurEnabled: true
|
||||
onFrameBlurEnabledChanged: saveSettings()
|
||||
|
||||
readonly property color effectiveFrameColor: {
|
||||
const fc = frameColor;
|
||||
if (!fc || fc === "default") return Theme.surfaceContainer;
|
||||
if (fc === "primary") return Theme.primary;
|
||||
if (fc === "surface") return Theme.surface;
|
||||
return fc;
|
||||
}
|
||||
|
||||
property bool showLauncherButton: true
|
||||
property bool showWorkspaceSwitcher: true
|
||||
property bool showFocusedWindow: true
|
||||
@@ -301,7 +329,6 @@ Singleton {
|
||||
property var workspaceNameIcons: ({})
|
||||
property bool waveProgressEnabled: true
|
||||
property bool scrollTitleEnabled: true
|
||||
property bool mediaAdaptiveWidthEnabled: true
|
||||
property bool audioVisualizerEnabled: true
|
||||
property string audioScrollMode: "volume"
|
||||
property int audioWheelScrollAmount: 5
|
||||
@@ -435,7 +462,6 @@ Singleton {
|
||||
property bool soundNewNotification: true
|
||||
property bool soundVolumeChanged: true
|
||||
property bool soundPluggedIn: true
|
||||
property bool soundLogin: false
|
||||
|
||||
property int acMonitorTimeout: 0
|
||||
property int acLockTimeout: 0
|
||||
@@ -1940,6 +1966,66 @@ Singleton {
|
||||
return filtered;
|
||||
}
|
||||
|
||||
function getFrameFilteredScreens() {
|
||||
var prefs = frameScreenPreferences || ["all"];
|
||||
if (!prefs || prefs.length === 0 || prefs.includes("all")) {
|
||||
return Quickshell.screens;
|
||||
}
|
||||
return Quickshell.screens.filter(screen => isScreenInPreferences(screen, prefs));
|
||||
}
|
||||
|
||||
function getActiveBarEdgeForScreen(screen) {
|
||||
if (!screen) return "";
|
||||
for (var i = 0; i < barConfigs.length; i++) {
|
||||
var bc = barConfigs[i];
|
||||
if (!bc.enabled) continue;
|
||||
var prefs = bc.screenPreferences || ["all"];
|
||||
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) continue;
|
||||
switch (bc.position ?? 0) {
|
||||
case SettingsData.Position.Top: return "top";
|
||||
case SettingsData.Position.Bottom: return "bottom";
|
||||
case SettingsData.Position.Left: return "left";
|
||||
case SettingsData.Position.Right: return "right";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function getActiveBarEdgesForScreen(screen) {
|
||||
if (!screen) return [];
|
||||
var edges = [];
|
||||
for (var i = 0; i < barConfigs.length; i++) {
|
||||
var bc = barConfigs[i];
|
||||
if (!bc.enabled) continue;
|
||||
var prefs = bc.screenPreferences || ["all"];
|
||||
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) continue;
|
||||
switch (bc.position ?? 0) {
|
||||
case SettingsData.Position.Top: edges.push("top"); break;
|
||||
case SettingsData.Position.Bottom: edges.push("bottom"); break;
|
||||
case SettingsData.Position.Left: edges.push("left"); break;
|
||||
case SettingsData.Position.Right: edges.push("right"); break;
|
||||
}
|
||||
}
|
||||
return edges;
|
||||
}
|
||||
|
||||
function getActiveBarThicknessForScreen(screen) {
|
||||
if (frameEnabled) return frameBarSize;
|
||||
if (!screen) return frameThickness;
|
||||
for (var i = 0; i < barConfigs.length; i++) {
|
||||
var bc = barConfigs[i];
|
||||
if (!bc.enabled) continue;
|
||||
var prefs = bc.screenPreferences || ["all"];
|
||||
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) continue;
|
||||
const innerPadding = bc.innerPadding ?? 4;
|
||||
const barT = Math.max(26 + innerPadding * 0.6, Theme.barHeight - 4 - (8 - innerPadding));
|
||||
const spacing = bc.spacing ?? 4;
|
||||
const bottomGap = bc.bottomGap ?? 0;
|
||||
return barT + spacing + bottomGap;
|
||||
}
|
||||
return frameThickness;
|
||||
}
|
||||
|
||||
function sendTestNotifications() {
|
||||
NotificationService.dismissAllPopups();
|
||||
sendTestNotification(0);
|
||||
|
||||
@@ -75,8 +75,6 @@ var SPEC = {
|
||||
|
||||
vpnLastConnected: { def: "" },
|
||||
|
||||
lastPlayerIdentity: { def: "" },
|
||||
|
||||
deviceMaxVolumes: { def: {} },
|
||||
hiddenOutputDeviceNames: { def: [] },
|
||||
hiddenInputDeviceNames: { def: [] },
|
||||
|
||||
@@ -140,7 +140,6 @@ var SPEC = {
|
||||
workspaceNameIcons: { def: {} },
|
||||
waveProgressEnabled: { def: true },
|
||||
scrollTitleEnabled: { def: true },
|
||||
mediaAdaptiveWidthEnabled: { def: true },
|
||||
audioVisualizerEnabled: { def: true },
|
||||
audioScrollMode: { def: "volume" },
|
||||
audioWheelScrollAmount: { def: 5 },
|
||||
@@ -243,7 +242,6 @@ var SPEC = {
|
||||
|
||||
soundsEnabled: { def: true },
|
||||
useSystemSoundTheme: { def: false },
|
||||
soundLogin: { def: false },
|
||||
soundNewNotification: { def: true },
|
||||
soundVolumeChanged: { def: true },
|
||||
soundPluggedIn: { def: true },
|
||||
@@ -549,7 +547,17 @@ var SPEC = {
|
||||
clipboardEnterToPaste: { def: false },
|
||||
|
||||
launcherPluginVisibility: { def: {} },
|
||||
launcherPluginOrder: { def: [] }
|
||||
launcherPluginOrder: { def: [] },
|
||||
|
||||
frameEnabled: { def: false },
|
||||
frameThickness: { def: 16 },
|
||||
frameRounding: { def: 23 },
|
||||
frameColor: { def: "" },
|
||||
frameOpacity: { def: 1.0 },
|
||||
frameScreenPreferences: { def: ["all"] },
|
||||
frameBarSize: { def: 40 },
|
||||
frameShowOnOverview: { def: false },
|
||||
frameBlurEnabled: { def: true }
|
||||
};
|
||||
|
||||
function getValidKeys() {
|
||||
|
||||
@@ -248,6 +248,10 @@ function migrateToVersion(obj, targetVersion) {
|
||||
settings.configVersion = 6;
|
||||
}
|
||||
|
||||
if (currentVersion < 11) {
|
||||
settings.configVersion = 11;
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import qs.Modules.OSD
|
||||
import qs.Modules.ProcessList
|
||||
import qs.Modules.DankBar
|
||||
import qs.Modules.DankBar.Popouts
|
||||
import qs.Modules.Frame
|
||||
import qs.Modules.WorkspaceOverlays
|
||||
import qs.Services
|
||||
|
||||
@@ -176,6 +177,8 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Frame {}
|
||||
|
||||
Repeater {
|
||||
id: dankBarRepeater
|
||||
model: ScriptModel {
|
||||
@@ -221,22 +224,10 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: loginSoundTimer
|
||||
// Half a second delay before playing login sound, otherwise the sound may be cut off
|
||||
// 50 is the minimum that seems to work, but 500 is safer
|
||||
interval: 500
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
AudioService.playLoginSoundIfApplicable();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
dockRecreateDebounce.start();
|
||||
// Force PolkitService singleton to initialize
|
||||
PolkitService.polkitAvailable;
|
||||
loginSoundTimer.start();
|
||||
}
|
||||
|
||||
Loader {
|
||||
|
||||
@@ -369,7 +369,9 @@ Item {
|
||||
}
|
||||
|
||||
function previous(): void {
|
||||
MprisController.previousOrRewind();
|
||||
if (MprisController.activePlayer && MprisController.activePlayer.canGoPrevious) {
|
||||
MprisController.activePlayer.previous();
|
||||
}
|
||||
}
|
||||
|
||||
function next(): void {
|
||||
|
||||
@@ -122,7 +122,7 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No recent clipboard entries found") : I18n.tr("Connecting to clipboard service…")
|
||||
text: I18n.tr("No recent clipboard entries found")
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
@@ -181,7 +181,7 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No saved clipboard entries") : I18n.tr("Connecting to clipboard service…")
|
||||
text: I18n.tr("No saved clipboard entries")
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
|
||||
@@ -60,12 +60,15 @@ DankModal {
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (!clipboardAvailable) {
|
||||
ToastService.showError(I18n.tr("Clipboard service not available"));
|
||||
return;
|
||||
}
|
||||
open();
|
||||
activeImageLoads = 0;
|
||||
shouldHaveFocus = true;
|
||||
ClipboardService.reset();
|
||||
if (clipboardAvailable)
|
||||
ClipboardService.refresh();
|
||||
ClipboardService.refresh();
|
||||
keyboardController.reset();
|
||||
|
||||
Qt.callLater(function () {
|
||||
|
||||
@@ -50,11 +50,14 @@ DankPopout {
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (!clipboardAvailable) {
|
||||
ToastService.showError(I18n.tr("Clipboard service not available"));
|
||||
return;
|
||||
}
|
||||
open();
|
||||
activeImageLoads = 0;
|
||||
ClipboardService.reset();
|
||||
if (clipboardAvailable)
|
||||
ClipboardService.refresh();
|
||||
ClipboardService.refresh();
|
||||
keyboardController.reset();
|
||||
|
||||
Qt.callLater(function () {
|
||||
@@ -119,10 +122,10 @@ DankPopout {
|
||||
onBackgroundClicked: hide()
|
||||
|
||||
onShouldBeVisibleChanged: {
|
||||
if (!shouldBeVisible)
|
||||
if (!shouldBeVisible) {
|
||||
return;
|
||||
if (clipboardAvailable)
|
||||
ClipboardService.refresh();
|
||||
}
|
||||
ClipboardService.refresh();
|
||||
keyboardController.reset();
|
||||
Qt.callLater(function () {
|
||||
if (contentLoader.item?.searchField) {
|
||||
|
||||
@@ -518,5 +518,20 @@ FocusScope {
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: frameLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 33
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: FrameTab {}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item)
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,12 @@ Rectangle {
|
||||
"text": I18n.tr("Widgets"),
|
||||
"icon": "widgets",
|
||||
"tabIndex": 22
|
||||
},
|
||||
{
|
||||
"id": "frame",
|
||||
"text": I18n.tr("Frame"),
|
||||
"icon": "frame_source",
|
||||
"tabIndex": 33
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -10,6 +10,8 @@ Item {
|
||||
required property var axis
|
||||
required property var barConfig
|
||||
|
||||
visible: !SettingsData.frameEnabled
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
anchors.left: parent.left
|
||||
@@ -37,6 +39,8 @@ Item {
|
||||
}
|
||||
|
||||
property real rt: {
|
||||
if (SettingsData.frameEnabled)
|
||||
return SettingsData.frameRounding;
|
||||
if (barConfig?.squareCorners ?? false)
|
||||
return 0;
|
||||
if (barWindow.hasMaximizedToplevel)
|
||||
@@ -255,11 +259,12 @@ Item {
|
||||
h = h - wing;
|
||||
const r = wing;
|
||||
const cr = rt;
|
||||
const crE = SettingsData.frameEnabled ? 0 : cr;
|
||||
|
||||
let d = `M ${cr} 0`;
|
||||
d += ` L ${w - cr} 0`;
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 1 ${w} ${cr}`;
|
||||
let d = `M ${crE} 0`;
|
||||
d += ` L ${w - crE} 0`;
|
||||
if (crE > 0)
|
||||
d += ` A ${crE} ${crE} 0 0 1 ${w} ${crE}`;
|
||||
if (r > 0) {
|
||||
d += ` L ${w} ${h + r}`;
|
||||
d += ` A ${r} ${r} 0 0 0 ${w - r} ${h}`;
|
||||
@@ -273,9 +278,9 @@ Item {
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 1 0 ${h - cr}`;
|
||||
}
|
||||
d += ` L 0 ${cr}`;
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 1 ${cr} 0`;
|
||||
d += ` L 0 ${crE}`;
|
||||
if (crE > 0)
|
||||
d += ` A ${crE} ${crE} 0 0 1 ${crE} 0`;
|
||||
d += " Z";
|
||||
return d;
|
||||
}
|
||||
@@ -285,11 +290,12 @@ Item {
|
||||
h = h - wing;
|
||||
const r = wing;
|
||||
const cr = rt;
|
||||
const crE = SettingsData.frameEnabled ? 0 : cr;
|
||||
|
||||
let d = `M ${cr} ${fullH}`;
|
||||
d += ` L ${w - cr} ${fullH}`;
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 0 ${w} ${fullH - cr}`;
|
||||
let d = `M ${crE} ${fullH}`;
|
||||
d += ` L ${w - crE} ${fullH}`;
|
||||
if (crE > 0)
|
||||
d += ` A ${crE} ${crE} 0 0 0 ${w} ${fullH - crE}`;
|
||||
if (r > 0) {
|
||||
d += ` L ${w} 0`;
|
||||
d += ` A ${r} ${r} 0 0 1 ${w - r} ${r}`;
|
||||
@@ -303,9 +309,9 @@ Item {
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 0 0 ${cr}`;
|
||||
}
|
||||
d += ` L 0 ${fullH - cr}`;
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 0 ${cr} ${fullH}`;
|
||||
d += ` L 0 ${fullH - crE}`;
|
||||
if (crE > 0)
|
||||
d += ` A ${crE} ${crE} 0 0 0 ${crE} ${fullH}`;
|
||||
d += " Z";
|
||||
return d;
|
||||
}
|
||||
@@ -314,11 +320,12 @@ Item {
|
||||
w = w - wing;
|
||||
const r = wing;
|
||||
const cr = rt;
|
||||
const crE = SettingsData.frameEnabled ? 0 : cr;
|
||||
|
||||
let d = `M 0 ${cr}`;
|
||||
d += ` L 0 ${h - cr}`;
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 0 ${cr} ${h}`;
|
||||
let d = `M 0 ${crE}`;
|
||||
d += ` L 0 ${h - crE}`;
|
||||
if (crE > 0)
|
||||
d += ` A ${crE} ${crE} 0 0 0 ${crE} ${h}`;
|
||||
if (r > 0) {
|
||||
d += ` L ${w + r} ${h}`;
|
||||
d += ` A ${r} ${r} 0 0 1 ${w} ${h - r}`;
|
||||
@@ -332,9 +339,9 @@ Item {
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 0 ${w - cr} 0`;
|
||||
}
|
||||
d += ` L ${cr} 0`;
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 0 0 ${cr}`;
|
||||
d += ` L ${crE} 0`;
|
||||
if (crE > 0)
|
||||
d += ` A ${crE} ${crE} 0 0 0 0 ${crE}`;
|
||||
d += " Z";
|
||||
return d;
|
||||
}
|
||||
@@ -344,11 +351,12 @@ Item {
|
||||
w = w - wing;
|
||||
const r = wing;
|
||||
const cr = rt;
|
||||
const crE = SettingsData.frameEnabled ? 0 : cr;
|
||||
|
||||
let d = `M ${fullW} ${cr}`;
|
||||
d += ` L ${fullW} ${h - cr}`;
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 1 ${fullW - cr} ${h}`;
|
||||
let d = `M ${fullW} ${crE}`;
|
||||
d += ` L ${fullW} ${h - crE}`;
|
||||
if (crE > 0)
|
||||
d += ` A ${crE} ${crE} 0 0 1 ${fullW - crE} ${h}`;
|
||||
if (r > 0) {
|
||||
d += ` L 0 ${h}`;
|
||||
d += ` A ${r} ${r} 0 0 0 ${r} ${h - r}`;
|
||||
@@ -362,9 +370,9 @@ Item {
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 1 ${cr} 0`;
|
||||
}
|
||||
d += ` L ${fullW - cr} 0`;
|
||||
if (cr > 0)
|
||||
d += ` A ${cr} ${cr} 0 0 1 ${fullW} ${cr}`;
|
||||
d += ` L ${fullW - crE} 0`;
|
||||
if (crE > 0)
|
||||
d += ` A ${crE} ${crE} 0 0 1 ${fullW} ${crE}`;
|
||||
d += " Z";
|
||||
return d;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,31 @@ Item {
|
||||
readonly property real innerPadding: barConfig?.innerPadding ?? 4
|
||||
readonly property real outlineThickness: (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0
|
||||
|
||||
readonly property real _frameLeftInset: {
|
||||
if (!SettingsData.frameEnabled || barWindow.isVertical) return 0
|
||||
return barWindow.hasAdjacentLeftBar
|
||||
? SettingsData.frameBarSize
|
||||
: 0
|
||||
}
|
||||
readonly property real _frameRightInset: {
|
||||
if (!SettingsData.frameEnabled || barWindow.isVertical) return 0
|
||||
return barWindow.hasAdjacentRightBar
|
||||
? SettingsData.frameBarSize
|
||||
: 0
|
||||
}
|
||||
readonly property real _frameTopInset: {
|
||||
if (!SettingsData.frameEnabled || !barWindow.isVertical) return 0
|
||||
return barWindow.hasAdjacentTopBar
|
||||
? SettingsData.frameThickness
|
||||
: 0
|
||||
}
|
||||
readonly property real _frameBottomInset: {
|
||||
if (!SettingsData.frameEnabled || !barWindow.isVertical) return 0
|
||||
return barWindow.hasAdjacentBottomBar
|
||||
? SettingsData.frameThickness
|
||||
: 0
|
||||
}
|
||||
|
||||
property alias hLeftSection: hLeftSection
|
||||
property alias hCenterSection: hCenterSection
|
||||
property alias hRightSection: hRightSection
|
||||
@@ -31,10 +56,14 @@ Item {
|
||||
property alias vRightSection: vRightSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Math.max(Theme.spacingXS, innerPadding * 0.8)
|
||||
anchors.rightMargin: Math.max(Theme.spacingXS, innerPadding * 0.8)
|
||||
anchors.topMargin: barWindow.isVertical ? (barWindow.hasAdjacentTopBar ? outlineThickness : Theme.spacingXS) : 0
|
||||
anchors.bottomMargin: barWindow.isVertical ? (barWindow.hasAdjacentBottomBar ? outlineThickness : Theme.spacingXS) : 0
|
||||
anchors.leftMargin: Math.max(Theme.spacingXS, innerPadding * 0.8) + _frameLeftInset
|
||||
anchors.rightMargin: Math.max(Theme.spacingXS, innerPadding * 0.8) + _frameRightInset
|
||||
anchors.topMargin: (barWindow.isVertical
|
||||
? (barWindow.hasAdjacentTopBar ? outlineThickness : Theme.spacingXS)
|
||||
: 0) + _frameTopInset
|
||||
anchors.bottomMargin: (barWindow.isVertical
|
||||
? (barWindow.hasAdjacentBottomBar ? outlineThickness : Theme.spacingXS)
|
||||
: 0) + _frameBottomInset
|
||||
clip: false
|
||||
|
||||
property int componentMapRevision: 0
|
||||
@@ -969,7 +998,6 @@ Item {
|
||||
axis: barWindow.axis
|
||||
barSpacing: barConfig?.spacing ?? 4
|
||||
barConfig: topBarContent.barConfig
|
||||
widgetData: parent.widgetData
|
||||
isAutoHideBar: topBarContent.barConfig?.autoHide ?? false
|
||||
isAtBottom: barWindow.axis?.edge === "bottom"
|
||||
visible: SettingsData.getFilteredScreens("systemTray").includes(barWindow.screen) && SystemTray.items.values.length > 0
|
||||
@@ -1438,21 +1466,12 @@ Item {
|
||||
parentScreen: barWindow.screen
|
||||
onClicked: {
|
||||
systemUpdateLoader.active = true;
|
||||
if (!systemUpdateLoader.item)
|
||||
return;
|
||||
const popout = systemUpdateLoader.item;
|
||||
const effectiveBarConfig = topBarContent.barConfig;
|
||||
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
|
||||
if (popout.setBarContext) {
|
||||
popout.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0);
|
||||
if (systemUpdateLoader.item && systemUpdateLoader.item.setBarContext) {
|
||||
systemUpdateLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0);
|
||||
}
|
||||
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");
|
||||
systemUpdateLoader.item?.toggle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +133,11 @@ PanelWindow {
|
||||
teardown();
|
||||
if (!BlurService.enabled || !BlurService.available)
|
||||
return;
|
||||
// In frame mode, FrameWindow owns the blur region for the entire screen edge
|
||||
// (including the bar area). The bar must not set its own competing blur region
|
||||
// so that frameBlurEnabled acts as the single control for all blur in frame mode.
|
||||
if (SettingsData.frameEnabled)
|
||||
return;
|
||||
|
||||
const widgets = barWindow._blurWidgetItems.filter(w => w && w.visible && w.width > 0 && w.height > 0);
|
||||
const hasBar = barHasTransparency;
|
||||
@@ -187,6 +192,11 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onFrameEnabledChanged() { barBlur.rebuild(); }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: topBarSlide
|
||||
function onXChanged() {
|
||||
@@ -238,7 +248,9 @@ PanelWindow {
|
||||
readonly property color _surfaceContainer: Theme.surfaceContainer
|
||||
readonly property string _barId: barConfig?.id ?? "default"
|
||||
property real _backgroundAlpha: barConfig?.transparency ?? 1.0
|
||||
readonly property color _bgColor: Theme.withAlpha(_surfaceContainer, _backgroundAlpha)
|
||||
readonly property color _bgColor: SettingsData.frameEnabled
|
||||
? Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity)
|
||||
: Theme.withAlpha(_surfaceContainer, _backgroundAlpha)
|
||||
|
||||
function _updateBackgroundAlpha() {
|
||||
const live = SettingsData.barConfigs.find(c => c.id === _barId);
|
||||
@@ -384,7 +396,7 @@ PanelWindow {
|
||||
shouldHideForWindows = filtered.length > 0;
|
||||
}
|
||||
|
||||
property real effectiveSpacing: hasMaximizedToplevel ? 0 : (barConfig?.spacing ?? 4)
|
||||
property real effectiveSpacing: SettingsData.frameEnabled ? 0 : (hasMaximizedToplevel ? 0 : (barConfig?.spacing ?? 4))
|
||||
|
||||
Behavior on effectiveSpacing {
|
||||
enabled: barWindow.visible
|
||||
@@ -395,7 +407,12 @@ PanelWindow {
|
||||
}
|
||||
|
||||
readonly property int notificationCount: NotificationService.notifications.length
|
||||
readonly property real effectiveBarThickness: Theme.snap(Math.max(barWindow.widgetThickness + (barConfig?.innerPadding ?? 4) + 4, Theme.barHeight - 4 - (8 - (barConfig?.innerPadding ?? 4))), _dpr)
|
||||
readonly property real effectiveBarThickness: SettingsData.frameEnabled
|
||||
? SettingsData.frameBarSize
|
||||
: Theme.snap(Math.max(barWindow.widgetThickness + (barConfig?.innerPadding ?? 4) + 4, Theme.barHeight - 4 - (8 - (barConfig?.innerPadding ?? 4))), _dpr)
|
||||
readonly property bool effectiveOpenOnOverview: SettingsData.frameEnabled
|
||||
? SettingsData.frameShowOnOverview
|
||||
: (barConfig?.openOnOverview ?? false)
|
||||
readonly property real widgetThickness: Theme.snap(Math.max(20, 26 + (barConfig?.innerPadding ?? 4) * 0.6), _dpr)
|
||||
|
||||
readonly property bool hasAdjacentTopBar: {
|
||||
@@ -651,7 +668,7 @@ PanelWindow {
|
||||
|
||||
readonly property int barThickness: Theme.px(barWindow.effectiveBarThickness + barWindow.effectiveSpacing, barWindow._dpr)
|
||||
|
||||
readonly property bool inOverviewWithShow: CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false)
|
||||
readonly property bool inOverviewWithShow: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview
|
||||
readonly property bool effectiveVisible: (barConfig?.visible ?? true) || inOverviewWithShow
|
||||
readonly property bool showing: effectiveVisible && (topBarCore.reveal || inOverviewWithShow || !topBarCore.autoHide)
|
||||
|
||||
@@ -792,7 +809,7 @@ PanelWindow {
|
||||
}
|
||||
|
||||
property bool reveal: {
|
||||
const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false);
|
||||
const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview;
|
||||
if (inOverviewWithShow)
|
||||
return true;
|
||||
|
||||
@@ -889,7 +906,7 @@ PanelWindow {
|
||||
top: barWindow.isVertical ? parent.top : undefined
|
||||
bottom: barWindow.isVertical ? parent.bottom : undefined
|
||||
}
|
||||
readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false)
|
||||
readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview
|
||||
hoverEnabled: (barConfig?.autoHide ?? false) && !inOverview && !topBarCore.hasActivePopout
|
||||
acceptedButtons: Qt.NoButton
|
||||
enabled: (barConfig?.autoHide ?? false) && !inOverview
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import QtQuick
|
||||
import Quickshell.Services.UPower
|
||||
import qs.Common
|
||||
import qs.Modules.Plugins
|
||||
import qs.Services
|
||||
@@ -11,8 +10,6 @@ BasePill {
|
||||
property bool batteryPopupVisible: false
|
||||
property var popoutTarget: null
|
||||
|
||||
property real touchpadAccumulator: 0
|
||||
|
||||
readonly property int barPosition: {
|
||||
switch (axis?.edge) {
|
||||
case "top":
|
||||
@@ -122,44 +119,5 @@ BasePill {
|
||||
battery.triggerRipple(this, mouse.x, mouse.y);
|
||||
toggleBatteryPopup();
|
||||
}
|
||||
onWheel: wheel => {
|
||||
var delta = wheel.angleDelta.y;
|
||||
if (delta === 0)
|
||||
return;
|
||||
|
||||
// Check if this is a touchpad
|
||||
if (delta !== 120 && delta !== -120) {
|
||||
touchpadAccumulator += delta;
|
||||
console.info("Acc: "+touchpadAccumulator);
|
||||
if (Math.abs(touchpadAccumulator) < 500)
|
||||
return;
|
||||
delta = touchpadAccumulator;
|
||||
touchpadAccumulator = 0;
|
||||
}
|
||||
console.info("Trigger! Delta: "+delta)
|
||||
|
||||
// This is after the other delta checks so it only shows on valid Y scroll
|
||||
if (typeof PowerProfiles === "undefined") {
|
||||
ToastService.showError("power-profiles-daemon not available");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get list of profiles, and current index
|
||||
const profiles = [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []);
|
||||
var index = profiles.findIndex(profile => PowerProfiles.profile === profile);
|
||||
|
||||
// Step once based on mouse wheel direction
|
||||
if (delta > 0) index += 1;
|
||||
else index -= 1;
|
||||
|
||||
// Already at end of list, can't go further
|
||||
if (index < 0 || index >= profiles.length) return;
|
||||
|
||||
// Set new profile
|
||||
PowerProfiles.profile = profiles[index];
|
||||
if (PowerProfiles.profile !== profiles[index]) {
|
||||
ToastService.showError("Failed to set power profile");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ BasePill {
|
||||
StyledTextMetrics {
|
||||
id: cpuBaseline
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
text: "100%"
|
||||
text: "88%"
|
||||
}
|
||||
|
||||
StyledTextMetrics {
|
||||
|
||||
@@ -17,7 +17,7 @@ BasePill {
|
||||
property int availableWidth: 400
|
||||
readonly property int maxNormalWidth: 456
|
||||
readonly property int maxCompactWidth: 288
|
||||
property Toplevel activeWindow: null
|
||||
readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
|
||||
property var activeDesktopEntry: null
|
||||
property bool isHovered: mouseArea.containsMouse
|
||||
property bool isAutoHideBar: false
|
||||
@@ -38,44 +38,10 @@ BasePill {
|
||||
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: {
|
||||
updateActiveWindow();
|
||||
updateDesktopEntry();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ToplevelManager
|
||||
function onActiveToplevelChanged() {
|
||||
root.updateActiveWindow();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: CompositorService
|
||||
function onToplevelsChanged() {
|
||||
root.updateActiveWindow();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DesktopEntries
|
||||
function onApplicationsChanged() {
|
||||
|
||||
@@ -19,8 +19,7 @@ BasePill {
|
||||
readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser
|
||||
property bool compactMode: false
|
||||
property var widgetData: null
|
||||
readonly property bool adaptiveWidthEnabled: SettingsData.mediaAdaptiveWidthEnabled
|
||||
readonly property int maxTextWidth: {
|
||||
readonly property int textWidth: {
|
||||
const size = widgetData?.mediaSize !== undefined ? widgetData.mediaSize : SettingsData.mediaSize;
|
||||
switch (size) {
|
||||
case 0:
|
||||
@@ -37,7 +36,10 @@ BasePill {
|
||||
if (isVerticalOrientation) {
|
||||
return widgetThickness - horizontalPadding * 2;
|
||||
}
|
||||
return 0;
|
||||
const controlsWidth = 20 + Theme.spacingXS + 24 + Theme.spacingXS + 20;
|
||||
const audioVizWidth = 20;
|
||||
const contentWidth = audioVizWidth + Theme.spacingXS + controlsWidth;
|
||||
return contentWidth + (textWidth > 0 ? textWidth + Theme.spacingXS : 0);
|
||||
}
|
||||
readonly property int currentContentHeight: {
|
||||
if (!isVerticalOrientation) {
|
||||
@@ -97,7 +99,7 @@ BasePill {
|
||||
|
||||
if (isMouseWheelY) {
|
||||
if (deltaY > 0) {
|
||||
MprisController.previousOrRewind();
|
||||
activePlayer.previous();
|
||||
} else {
|
||||
activePlayer.next();
|
||||
}
|
||||
@@ -105,7 +107,7 @@ BasePill {
|
||||
scrollAccumulatorY += deltaY;
|
||||
if (Math.abs(scrollAccumulatorY) >= touchpadThreshold) {
|
||||
if (scrollAccumulatorY > 0) {
|
||||
MprisController.previousOrRewind();
|
||||
activePlayer.previous();
|
||||
} else {
|
||||
activePlayer.next();
|
||||
}
|
||||
@@ -117,28 +119,7 @@ BasePill {
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
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
|
||||
implicitWidth: root.playerAvailable ? root.currentContentWidth : 0
|
||||
implicitHeight: root.playerAvailable ? root.currentContentHeight : 0
|
||||
opacity: root.playerAvailable ? 1 : 0
|
||||
|
||||
@@ -151,9 +132,8 @@ BasePill {
|
||||
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +214,7 @@ BasePill {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
activePlayer.togglePlaying();
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
MprisController.previousOrRewind();
|
||||
activePlayer.previous();
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
activePlayer.next();
|
||||
}
|
||||
@@ -289,7 +269,7 @@ BasePill {
|
||||
}
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: contentRoot.measuredTextWidth
|
||||
width: textWidth
|
||||
height: root.widgetThickness
|
||||
visible: {
|
||||
const size = widgetData?.mediaSize !== undefined ? widgetData.mediaSize : SettingsData.mediaSize;
|
||||
@@ -298,95 +278,50 @@ BasePill {
|
||||
clip: true
|
||||
color: "transparent"
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||
StyledText {
|
||||
id: mediaText
|
||||
property bool needsScrolling: implicitWidth > textContainer.width && SettingsData.scrollTitleEnabled
|
||||
property real scrollOffset: 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
|
||||
onTextChanged: {
|
||||
scrollOffset = 0;
|
||||
scrollAnimation.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: textClip
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
SequentialAnimation {
|
||||
id: scrollAnimation
|
||||
running: mediaText.needsScrolling && textContainer.visible
|
||||
loops: Animation.Infinite
|
||||
|
||||
StyledText {
|
||||
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();
|
||||
PauseAnimation {
|
||||
duration: 2000
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: scrollAnimation
|
||||
running: mediaText.needsScrolling && textContainer.visible
|
||||
loops: Animation.Infinite
|
||||
|
||||
PauseAnimation {
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: textChangeAnimation
|
||||
PauseAnimation {
|
||||
duration: 2000
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
NumberAnimation {
|
||||
target: mediaText
|
||||
property: "opacity"
|
||||
from: 0.7
|
||||
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
|
||||
}
|
||||
}
|
||||
NumberAnimation {
|
||||
target: mediaText
|
||||
property: "scrollOffset"
|
||||
to: 0
|
||||
duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60)
|
||||
easing.type: Easing.Linear
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -435,7 +370,11 @@ BasePill {
|
||||
anchors.fill: parent
|
||||
enabled: root.playerAvailable
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: MprisController.previousOrRewind()
|
||||
onClicked: {
|
||||
if (activePlayer) {
|
||||
activePlayer.previous();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -271,7 +271,7 @@ BasePill {
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (isFocused) {
|
||||
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.45);
|
||||
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.2);
|
||||
}
|
||||
return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
|
||||
}
|
||||
@@ -526,7 +526,7 @@ BasePill {
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (isFocused) {
|
||||
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.45);
|
||||
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.2);
|
||||
}
|
||||
return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
|
||||
}
|
||||
|
||||
@@ -16,11 +16,8 @@ BasePill {
|
||||
enableCursor: false
|
||||
|
||||
property var parentWindow: null
|
||||
property var widgetData: null
|
||||
property string section: "right"
|
||||
property bool isAtBottom: false
|
||||
property bool isAutoHideBar: false
|
||||
property bool useOverflowPopup: !widgetData?.trayUseInlineExpansion
|
||||
readonly property var hiddenTrayIds: {
|
||||
const envValue = Quickshell.env("DMS_HIDE_TRAYIDS") || "";
|
||||
return envValue ? envValue.split(",").map(id => id.trim().toLowerCase()) : [];
|
||||
@@ -43,76 +40,6 @@ BasePill {
|
||||
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
|
||||
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");
|
||||
@@ -151,13 +78,6 @@ BasePill {
|
||||
item: 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) {
|
||||
if (visibleFromIndex === visibleToIndex || visibleFromIndex < 0 || visibleToIndex < 0)
|
||||
@@ -183,7 +103,6 @@ BasePill {
|
||||
property int dropTargetIndex: -1
|
||||
property bool suppressShiftAnimation: false
|
||||
readonly property bool hasHiddenItems: allTrayItems.length > mainBarItems.length
|
||||
readonly property bool inlineExpanded: hasHiddenItems && !useOverflowPopup && menuOpen
|
||||
visible: allTrayItems.length > 0
|
||||
opacity: allTrayItems.length > 0 ? 1 : 0
|
||||
|
||||
@@ -279,11 +198,10 @@ BasePill {
|
||||
id: rowComp
|
||||
Row {
|
||||
spacing: 0
|
||||
layoutDirection: root.reverseInlineHorizontal ? Qt.RightToLeft : Qt.LeftToRight
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.displayedMainBarItems
|
||||
values: root.mainBarItems
|
||||
objectProp: "key"
|
||||
}
|
||||
|
||||
@@ -291,7 +209,29 @@ BasePill {
|
||||
id: delegateRoot
|
||||
property var trayItem: modelData.item
|
||||
property string itemKey: modelData.key
|
||||
property string iconSource: root.trayIconSourceFor(trayItem)
|
||||
property string iconSource: {
|
||||
let icon = trayItem && trayItem.icon;
|
||||
if (typeof icon === 'string' || icon instanceof String) {
|
||||
if (icon === "")
|
||||
return "";
|
||||
if (icon.includes("?path=")) {
|
||||
const split = icon.split("?path=");
|
||||
if (split.length !== 2)
|
||||
return icon;
|
||||
const name = split[0];
|
||||
const path = split[1];
|
||||
let fileName = name.substring(name.lastIndexOf("/") + 1);
|
||||
if (fileName.startsWith("dropboxstatus")) {
|
||||
fileName = `hicolor/16x16/status/${fileName}`;
|
||||
}
|
||||
return `file://${path}/${fileName}`;
|
||||
}
|
||||
if (icon.startsWith("/") && !icon.startsWith("file://"))
|
||||
return `file://${icon}`;
|
||||
return icon;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
width: root.trayItemSize
|
||||
height: root.barThickness
|
||||
@@ -431,8 +371,7 @@ BasePill {
|
||||
}
|
||||
if (!delegateRoot.trayItem.hasMenu)
|
||||
return;
|
||||
if (root.useOverflowPopup)
|
||||
root.menuOpen = false;
|
||||
root.menuOpen = false;
|
||||
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
|
||||
}
|
||||
|
||||
@@ -441,8 +380,8 @@ BasePill {
|
||||
const distance = Math.abs(mouse.x - dragHandler.dragStartPos.x);
|
||||
if (distance > 5) {
|
||||
dragHandler.dragging = true;
|
||||
root.draggedIndex = root.reverseInlineHorizontal ? (root.mainBarItems.length - 1 - index) : index;
|
||||
root.dropTargetIndex = root.draggedIndex;
|
||||
root.draggedIndex = index;
|
||||
root.dropTargetIndex = index;
|
||||
}
|
||||
}
|
||||
if (!dragHandler.dragging)
|
||||
@@ -452,8 +391,7 @@ BasePill {
|
||||
dragHandler.dragAxisOffset = axisOffset;
|
||||
const itemSize = root.trayItemSize;
|
||||
const slotOffset = Math.round(axisOffset / itemSize);
|
||||
const visualTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
|
||||
const newTargetIndex = root.reverseInlineHorizontal ? (root.mainBarItems.length - 1 - visualTargetIndex) : visualTargetIndex;
|
||||
const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
|
||||
if (newTargetIndex !== root.dropTargetIndex) {
|
||||
root.dropTargetIndex = newTargetIndex;
|
||||
}
|
||||
@@ -469,8 +407,7 @@ BasePill {
|
||||
root.callContextMenuFallback(delegateRoot.trayItem.id, Math.round(gp.x), Math.round(gp.y));
|
||||
return;
|
||||
}
|
||||
if (root.useOverflowPopup)
|
||||
root.menuOpen = false;
|
||||
root.menuOpen = false;
|
||||
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
|
||||
}
|
||||
}
|
||||
@@ -492,7 +429,7 @@ BasePill {
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: root.toggleIconName()
|
||||
name: root.menuOpen ? "expand_less" : "expand_more"
|
||||
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
color: Theme.widgetTextColor
|
||||
}
|
||||
@@ -514,301 +451,6 @@ BasePill {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.displayedInlineExpandedItems
|
||||
objectProp: "key"
|
||||
}
|
||||
|
||||
delegate: inlineExpandedTrayItemDelegate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: inlineExpandedTrayItemDelegate
|
||||
|
||||
Item {
|
||||
property var trayItem: modelData.item
|
||||
property string itemKey: modelData.key
|
||||
property string iconSource: root.trayIconSourceFor(trayItem)
|
||||
|
||||
width: root.isVerticalOrientation ? root.barThickness : (root.inlineExpanded ? root.trayItemSize : 0)
|
||||
height: root.isVerticalOrientation ? (root.inlineExpanded ? root.trayItemSize : 0) : root.barThickness
|
||||
visible: width > 0 || height > 0
|
||||
|
||||
Behavior on width {
|
||||
enabled: !root.isVerticalOrientation
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
enabled: root.isVerticalOrientation
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: inlineVisualContent
|
||||
width: root.trayItemSize
|
||||
height: root.trayItemSize
|
||||
x: root.isVerticalOrientation ? Math.round((parent.width - width) / 2) : (root.reverseInlineHorizontal ? parent.width - width : 0)
|
||||
y: root.isVerticalOrientation ? (root.reverseInlineVertical ? parent.height - height : 0) : Math.round((parent.height - height) / 2)
|
||||
radius: Theme.cornerRadius
|
||||
color: inlineTrayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||
opacity: root.inlineExpanded ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
IconImage {
|
||||
id: inlineIconImg
|
||||
anchors.centerIn: parent
|
||||
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
source: iconSource
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
mipmap: true
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: !inlineIconImg.visible
|
||||
text: {
|
||||
const itemId = trayItem?.id || "";
|
||||
if (!itemId)
|
||||
return "?";
|
||||
return itemId.charAt(0).toUpperCase();
|
||||
}
|
||||
font.pixelSize: 10
|
||||
color: Theme.widgetTextColor
|
||||
}
|
||||
|
||||
DankRipple {
|
||||
id: inlineItemRipple
|
||||
cornerRadius: Theme.cornerRadius
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: inlineTrayItemArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: root.inlineExpanded
|
||||
|
||||
onPressed: mouse => {
|
||||
const pos = mapToItem(inlineVisualContent, mouse.x, mouse.y);
|
||||
inlineItemRipple.trigger(pos.x, pos.y);
|
||||
}
|
||||
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
root.activateInlineTrayItem(trayItem, inlineVisualContent);
|
||||
return;
|
||||
}
|
||||
if (mouse.button !== Qt.RightButton)
|
||||
return;
|
||||
root.openInlineTrayContextMenu(trayItem, inlineTrayItemArea, mouse, inlineVisualContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: verticalMainTrayItemDelegate
|
||||
|
||||
Item {
|
||||
property var trayItem: modelData.item
|
||||
property string itemKey: modelData.key
|
||||
property string iconSource: root.trayIconSourceFor(trayItem)
|
||||
|
||||
width: root.barThickness
|
||||
height: root.trayItemSize
|
||||
z: dragHandler.dragging ? 100 : 0
|
||||
|
||||
property real shiftOffset: {
|
||||
if (root.draggedIndex < 0)
|
||||
return 0;
|
||||
if (index === root.draggedIndex)
|
||||
return 0;
|
||||
const dragIdx = root.draggedIndex;
|
||||
const dropIdx = root.dropTargetIndex;
|
||||
const shiftAmount = root.trayItemSize;
|
||||
if (dropIdx < 0)
|
||||
return 0;
|
||||
if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
|
||||
return -shiftAmount;
|
||||
if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx)
|
||||
return shiftAmount;
|
||||
return 0;
|
||||
}
|
||||
|
||||
transform: Translate {
|
||||
y: shiftOffset
|
||||
Behavior on y {
|
||||
enabled: !root.suppressShiftAnimation
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: dragHandler
|
||||
anchors.fill: parent
|
||||
property bool dragging: false
|
||||
property point dragStartPos: Qt.point(0, 0)
|
||||
property real dragAxisOffset: 0
|
||||
property bool longPressing: false
|
||||
|
||||
Timer {
|
||||
id: longPressTimer
|
||||
interval: 400
|
||||
repeat: false
|
||||
onTriggered: dragHandler.longPressing = true
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: visualContent
|
||||
width: root.trayItemSize
|
||||
height: root.trayItemSize
|
||||
anchors.centerIn: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: trayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||
border.width: dragHandler.dragging ? 2 : 0
|
||||
border.color: Theme.primary
|
||||
opacity: dragHandler.dragging ? 0.8 : 1.0
|
||||
|
||||
transform: Translate {
|
||||
y: dragHandler.dragging ? dragHandler.dragAxisOffset : 0
|
||||
}
|
||||
|
||||
IconImage {
|
||||
id: iconImg
|
||||
anchors.centerIn: parent
|
||||
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
source: iconSource
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
mipmap: true
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: !iconImg.visible
|
||||
text: {
|
||||
const itemId = trayItem?.id || "";
|
||||
if (!itemId)
|
||||
return "?";
|
||||
return itemId.charAt(0).toUpperCase();
|
||||
}
|
||||
font.pixelSize: 10
|
||||
color: Theme.widgetTextColor
|
||||
}
|
||||
|
||||
DankRipple {
|
||||
id: itemRipple
|
||||
cornerRadius: Theme.cornerRadius
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: trayItemArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
cursorShape: dragHandler.longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
|
||||
|
||||
onPressed: mouse => {
|
||||
const pos = mapToItem(visualContent, mouse.x, mouse.y);
|
||||
itemRipple.trigger(pos.x, pos.y);
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
dragHandler.dragStartPos = Qt.point(mouse.x, mouse.y);
|
||||
longPressTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
onReleased: mouse => {
|
||||
longPressTimer.stop();
|
||||
const wasDragging = dragHandler.dragging;
|
||||
const didReorder = wasDragging && root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex;
|
||||
|
||||
if (didReorder) {
|
||||
root.suppressShiftAnimation = true;
|
||||
root.moveTrayItemInFullOrder(root.draggedIndex, root.dropTargetIndex);
|
||||
Qt.callLater(() => root.suppressShiftAnimation = false);
|
||||
}
|
||||
|
||||
dragHandler.longPressing = false;
|
||||
dragHandler.dragging = false;
|
||||
dragHandler.dragAxisOffset = 0;
|
||||
root.draggedIndex = -1;
|
||||
root.dropTargetIndex = -1;
|
||||
|
||||
if (wasDragging || mouse.button !== Qt.LeftButton)
|
||||
return;
|
||||
|
||||
if (!trayItem)
|
||||
return;
|
||||
if (!trayItem.onlyMenu) {
|
||||
trayItem.activate();
|
||||
return;
|
||||
}
|
||||
if (!trayItem.hasMenu)
|
||||
return;
|
||||
if (root.useOverflowPopup)
|
||||
root.menuOpen = false;
|
||||
root.showForTrayItem(trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
|
||||
}
|
||||
|
||||
onPositionChanged: mouse => {
|
||||
if (dragHandler.longPressing && !dragHandler.dragging) {
|
||||
const distance = Math.abs(mouse.y - dragHandler.dragStartPos.y);
|
||||
if (distance > 5) {
|
||||
dragHandler.dragging = true;
|
||||
root.draggedIndex = index;
|
||||
root.dropTargetIndex = root.draggedIndex;
|
||||
}
|
||||
}
|
||||
if (!dragHandler.dragging)
|
||||
return;
|
||||
|
||||
const axisOffset = mouse.y - dragHandler.dragStartPos.y;
|
||||
dragHandler.dragAxisOffset = axisOffset;
|
||||
const itemSize = root.trayItemSize;
|
||||
const slotOffset = Math.round(axisOffset / itemSize);
|
||||
const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
|
||||
if (newTargetIndex !== root.dropTargetIndex) {
|
||||
root.dropTargetIndex = newTargetIndex;
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: mouse => {
|
||||
if (dragHandler.dragging)
|
||||
return;
|
||||
if (mouse.button !== Qt.RightButton)
|
||||
return;
|
||||
root.openInlineTrayContextMenu(trayItem, trayItemArea, mouse, visualContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -817,23 +459,219 @@ BasePill {
|
||||
Column {
|
||||
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 {
|
||||
model: ScriptModel {
|
||||
values: root.reverseInlineVertical ? [] : root.displayedMainBarItems
|
||||
values: root.mainBarItems
|
||||
objectProp: "key"
|
||||
}
|
||||
delegate: verticalMainTrayItemDelegate
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.reverseInlineVertical ? root.displayedInlineExpandedItems : []
|
||||
objectProp: "key"
|
||||
delegate: Item {
|
||||
id: delegateRoot
|
||||
property var trayItem: modelData.item
|
||||
property string itemKey: modelData.key
|
||||
property string iconSource: {
|
||||
let icon = trayItem && trayItem.icon;
|
||||
if (typeof icon === 'string' || icon instanceof String) {
|
||||
if (icon === "")
|
||||
return "";
|
||||
if (icon.includes("?path=")) {
|
||||
const split = icon.split("?path=");
|
||||
if (split.length !== 2)
|
||||
return icon;
|
||||
const name = split[0];
|
||||
const path = split[1];
|
||||
let fileName = name.substring(name.lastIndexOf("/") + 1);
|
||||
if (fileName.startsWith("dropboxstatus")) {
|
||||
fileName = `hicolor/16x16/status/${fileName}`;
|
||||
}
|
||||
return `file://${path}/${fileName}`;
|
||||
}
|
||||
if (icon.startsWith("/") && !icon.startsWith("file://"))
|
||||
return `file://${icon}`;
|
||||
return icon;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
width: root.barThickness
|
||||
height: root.trayItemSize
|
||||
z: dragHandler.dragging ? 100 : 0
|
||||
|
||||
property real shiftOffset: {
|
||||
if (root.draggedIndex < 0)
|
||||
return 0;
|
||||
if (index === root.draggedIndex)
|
||||
return 0;
|
||||
const dragIdx = root.draggedIndex;
|
||||
const dropIdx = root.dropTargetIndex;
|
||||
const shiftAmount = root.trayItemSize;
|
||||
if (dropIdx < 0)
|
||||
return 0;
|
||||
if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
|
||||
return -shiftAmount;
|
||||
if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx)
|
||||
return shiftAmount;
|
||||
return 0;
|
||||
}
|
||||
|
||||
transform: Translate {
|
||||
y: delegateRoot.shiftOffset
|
||||
Behavior on y {
|
||||
enabled: !root.suppressShiftAnimation
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: dragHandler
|
||||
anchors.fill: parent
|
||||
property bool dragging: false
|
||||
property point dragStartPos: Qt.point(0, 0)
|
||||
property real dragAxisOffset: 0
|
||||
property bool longPressing: false
|
||||
|
||||
Timer {
|
||||
id: longPressTimer
|
||||
interval: 400
|
||||
repeat: false
|
||||
onTriggered: dragHandler.longPressing = true
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: visualContent
|
||||
width: root.trayItemSize
|
||||
height: root.trayItemSize
|
||||
anchors.centerIn: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: trayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||
border.width: dragHandler.dragging ? 2 : 0
|
||||
border.color: Theme.primary
|
||||
opacity: dragHandler.dragging ? 0.8 : 1.0
|
||||
|
||||
transform: Translate {
|
||||
y: dragHandler.dragging ? dragHandler.dragAxisOffset : 0
|
||||
}
|
||||
|
||||
IconImage {
|
||||
id: iconImg
|
||||
anchors.centerIn: parent
|
||||
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
source: delegateRoot.iconSource
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
mipmap: true
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: !iconImg.visible
|
||||
text: {
|
||||
const itemId = trayItem?.id || "";
|
||||
if (!itemId)
|
||||
return "?";
|
||||
return itemId.charAt(0).toUpperCase();
|
||||
}
|
||||
font.pixelSize: 10
|
||||
color: Theme.widgetTextColor
|
||||
}
|
||||
|
||||
DankRipple {
|
||||
id: itemRipple
|
||||
cornerRadius: Theme.cornerRadius
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: trayItemArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
cursorShape: dragHandler.longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
|
||||
|
||||
onPressed: mouse => {
|
||||
const pos = mapToItem(visualContent, mouse.x, mouse.y);
|
||||
itemRipple.trigger(pos.x, pos.y);
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
dragHandler.dragStartPos = Qt.point(mouse.x, mouse.y);
|
||||
longPressTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
onReleased: mouse => {
|
||||
longPressTimer.stop();
|
||||
const wasDragging = dragHandler.dragging;
|
||||
const didReorder = wasDragging && root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex;
|
||||
|
||||
if (didReorder) {
|
||||
root.suppressShiftAnimation = true;
|
||||
root.moveTrayItemInFullOrder(root.draggedIndex, root.dropTargetIndex);
|
||||
Qt.callLater(() => root.suppressShiftAnimation = false);
|
||||
}
|
||||
|
||||
dragHandler.longPressing = false;
|
||||
dragHandler.dragging = false;
|
||||
dragHandler.dragAxisOffset = 0;
|
||||
root.draggedIndex = -1;
|
||||
root.dropTargetIndex = -1;
|
||||
|
||||
if (wasDragging || mouse.button !== Qt.LeftButton)
|
||||
return;
|
||||
|
||||
if (!delegateRoot.trayItem)
|
||||
return;
|
||||
if (!delegateRoot.trayItem.onlyMenu) {
|
||||
delegateRoot.trayItem.activate();
|
||||
return;
|
||||
}
|
||||
if (!delegateRoot.trayItem.hasMenu)
|
||||
return;
|
||||
root.menuOpen = false;
|
||||
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
|
||||
}
|
||||
|
||||
onPositionChanged: mouse => {
|
||||
if (dragHandler.longPressing && !dragHandler.dragging) {
|
||||
const distance = Math.abs(mouse.y - dragHandler.dragStartPos.y);
|
||||
if (distance > 5) {
|
||||
dragHandler.dragging = true;
|
||||
root.draggedIndex = index;
|
||||
root.dropTargetIndex = index;
|
||||
}
|
||||
}
|
||||
if (!dragHandler.dragging)
|
||||
return;
|
||||
|
||||
const axisOffset = mouse.y - dragHandler.dragStartPos.y;
|
||||
dragHandler.dragAxisOffset = axisOffset;
|
||||
const itemSize = root.trayItemSize;
|
||||
const slotOffset = Math.round(axisOffset / itemSize);
|
||||
const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
|
||||
if (newTargetIndex !== root.dropTargetIndex) {
|
||||
root.dropTargetIndex = newTargetIndex;
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: mouse => {
|
||||
if (dragHandler.dragging)
|
||||
return;
|
||||
if (mouse.button !== Qt.RightButton)
|
||||
return;
|
||||
if (!delegateRoot.trayItem?.hasMenu) {
|
||||
const gp = trayItemArea.mapToGlobal(mouse.x, mouse.y);
|
||||
root.callContextMenuFallback(delegateRoot.trayItem.id, Math.round(gp.x), Math.round(gp.y));
|
||||
return;
|
||||
}
|
||||
root.menuOpen = false;
|
||||
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
|
||||
}
|
||||
}
|
||||
}
|
||||
delegate: inlineExpandedTrayItemDelegate
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -851,7 +689,14 @@ BasePill {
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: root.toggleIconName()
|
||||
name: {
|
||||
const edge = root.axis?.edge;
|
||||
if (edge === "left") {
|
||||
return root.menuOpen ? "chevron_left" : "chevron_right";
|
||||
} else {
|
||||
return root.menuOpen ? "chevron_right" : "chevron_left";
|
||||
}
|
||||
}
|
||||
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
color: Theme.widgetTextColor
|
||||
}
|
||||
@@ -873,22 +718,6 @@ BasePill {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.reverseInlineVertical ? [] : root.displayedInlineExpandedItems
|
||||
objectProp: "key"
|
||||
}
|
||||
delegate: inlineExpandedTrayItemDelegate
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.reverseInlineVertical ? root.displayedMainBarItems : []
|
||||
objectProp: "key"
|
||||
}
|
||||
delegate: verticalMainTrayItemDelegate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -904,7 +733,7 @@ BasePill {
|
||||
blurRadius: Theme.cornerRadius
|
||||
}
|
||||
|
||||
visible: root.useOverflowPopup && root.menuOpen
|
||||
visible: root.menuOpen
|
||||
screen: root.parentScreen
|
||||
WlrLayershell.layer: WlrLayershell.Top
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
@@ -920,14 +749,13 @@ BasePill {
|
||||
|
||||
HyprlandFocusGrab {
|
||||
windows: [overflowMenu]
|
||||
active: CompositorService.useHyprlandFocusGrab && root.useOverflowPopup && root.menuOpen
|
||||
active: CompositorService.useHyprlandFocusGrab && root.menuOpen
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: PopoutManager
|
||||
function onPopoutOpening() {
|
||||
if (root.useOverflowPopup)
|
||||
root.menuOpen = false;
|
||||
root.menuOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1193,7 +1021,30 @@ BasePill {
|
||||
|
||||
delegate: Rectangle {
|
||||
property var trayItem: modelData
|
||||
property string iconSource: root.trayIconSourceFor(trayItem)
|
||||
property string iconSource: {
|
||||
let icon = trayItem?.icon;
|
||||
if (typeof icon === 'string' || icon instanceof String) {
|
||||
if (icon === "")
|
||||
return "";
|
||||
if (icon.includes("?path=")) {
|
||||
const split = icon.split("?path=");
|
||||
if (split.length !== 2)
|
||||
return icon;
|
||||
const name = split[0];
|
||||
const path = split[1];
|
||||
let fileName = name.substring(name.lastIndexOf("/") + 1);
|
||||
if (fileName.startsWith("dropboxstatus")) {
|
||||
fileName = `hicolor/16x16/status/${fileName}`;
|
||||
}
|
||||
return `file://${path}/${fileName}`;
|
||||
}
|
||||
if (icon.startsWith("/") && !icon.startsWith("file://")) {
|
||||
return `file://${icon}`;
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
width: root.trayItemSize + 4
|
||||
height: root.trayItemSize + 4
|
||||
@@ -1462,8 +1313,7 @@ BasePill {
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
updatePosition();
|
||||
if (root.useOverflowPopup)
|
||||
root.menuOpen = false;
|
||||
root.menuOpen = false;
|
||||
PopoutManager.closeAllPopouts();
|
||||
ModalManager.closeAllModalsExcept(null);
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ DankPopout {
|
||||
if (currentPlayer && currentPlayer !== player && currentPlayer.canPause) {
|
||||
currentPlayer.pause();
|
||||
}
|
||||
MprisController.setActivePlayer(player);
|
||||
MprisController.activePlayer = player;
|
||||
root.__hideDropdowns();
|
||||
}
|
||||
onDeviceSelected: device => {
|
||||
|
||||
@@ -487,7 +487,17 @@ Item {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: MprisController.previousOrRewind()
|
||||
onClicked: {
|
||||
if (!activePlayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activePlayer.position > 8 && activePlayer.canSeek) {
|
||||
activePlayer.position = 0;
|
||||
} else {
|
||||
activePlayer.previous();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +145,14 @@ Card {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: MprisController.previousOrRewind()
|
||||
onClicked: {
|
||||
if (!activePlayer) return
|
||||
if (activePlayer.position > 8 && activePlayer.canSeek) {
|
||||
activePlayer.position = 0
|
||||
} else {
|
||||
activePlayer.previous()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
17
quickshell/Modules/Frame/Frame.qml
Normal file
17
quickshell/Modules/Frame/Frame.qml
Normal file
@@ -0,0 +1,17 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
|
||||
Variants {
|
||||
id: root
|
||||
|
||||
model: Quickshell.screens
|
||||
|
||||
FrameInstance {
|
||||
required property var modelData
|
||||
|
||||
screen: modelData
|
||||
}
|
||||
}
|
||||
59
quickshell/Modules/Frame/FrameBorder.qml
Normal file
59
quickshell/Modules/Frame/FrameBorder.qml
Normal file
@@ -0,0 +1,59 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import qs.Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
required property real cutoutTopInset
|
||||
required property real cutoutBottomInset
|
||||
required property real cutoutLeftInset
|
||||
required property real cutoutRightInset
|
||||
required property real cutoutRadius
|
||||
|
||||
Rectangle {
|
||||
id: borderRect
|
||||
|
||||
anchors.fill: parent
|
||||
// Bake frameOpacity into the color alpha rather than using the `opacity` property.
|
||||
// Qt Quick can skip layer.effect processing on items with opacity < 1 as an
|
||||
// optimization, causing the MultiEffect inverted mask to stop working and the
|
||||
// Rectangle to render as a plain square at low opacity values.
|
||||
color: Qt.rgba(SettingsData.effectiveFrameColor.r,
|
||||
SettingsData.effectiveFrameColor.g,
|
||||
SettingsData.effectiveFrameColor.b,
|
||||
SettingsData.frameOpacity)
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
maskSource: cutoutMask
|
||||
maskEnabled: true
|
||||
maskInverted: true
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: cutoutMask
|
||||
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
visible: false
|
||||
|
||||
Rectangle {
|
||||
anchors {
|
||||
fill: parent
|
||||
topMargin: root.cutoutTopInset
|
||||
bottomMargin: root.cutoutBottomInset
|
||||
leftMargin: root.cutoutLeftInset
|
||||
rightMargin: root.cutoutRightInset
|
||||
}
|
||||
radius: root.cutoutRadius
|
||||
}
|
||||
}
|
||||
}
|
||||
87
quickshell/Modules/Frame/FrameExclusions.qml
Normal file
87
quickshell/Modules/Frame/FrameExclusions.qml
Normal file
@@ -0,0 +1,87 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
required property var screen
|
||||
|
||||
readonly property var barEdges: {
|
||||
SettingsData.barConfigs; // force re-eval when bar configs change
|
||||
return SettingsData.getActiveBarEdgesForScreen(screen);
|
||||
}
|
||||
|
||||
// One thin invisible PanelWindow per edge.
|
||||
// Skips any edge where a bar already provides its own exclusiveZone.
|
||||
|
||||
readonly property bool screenEnabled: SettingsData.frameEnabled && SettingsData.isScreenInPreferences(root.screen, SettingsData.frameScreenPreferences)
|
||||
|
||||
Loader {
|
||||
active: root.screenEnabled && !root.barEdges.includes("top")
|
||||
sourceComponent: EdgeExclusion {
|
||||
targetScreen: root.screen
|
||||
anchorTop: true
|
||||
anchorLeft: true
|
||||
anchorRight: true
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: root.screenEnabled && !root.barEdges.includes("bottom")
|
||||
sourceComponent: EdgeExclusion {
|
||||
targetScreen: root.screen
|
||||
anchorBottom: true
|
||||
anchorLeft: true
|
||||
anchorRight: true
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: root.screenEnabled && !root.barEdges.includes("left")
|
||||
sourceComponent: EdgeExclusion {
|
||||
targetScreen: root.screen
|
||||
anchorLeft: true
|
||||
anchorTop: true
|
||||
anchorBottom: true
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: root.screenEnabled && !root.barEdges.includes("right")
|
||||
sourceComponent: EdgeExclusion {
|
||||
targetScreen: root.screen
|
||||
anchorRight: true
|
||||
anchorTop: true
|
||||
anchorBottom: true
|
||||
}
|
||||
}
|
||||
|
||||
component EdgeExclusion: PanelWindow {
|
||||
required property var targetScreen
|
||||
|
||||
screen: targetScreen
|
||||
property bool anchorTop: false
|
||||
property bool anchorBottom: false
|
||||
property bool anchorLeft: false
|
||||
property bool anchorRight: false
|
||||
|
||||
WlrLayershell.namespace: "dms:frame-exclusion"
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
exclusiveZone: SettingsData.frameThickness
|
||||
color: "transparent"
|
||||
mask: Region {}
|
||||
implicitWidth: 1
|
||||
implicitHeight: 1
|
||||
|
||||
anchors {
|
||||
top: anchorTop
|
||||
bottom: anchorBottom
|
||||
left: anchorLeft
|
||||
right: anchorRight
|
||||
}
|
||||
}
|
||||
}
|
||||
18
quickshell/Modules/Frame/FrameInstance.qml
Normal file
18
quickshell/Modules/Frame/FrameInstance.qml
Normal file
@@ -0,0 +1,18 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var screen
|
||||
|
||||
FrameWindow {
|
||||
targetScreen: root.screen
|
||||
}
|
||||
|
||||
FrameExclusions {
|
||||
screen: root.screen
|
||||
}
|
||||
}
|
||||
169
quickshell/Modules/Frame/FrameWindow.qml
Normal file
169
quickshell/Modules/Frame/FrameWindow.qml
Normal file
@@ -0,0 +1,169 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
PanelWindow {
|
||||
id: win
|
||||
|
||||
required property var targetScreen
|
||||
|
||||
screen: targetScreen
|
||||
visible: true
|
||||
|
||||
WlrLayershell.namespace: "dms:frame"
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
bottom: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
color: "transparent"
|
||||
|
||||
// No input — pass everything through to apps and bar
|
||||
mask: Region {}
|
||||
|
||||
readonly property var barEdges: {
|
||||
SettingsData.barConfigs;
|
||||
return SettingsData.getActiveBarEdgesForScreen(win.screen);
|
||||
}
|
||||
|
||||
readonly property real _dpr: CompositorService.getScreenScale(win.screen)
|
||||
readonly property bool _frameActive: SettingsData.frameEnabled
|
||||
&& SettingsData.isScreenInPreferences(win.screen, SettingsData.frameScreenPreferences)
|
||||
readonly property int _windowRegionWidth: win._regionInt(win.width)
|
||||
readonly property int _windowRegionHeight: win._regionInt(win.height)
|
||||
|
||||
function _regionInt(value) {
|
||||
return Math.max(0, Math.round(Theme.px(value, win._dpr)));
|
||||
}
|
||||
|
||||
readonly property int cutoutTopInset: win._regionInt(barEdges.includes("top") ? SettingsData.frameBarSize : SettingsData.frameThickness)
|
||||
readonly property int cutoutBottomInset: win._regionInt(barEdges.includes("bottom") ? SettingsData.frameBarSize : SettingsData.frameThickness)
|
||||
readonly property int cutoutLeftInset: win._regionInt(barEdges.includes("left") ? SettingsData.frameBarSize : SettingsData.frameThickness)
|
||||
readonly property int cutoutRightInset: win._regionInt(barEdges.includes("right") ? SettingsData.frameBarSize : SettingsData.frameThickness)
|
||||
readonly property int cutoutWidth: Math.max(0, win._windowRegionWidth - win.cutoutLeftInset - win.cutoutRightInset)
|
||||
readonly property int cutoutHeight: Math.max(0, win._windowRegionHeight - win.cutoutTopInset - win.cutoutBottomInset)
|
||||
readonly property int cutoutRadius: {
|
||||
const requested = win._regionInt(SettingsData.frameRounding);
|
||||
const maxRadius = Math.floor(Math.min(win.cutoutWidth, win.cutoutHeight) / 2);
|
||||
return Math.max(0, Math.min(requested, maxRadius));
|
||||
}
|
||||
|
||||
// Slightly expand the subtractive blur cutout at very low opacity levels
|
||||
readonly property int _blurCutoutCompensation: SettingsData.frameOpacity <= 0.2 ? 1 : 0
|
||||
readonly property int _blurCutoutLeft: Math.max(0, win.cutoutLeftInset - win._blurCutoutCompensation)
|
||||
readonly property int _blurCutoutTop: Math.max(0, win.cutoutTopInset - win._blurCutoutCompensation)
|
||||
readonly property int _blurCutoutRight: Math.min(win._windowRegionWidth, win._windowRegionWidth - win.cutoutRightInset + win._blurCutoutCompensation)
|
||||
readonly property int _blurCutoutBottom: Math.min(win._windowRegionHeight, win._windowRegionHeight - win.cutoutBottomInset + win._blurCutoutCompensation)
|
||||
readonly property int _blurCutoutRadius: {
|
||||
const requested = win.cutoutRadius + win._blurCutoutCompensation;
|
||||
const maxRadius = Math.floor(Math.min(_blurCutout.width, _blurCutout.height) / 2);
|
||||
return Math.max(0, Math.min(requested, maxRadius));
|
||||
}
|
||||
|
||||
// Must stay visible so Region.item can resolve scene coordinates.
|
||||
Item {
|
||||
id: _blurCutout
|
||||
x: win._blurCutoutLeft
|
||||
y: win._blurCutoutTop
|
||||
width: Math.max(0, win._blurCutoutRight - win._blurCutoutLeft)
|
||||
height: Math.max(0, win._blurCutoutBottom - win._blurCutoutTop)
|
||||
}
|
||||
|
||||
property var _frameBlurRegion: null
|
||||
|
||||
function _buildBlur() {
|
||||
_teardownBlur();
|
||||
// Follow the global blur toggle
|
||||
if (!BlurService.enabled || !SettingsData.frameBlurEnabled || !win._frameActive || !win.visible)
|
||||
return;
|
||||
try {
|
||||
const region = Qt.createQmlObject(
|
||||
'import QtQuick; import Quickshell; Region {' +
|
||||
' property Item cutoutItem;' +
|
||||
' property int cutoutRadius: 0;' +
|
||||
' Region {' +
|
||||
' item: cutoutItem;' +
|
||||
' intersection: Intersection.Subtract;' +
|
||||
' radius: cutoutRadius;' +
|
||||
' }' +
|
||||
'}',
|
||||
win, "FrameBlurRegion");
|
||||
|
||||
region.x = Qt.binding(() => 0);
|
||||
region.y = Qt.binding(() => 0);
|
||||
region.width = Qt.binding(() => win._windowRegionWidth);
|
||||
region.height = Qt.binding(() => win._windowRegionHeight);
|
||||
region.cutoutItem = _blurCutout;
|
||||
region.cutoutRadius = Qt.binding(() => win._blurCutoutRadius);
|
||||
|
||||
win.BackgroundEffect.blurRegion = region;
|
||||
win._frameBlurRegion = region;
|
||||
} catch (e) {
|
||||
console.warn("FrameWindow: Failed to create blur region:", e);
|
||||
}
|
||||
}
|
||||
|
||||
function _teardownBlur() {
|
||||
if (!win._frameBlurRegion)
|
||||
return;
|
||||
try {
|
||||
win.BackgroundEffect.blurRegion = null;
|
||||
} catch (e) {}
|
||||
win._frameBlurRegion.destroy();
|
||||
win._frameBlurRegion = null;
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: _blurRebuildTimer
|
||||
interval: 1
|
||||
onTriggered: win._buildBlur()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onFrameBlurEnabledChanged() { _blurRebuildTimer.restart(); }
|
||||
function onFrameEnabledChanged() { _blurRebuildTimer.restart(); }
|
||||
function onFrameThicknessChanged() { _blurRebuildTimer.restart(); }
|
||||
function onFrameBarSizeChanged() { _blurRebuildTimer.restart(); }
|
||||
function onFrameOpacityChanged() { _blurRebuildTimer.restart(); }
|
||||
function onFrameRoundingChanged() { _blurRebuildTimer.restart(); }
|
||||
function onFrameScreenPreferencesChanged() { _blurRebuildTimer.restart(); }
|
||||
function onBarConfigsChanged() { _blurRebuildTimer.restart(); }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: BlurService
|
||||
function onEnabledChanged() { _blurRebuildTimer.restart(); }
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
win._frameBlurRegion = null;
|
||||
_blurRebuildTimer.restart();
|
||||
} else {
|
||||
_teardownBlur();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: Qt.callLater(() => win._buildBlur())
|
||||
Component.onDestruction: win._teardownBlur()
|
||||
|
||||
FrameBorder {
|
||||
anchors.fill: parent
|
||||
visible: win._frameActive
|
||||
cutoutTopInset: win.cutoutTopInset
|
||||
cutoutBottomInset: win.cutoutBottomInset
|
||||
cutoutLeftInset: win.cutoutLeftInset
|
||||
cutoutRightInset: win.cutoutRightInset
|
||||
cutoutRadius: win.cutoutRadius
|
||||
}
|
||||
}
|
||||
@@ -1338,7 +1338,7 @@ Item {
|
||||
enabled: MprisController.activePlayer?.canGoPrevious ?? false
|
||||
hoverEnabled: enabled
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: MprisController.previousOrRewind()
|
||||
onClicked: MprisController.activePlayer?.previous()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -693,6 +693,8 @@ Item {
|
||||
|
||||
SettingsToggleRow {
|
||||
visible: CompositorService.isNiri
|
||||
enabled: !SettingsData.frameEnabled
|
||||
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
|
||||
text: I18n.tr("Show on Overview")
|
||||
checked: selectedBarConfig?.openOnOverview ?? false
|
||||
onToggled: toggled => {
|
||||
@@ -798,11 +800,42 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: SettingsData.frameEnabled
|
||||
width: parent.width
|
||||
implicitHeight: frameNote.implicitHeight + Theme.spacingS * 2
|
||||
|
||||
Row {
|
||||
id: frameNote
|
||||
x: Theme.spacingM
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "frame_source"
|
||||
size: Theme.fontSizeMedium
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Spacing and size are managed by Frame mode")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width - Theme.fontSizeMedium - Theme.spacingS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
iconName: "space_bar"
|
||||
title: I18n.tr("Spacing")
|
||||
settingKey: "barSpacing"
|
||||
visible: selectedBarConfig?.enabled
|
||||
enabled: !SettingsData.frameEnabled
|
||||
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
|
||||
|
||||
SettingsSliderRow {
|
||||
id: edgeSpacingSlider
|
||||
@@ -1003,6 +1036,8 @@ Item {
|
||||
|
||||
SettingsSliderRow {
|
||||
id: barTransparencySlider
|
||||
enabled: !SettingsData.frameEnabled
|
||||
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
|
||||
text: I18n.tr("Bar Transparency")
|
||||
value: (selectedBarConfig?.transparency ?? 1.0) * 100
|
||||
minimum: 0
|
||||
@@ -1044,6 +1079,35 @@ Item {
|
||||
restoreMode: Binding.RestoreBinding
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: SettingsData.frameEnabled
|
||||
width: parent.width
|
||||
implicitHeight: transparencyFrameNote.implicitHeight + Theme.spacingS * 2
|
||||
|
||||
Row {
|
||||
id: transparencyFrameNote
|
||||
x: Theme.spacingM
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "frame_source"
|
||||
size: Theme.fontSizeMedium
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Opacity is controlled by Frame Border Opacity in Frame settings")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width - Theme.fontSizeMedium - Theme.spacingS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
@@ -1287,6 +1351,8 @@ Item {
|
||||
|
||||
SettingsToggleRow {
|
||||
text: I18n.tr("Square Corners")
|
||||
enabled: !SettingsData.frameEnabled
|
||||
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
|
||||
checked: selectedBarConfig?.squareCorners ?? false
|
||||
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
||||
squareCorners: checked
|
||||
@@ -1334,6 +1400,8 @@ Item {
|
||||
|
||||
SettingsToggleRow {
|
||||
text: I18n.tr("Goth Corners")
|
||||
enabled: !SettingsData.frameEnabled
|
||||
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
|
||||
checked: selectedBarConfig?.gothCornersEnabled ?? false
|
||||
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
||||
gothCornersEnabled: checked
|
||||
|
||||
295
quickshell/Modules/Settings/FrameTab.qml
Normal file
295
quickshell/Modules/Settings/FrameTab.qml
Normal file
@@ -0,0 +1,295 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Settings.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
LayoutMirroring.enabled: I18n.isRtl
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
DankFlickable {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
contentHeight: mainColumn.height + Theme.spacingXL
|
||||
contentWidth: width
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
topPadding: 4
|
||||
width: Math.min(550, parent.width - Theme.spacingL * 2)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
// ── Enable Frame ──────────────────────────────────────────────────
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "frame_source"
|
||||
title: I18n.tr("Frame")
|
||||
settingKey: "frameEnabled"
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "frameEnable"
|
||||
tags: ["frame", "border", "outline", "display"]
|
||||
text: I18n.tr("Enable Frame")
|
||||
description: I18n.tr("Draw a connected picture-frame border around the entire display")
|
||||
checked: SettingsData.frameEnabled
|
||||
onToggled: checked => SettingsData.set("frameEnabled", checked)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Border ────────────────────────────────────────────────────────
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "border_outer"
|
||||
title: I18n.tr("Border")
|
||||
settingKey: "frameBorder"
|
||||
collapsible: true
|
||||
visible: SettingsData.frameEnabled
|
||||
|
||||
SettingsSliderRow {
|
||||
id: roundingSlider
|
||||
settingKey: "frameRounding"
|
||||
tags: ["frame", "border", "rounding", "radius", "corner"]
|
||||
text: I18n.tr("Border Radius")
|
||||
unit: "px"
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
step: 1
|
||||
defaultValue: 23
|
||||
value: SettingsData.frameRounding
|
||||
onSliderDragFinished: v => SettingsData.set("frameRounding", v)
|
||||
|
||||
Binding {
|
||||
target: roundingSlider
|
||||
property: "value"
|
||||
value: SettingsData.frameRounding
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
id: thicknessSlider
|
||||
settingKey: "frameThickness"
|
||||
tags: ["frame", "border", "thickness", "size", "width"]
|
||||
text: I18n.tr("Border Width")
|
||||
unit: "px"
|
||||
minimum: 2
|
||||
maximum: 100
|
||||
step: 1
|
||||
defaultValue: 16
|
||||
value: SettingsData.frameThickness
|
||||
onSliderDragFinished: v => SettingsData.set("frameThickness", v)
|
||||
|
||||
Binding {
|
||||
target: thicknessSlider
|
||||
property: "value"
|
||||
value: SettingsData.frameThickness
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
id: barThicknessSlider
|
||||
settingKey: "frameBarSize"
|
||||
tags: ["frame", "bar", "thickness", "size", "height", "width"]
|
||||
text: I18n.tr("Size")
|
||||
description: I18n.tr("Height of horizontal bars / width of vertical bars in frame mode")
|
||||
unit: "px"
|
||||
minimum: 24
|
||||
maximum: 100
|
||||
step: 1
|
||||
defaultValue: 40
|
||||
value: SettingsData.frameBarSize
|
||||
onSliderDragFinished: v => SettingsData.set("frameBarSize", v)
|
||||
|
||||
Binding {
|
||||
target: barThicknessSlider
|
||||
property: "value"
|
||||
value: SettingsData.frameBarSize
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
id: opacitySlider
|
||||
settingKey: "frameOpacity"
|
||||
tags: ["frame", "border", "opacity", "transparency"]
|
||||
text: I18n.tr("Frame Opacity")
|
||||
unit: "%"
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
defaultValue: 100
|
||||
value: SettingsData.frameOpacity * 100
|
||||
onSliderDragFinished: v => SettingsData.set("frameOpacity", v / 100)
|
||||
|
||||
Binding {
|
||||
target: opacitySlider
|
||||
property: "value"
|
||||
value: SettingsData.frameOpacity * 100
|
||||
}
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
id: frameBlurToggle
|
||||
settingKey: "frameBlurEnabled"
|
||||
tags: ["frame", "blur", "background", "glass", "transparency", "frosted"]
|
||||
text: I18n.tr("Frame Blur")
|
||||
description: !BlurService.available
|
||||
? I18n.tr("Requires a newer version of Quickshell")
|
||||
: I18n.tr("Apply compositor blur behind the frame border")
|
||||
checked: SettingsData.frameBlurEnabled
|
||||
onToggled: checked => SettingsData.set("frameBlurEnabled", checked)
|
||||
enabled: BlurService.available && SettingsData.blurEnabled
|
||||
opacity: enabled ? 1.0 : 0.5
|
||||
visible: BlurService.available
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: BlurService.available && !SettingsData.blurEnabled
|
||||
width: parent.width
|
||||
height: blurToggleNote.height + Theme.spacingM * 2
|
||||
|
||||
Row {
|
||||
id: blurToggleNote
|
||||
x: Theme.spacingM
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "blur_on"
|
||||
size: Theme.fontSizeMedium
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Frame Blur is controlled by Background Blur in Theme & Colors")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width - Theme.fontSizeMedium - Theme.spacingS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Color mode buttons
|
||||
SettingsButtonGroupRow {
|
||||
settingKey: "frameColor"
|
||||
tags: ["frame", "border", "color", "theme", "primary", "surface", "default"]
|
||||
text: I18n.tr("Border color")
|
||||
model: [I18n.tr("Default"), I18n.tr("Primary"), I18n.tr("Surface"), I18n.tr("Custom")]
|
||||
currentIndex: {
|
||||
const fc = SettingsData.frameColor;
|
||||
if (!fc || fc === "default") return 0;
|
||||
if (fc === "primary") return 1;
|
||||
if (fc === "surface") return 2;
|
||||
return 3;
|
||||
}
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (!selected) return;
|
||||
switch (index) {
|
||||
case 0: SettingsData.set("frameColor", ""); break;
|
||||
case 1: SettingsData.set("frameColor", "primary"); break;
|
||||
case 2: SettingsData.set("frameColor", "surface"); break;
|
||||
case 3:
|
||||
const cur = SettingsData.frameColor;
|
||||
const isPreset = !cur || cur === "primary" || cur === "surface";
|
||||
if (isPreset) SettingsData.set("frameColor", "#2a2a2a");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom color swatch — only visible when a hex color is stored (Custom mode)
|
||||
Item {
|
||||
visible: {
|
||||
const fc = SettingsData.frameColor;
|
||||
return !!(fc && fc !== "primary" && fc !== "surface");
|
||||
}
|
||||
width: parent.width
|
||||
height: customColorRow.height + Theme.spacingM * 2
|
||||
|
||||
Row {
|
||||
id: customColorRow
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
x: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: I18n.tr("Custom color")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: colorSwatch
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: SettingsData.effectiveFrameColor
|
||||
border.color: Theme.outline
|
||||
border.width: 1
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
PopoutService.colorPickerModal.selectedColor = SettingsData.effectiveFrameColor;
|
||||
PopoutService.colorPickerModal.pickerTitle = I18n.tr("Frame Border Color");
|
||||
PopoutService.colorPickerModal.onColorSelectedCallback = function (color) {
|
||||
SettingsData.set("frameColor", color.toString());
|
||||
};
|
||||
PopoutService.colorPickerModal.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Bar Integration ───────────────────────────────────────────────
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "toolbar"
|
||||
title: I18n.tr("Bar Integration")
|
||||
settingKey: "frameBarIntegration"
|
||||
collapsible: true
|
||||
expanded: false
|
||||
visible: SettingsData.frameEnabled
|
||||
|
||||
SettingsToggleRow {
|
||||
visible: CompositorService.isNiri
|
||||
settingKey: "frameShowOnOverview"
|
||||
tags: ["frame", "overview", "show", "hide", "niri"]
|
||||
text: I18n.tr("Show on Overview")
|
||||
description: I18n.tr("Show the bar and frame during Niri overview mode")
|
||||
checked: SettingsData.frameShowOnOverview
|
||||
onToggled: checked => SettingsData.set("frameShowOnOverview", checked)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Display Assignment ────────────────────────────────────────────
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "monitor"
|
||||
title: I18n.tr("Display Assignment")
|
||||
settingKey: "frameDisplays"
|
||||
collapsible: true
|
||||
expanded: false
|
||||
visible: SettingsData.frameEnabled
|
||||
|
||||
SettingsDisplayPicker {
|
||||
displayPreferences: SettingsData.frameScreenPreferences
|
||||
onPreferencesChanged: prefs => SettingsData.set("frameScreenPreferences", prefs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,13 +46,6 @@ Item {
|
||||
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 {
|
||||
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")]
|
||||
|
||||
@@ -91,16 +91,6 @@ Item {
|
||||
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 {
|
||||
tab: "sounds"
|
||||
tags: ["sound", "notification", "new"]
|
||||
|
||||
@@ -83,7 +83,6 @@ Item {
|
||||
description: modelData.width + "×" + modelData.height
|
||||
checked: localChecked
|
||||
onToggled: isChecked => {
|
||||
localChecked = isChecked;
|
||||
var prefs = JSON.parse(JSON.stringify(root.displayPreferences));
|
||||
if (!Array.isArray(prefs) || prefs.includes("all"))
|
||||
prefs = [];
|
||||
@@ -94,6 +93,11 @@ Item {
|
||||
model: modelData.model || ""
|
||||
});
|
||||
}
|
||||
if (prefs.length === 0) {
|
||||
localChecked = true;
|
||||
return;
|
||||
}
|
||||
localChecked = isChecked;
|
||||
root.preferencesChanged(prefs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,7 +430,7 @@ Item {
|
||||
"id": widget.id,
|
||||
"enabled": widget.enabled
|
||||
};
|
||||
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion"];
|
||||
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge"];
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if (widget[keys[i]] !== undefined)
|
||||
result[keys[i]] = widget[keys[i]];
|
||||
@@ -712,8 +712,6 @@ Item {
|
||||
item.barMaxVisibleRunningApps = widget.barMaxVisibleRunningApps;
|
||||
if (widget.barShowOverflowBadge !== undefined)
|
||||
item.barShowOverflowBadge = widget.barShowOverflowBadge;
|
||||
if (widget.trayUseInlineExpansion !== undefined)
|
||||
item.trayUseInlineExpansion = widget.trayUseInlineExpansion;
|
||||
}
|
||||
widgets.push(item);
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ Column {
|
||||
"id": widget.id,
|
||||
"enabled": widget.enabled
|
||||
};
|
||||
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion"];
|
||||
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge"];
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if (widget[keys[i]] !== undefined)
|
||||
result[keys[i]] = widget[keys[i]];
|
||||
@@ -437,7 +437,7 @@ Column {
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
visible: modelData.id === "clock" || modelData.id === "focusedWindow" || modelData.id === "keyboard_layout_name" || modelData.id === "appsDock" || modelData.id === "systemTray"
|
||||
visible: modelData.id === "clock" || modelData.id === "focusedWindow" || modelData.id === "keyboard_layout_name" || modelData.id === "appsDock"
|
||||
|
||||
DankActionButton {
|
||||
id: compactModeButton
|
||||
@@ -543,39 +543,6 @@ Column {
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: trayMenuButton
|
||||
buttonSize: 32
|
||||
visible: modelData.id === "systemTray"
|
||||
iconName: "more_vert"
|
||||
iconSize: 18
|
||||
iconColor: Theme.outline
|
||||
onClicked: {
|
||||
trayContextMenu.widgetData = modelData;
|
||||
trayContextMenu.sectionId = root.sectionId;
|
||||
trayContextMenu.widgetIndex = index;
|
||||
|
||||
var buttonPos = trayMenuButton.mapToItem(root, 0, 0);
|
||||
var popupWidth = trayContextMenu.width;
|
||||
var popupHeight = trayContextMenu.height;
|
||||
|
||||
var xPos = buttonPos.x - popupWidth - Theme.spacingS;
|
||||
if (xPos < 0)
|
||||
xPos = buttonPos.x + trayMenuButton.width + Theme.spacingS;
|
||||
|
||||
var yPos = buttonPos.y - popupHeight / 2 + trayMenuButton.height / 2;
|
||||
if (yPos < 0) {
|
||||
yPos = Theme.spacingS;
|
||||
} else if (yPos + popupHeight > root.height) {
|
||||
yPos = root.height - popupHeight - Theme.spacingS;
|
||||
}
|
||||
|
||||
trayContextMenu.x = xPos;
|
||||
trayContextMenu.y = yPos;
|
||||
trayContextMenu.open();
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: compactModeTooltip
|
||||
width: tooltipText.contentWidth + Theme.spacingM * 2
|
||||
@@ -964,88 +931,6 @@ Column {
|
||||
}
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: trayContextMenu
|
||||
|
||||
property var widgetData: null
|
||||
property string sectionId: ""
|
||||
property int widgetIndex: -1
|
||||
readonly property var currentWidgetData: (widgetIndex >= 0 && widgetIndex < root.items.length) ? root.items[widgetIndex] : widgetData
|
||||
|
||||
width: 220
|
||||
height: contentColumn.implicitHeight + Theme.spacingS * 2
|
||||
padding: 0
|
||||
modal: true
|
||||
focus: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
background: Rectangle {
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 0
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
Column {
|
||||
id: contentColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: 2
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: trayOverflowArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "arrow_selector_tool"
|
||||
size: 16
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Use Inline Expansion")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: trayOverflowToggle
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 40
|
||||
height: 20
|
||||
checked: trayContextMenu.currentWidgetData?.trayUseInlineExpansion ?? false
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: trayOverflowArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
const newValue = !(trayContextMenu.currentWidgetData?.trayUseInlineExpansion ?? false);
|
||||
root.overflowSettingChanged(trayContextMenu.sectionId, trayContextMenu.widgetIndex, "trayUseInlineExpansion", newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: diskUsageContextMenu
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ Singleton {
|
||||
property var powerUnplugSound: null
|
||||
property var normalNotificationSound: null
|
||||
property var criticalNotificationSound: null
|
||||
property var loginSound: null
|
||||
property real notificationsVolume: 1.0
|
||||
property bool notificationsAudioMuted: false
|
||||
|
||||
@@ -68,16 +67,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Used in playLoginSoundIfApplicable()
|
||||
Process {
|
||||
id: loginSoundChecker
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
playLoginSound();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getAvailableSinks() {
|
||||
const hidden = SessionData.hiddenOutputDeviceNames ?? [];
|
||||
return Pipewire.nodes.values.filter(node => node.audio && node.isSink && !node.isStream && !hidden.includes(node.name));
|
||||
@@ -406,7 +395,7 @@ EOFCONFIG
|
||||
const themesToSearch = themeName !== "freedesktop" ? `${themeName} freedesktop` : themeName;
|
||||
|
||||
const script = `
|
||||
for event_key in audio-volume-change power-plug power-unplug message message-new-instant desktop-login; do
|
||||
for event_key in audio-volume-change power-plug power-unplug message message-new-instant; do
|
||||
found=0
|
||||
|
||||
case "$event_key" in
|
||||
@@ -468,8 +457,7 @@ EOFCONFIG
|
||||
"power-plug": "../assets/sounds/plasma/power-plug.wav",
|
||||
"power-unplug": "../assets/sounds/plasma/power-unplug.wav",
|
||||
"message": "../assets/sounds/freedesktop/message.wav",
|
||||
"message-new-instant": "../assets/sounds/freedesktop/message-new-instant.wav",
|
||||
"desktop-login": "../assets/sounds/freedesktop/desktop-login.wav"
|
||||
"message-new-instant": "../assets/sounds/freedesktop/message-new-instant.wav"
|
||||
};
|
||||
|
||||
const specialConditions = {
|
||||
@@ -563,10 +551,6 @@ EOFCONFIG
|
||||
criticalNotificationSound.destroy();
|
||||
criticalNotificationSound = null;
|
||||
}
|
||||
if (loginSound) {
|
||||
loginSound.destroy();
|
||||
loginSound = null;
|
||||
}
|
||||
}
|
||||
|
||||
function createSoundPlayers() {
|
||||
@@ -638,19 +622,6 @@ EOFCONFIG
|
||||
}
|
||||
}
|
||||
`, 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) {
|
||||
console.warn("AudioService: Error creating sound players:", e);
|
||||
}
|
||||
@@ -690,31 +661,6 @@ EOFCONFIG
|
||||
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() {
|
||||
if (SettingsData.soundsEnabled && SettingsData.soundVolumeChanged && !notificationsAudioMuted) {
|
||||
playVolumeChangeSound();
|
||||
|
||||
@@ -255,12 +255,6 @@ Singleton {
|
||||
return pinnedEntries.some(pinnedEntry => pinnedEntry.hash === entryHash);
|
||||
}
|
||||
|
||||
onClipboardAvailableChanged: {
|
||||
if (!clipboardAvailable || refCount <= 0)
|
||||
return;
|
||||
refresh();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DMSService
|
||||
enabled: root.refCount > 0
|
||||
|
||||
@@ -819,7 +819,6 @@ Singleton {
|
||||
if (event.event === "unlock" || event.event === "resume") {
|
||||
suppressOsd = true;
|
||||
osdSuppressTimer.restart();
|
||||
evaluateNightMode();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1051,19 +1050,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function status(): string {
|
||||
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();
|
||||
return root.nightModeEnabled ? "Night mode is enabled" : "Night mode is disabled";
|
||||
}
|
||||
|
||||
function temperature(value: string): string {
|
||||
|
||||
@@ -4,75 +4,10 @@ pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Mpris
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property list<MprisPlayer> availablePlayers: Mpris.players.values
|
||||
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();
|
||||
}
|
||||
property MprisPlayer activePlayer: availablePlayers.find(p => p.isPlaying) ?? availablePlayers.find(p => p.canControl && p.canPlay) ?? null
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -59,10 +58,6 @@ Singleton {
|
||||
}
|
||||
|
||||
readonly property bool screensharingActive: {
|
||||
if (CompositorService.isNiri && NiriService.hasActiveCast) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!Pipewire.ready || !Pipewire.nodes?.values) {
|
||||
return false
|
||||
}
|
||||
@@ -79,12 +74,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
if (node.properties && node.properties["media.class"] === "Stream/Output/Video") {
|
||||
if (looksLikeScreencast(node)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (node.properties && node.properties["media.class"] === "Stream/Input/Audio") {
|
||||
const mediaName = (node.properties["media.name"] || "").toLowerCase()
|
||||
const appName = (node.properties["application.name"] || "").toLowerCase()
|
||||
@@ -121,9 +110,8 @@ Singleton {
|
||||
}
|
||||
const appName = (node.properties && node.properties["application.name"] || "").toLowerCase()
|
||||
const nodeName = (node.name || "").toLowerCase()
|
||||
const mediaName = (node.properties && node.properties["media.name"] || "").toLowerCase()
|
||||
const combined = appName + " " + nodeName + " " + mediaName
|
||||
return /xdg-desktop-portal|xdpw|screencast|screen-cast|screen|gnome shell|kwin|obs|niri/.test(combined)
|
||||
const combined = appName + " " + nodeName
|
||||
return /xdg-desktop-portal|xdpw|screencast|screen|gnome shell|kwin|obs/.test(combined)
|
||||
}
|
||||
|
||||
function getMicrophoneStatus() {
|
||||
|
||||
@@ -231,10 +231,7 @@ Singleton {
|
||||
return;
|
||||
isChecking = true;
|
||||
hasError = false;
|
||||
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) {
|
||||
if (updChecker.length > 0) {
|
||||
updateChecker.command = [updChecker].concat(updateCheckerParams[updChecker].listUpdatesSettings.params);
|
||||
} else {
|
||||
updateChecker.command = [pkgManager].concat(packageManagerParams[pkgManager].listUpdatesSettings.params);
|
||||
|
||||
@@ -8,122 +8,13 @@ Item {
|
||||
id: root
|
||||
|
||||
property MprisPlayer activePlayer
|
||||
property real seekPreviewRatio: -1
|
||||
readonly property real playerValue: {
|
||||
if (!activePlayer || activePlayer.length <= 0)
|
||||
return 0;
|
||||
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: {
|
||||
if (!activePlayer || activePlayer.length <= 0) return 0
|
||||
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 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
|
||||
|
||||
@@ -138,35 +29,58 @@ Item {
|
||||
|
||||
M3WaveProgress {
|
||||
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
|
||||
|
||||
MouseArea {
|
||||
id: waveMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0
|
||||
|
||||
property real pendingSeekPosition: -1
|
||||
property real pressX: 0
|
||||
|
||||
Timer {
|
||||
id: waveHoldIndicatorTimer
|
||||
interval: root.holdIndicatorDelay
|
||||
repeat: false
|
||||
id: waveSeekDebounceTimer
|
||||
interval: 150
|
||||
onTriggered: {
|
||||
if (parent.pressed && root.isSeeking)
|
||||
root.isDraggingSeek = true;
|
||||
if (parent.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
|
||||
const clamped = Math.min(parent.pendingSeekPosition, activePlayer.length * 0.99)
|
||||
activePlayer.position = clamped
|
||||
parent.pendingSeekPosition = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPressed: mouse => root.handleSeekPressed(mouse, parent.width, waveMouseArea, waveHoldIndicatorTimer)
|
||||
onReleased: root.handleSeekReleased(waveMouseArea, waveHoldIndicatorTimer)
|
||||
onPositionChanged: mouse => root.handleSeekPositionChanged(mouse, parent.width, waveMouseArea)
|
||||
onCanceled: root.handleSeekCanceled(waveMouseArea, waveHoldIndicatorTimer)
|
||||
onPressed: (mouse) => {
|
||||
root.isSeeking = true
|
||||
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
|
||||
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
|
||||
pendingSeekPosition = r * activePlayer.length
|
||||
waveSeekDebounceTimer.restart()
|
||||
}
|
||||
}
|
||||
onReleased: {
|
||||
root.isSeeking = false
|
||||
waveSeekDebounceTimer.stop()
|
||||
if (pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
|
||||
const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
|
||||
activePlayer.position = clamped
|
||||
pendingSeekPosition = -1
|
||||
}
|
||||
}
|
||||
onPositionChanged: (mouse) => {
|
||||
if (pressed && root.isSeeking && activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
|
||||
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
|
||||
pendingSeekPosition = r * activePlayer.length
|
||||
waveSeekDebounceTimer.restart()
|
||||
}
|
||||
}
|
||||
onClicked: (mouse) => {
|
||||
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
|
||||
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
|
||||
activePlayer.position = r * activePlayer.length
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,7 +93,6 @@ Item {
|
||||
property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40)
|
||||
property color fillColor: 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
|
||||
|
||||
Rectangle {
|
||||
@@ -197,22 +110,7 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: parent.fillColor
|
||||
radius: height / 2
|
||||
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
|
||||
Behavior on width { NumberAnimation { duration: 80 } }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@@ -224,37 +122,59 @@ Item {
|
||||
x: Math.max(0, Math.min(parent.width, parent.width * root.value)) - width / 2
|
||||
y: parent.midY - height / 2
|
||||
z: 3
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: 80
|
||||
}
|
||||
}
|
||||
Behavior on x { NumberAnimation { duration: 80 } }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: flatMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0
|
||||
|
||||
property real pendingSeekPosition: -1
|
||||
property real pressX: 0
|
||||
|
||||
Timer {
|
||||
id: flatHoldIndicatorTimer
|
||||
interval: root.holdIndicatorDelay
|
||||
repeat: false
|
||||
id: flatSeekDebounceTimer
|
||||
interval: 150
|
||||
onTriggered: {
|
||||
if (parent.pressed && root.isSeeking)
|
||||
root.isDraggingSeek = true;
|
||||
if (parent.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
|
||||
const clamped = Math.min(parent.pendingSeekPosition, activePlayer.length * 0.99)
|
||||
activePlayer.position = clamped
|
||||
parent.pendingSeekPosition = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPressed: mouse => root.handleSeekPressed(mouse, parent.width, flatMouseArea, flatHoldIndicatorTimer)
|
||||
onReleased: root.handleSeekReleased(flatMouseArea, flatHoldIndicatorTimer)
|
||||
onPositionChanged: mouse => root.handleSeekPositionChanged(mouse, parent.width, flatMouseArea)
|
||||
onCanceled: root.handleSeekCanceled(flatMouseArea, flatHoldIndicatorTimer)
|
||||
onPressed: (mouse) => {
|
||||
root.isSeeking = true
|
||||
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
|
||||
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
|
||||
pendingSeekPosition = r * activePlayer.length
|
||||
flatSeekDebounceTimer.restart()
|
||||
}
|
||||
}
|
||||
onReleased: {
|
||||
root.isSeeking = false
|
||||
flatSeekDebounceTimer.stop()
|
||||
if (pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
|
||||
const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
|
||||
activePlayer.position = clamped
|
||||
pendingSeekPosition = -1
|
||||
}
|
||||
}
|
||||
onPositionChanged: (mouse) => {
|
||||
if (pressed && root.isSeeking && activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
|
||||
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
|
||||
pendingSeekPosition = r * activePlayer.length
|
||||
flatSeekDebounceTimer.restart()
|
||||
}
|
||||
}
|
||||
onClicked: (mouse) => {
|
||||
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
|
||||
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
|
||||
activePlayer.position = r * activePlayer.length
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ Item {
|
||||
id: root
|
||||
|
||||
property real value: 0
|
||||
property real actualValue: value
|
||||
property bool showActualPlaybackState: false
|
||||
property real lineWidth: 2
|
||||
property real wavelength: 20
|
||||
property real amp: 1.6
|
||||
@@ -17,7 +15,6 @@ Item {
|
||||
property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40)
|
||||
property color fillColor: 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)
|
||||
function snap(v) {
|
||||
@@ -25,12 +22,7 @@ Item {
|
||||
}
|
||||
|
||||
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 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 {
|
||||
NumberAnimation {
|
||||
@@ -73,9 +65,7 @@ Item {
|
||||
|
||||
readonly property real startX: snap(root.lineWidth / 2)
|
||||
readonly property real aaBias: (0.25 / root.dpr)
|
||||
readonly property real endX: root.previewAhead ? Math.max(startX, Math.min(root.actualX - aaBias, width)) : Math.max(startX, Math.min(root.playX - startX - aaBias, width))
|
||||
readonly property real 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))
|
||||
readonly property real endX: Math.max(startX, Math.min(root.playX - startX - aaBias, width))
|
||||
|
||||
Rectangle {
|
||||
id: mask
|
||||
@@ -110,37 +100,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: actualMask
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
x: waveClip.gapStartX
|
||||
width: Math.max(0, waveClip.gapEndX - waveClip.gapStartX)
|
||||
color: "transparent"
|
||||
clip: true
|
||||
visible: (root.previewBehind || root.previewAhead) && width > 0
|
||||
|
||||
Shape {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: root.width + 4 * root.wavelength
|
||||
antialiasing: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
x: waveOffsetX
|
||||
|
||||
ShapePath {
|
||||
strokeColor: root.actualProgressColor
|
||||
strokeWidth: snap(root.lineWidth)
|
||||
capStyle: ShapePath.RoundCap
|
||||
joinStyle: ShapePath.RoundJoin
|
||||
fillColor: "transparent"
|
||||
PathSvg {
|
||||
path: waveSvg.path
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: startCap
|
||||
width: snap(root.lineWidth)
|
||||
@@ -148,7 +107,7 @@ Item {
|
||||
radius: width / 2
|
||||
color: root.fillColor
|
||||
x: waveClip.startX - width / 2
|
||||
y: waveY(waveClip.startX) - height / 2
|
||||
y: root.midY - height / 2 + root.currentAmp * Math.sin((waveClip.startX / root.wavelength) * 2 * Math.PI + root.phase)
|
||||
visible: waveClip.endX > waveClip.startX
|
||||
z: 2
|
||||
}
|
||||
@@ -160,34 +119,10 @@ Item {
|
||||
radius: width / 2
|
||||
color: root.fillColor
|
||||
x: waveClip.endX - width / 2
|
||||
y: waveY(waveClip.endX) - height / 2
|
||||
y: root.midY - height / 2 + root.currentAmp * Math.sin((waveClip.endX / root.wavelength) * 2 * Math.PI + root.phase)
|
||||
visible: waveClip.endX > waveClip.startX
|
||||
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 {
|
||||
@@ -206,10 +141,6 @@ Item {
|
||||
let r = a % m;
|
||||
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)
|
||||
|
||||
FrameAnimation {
|
||||
@@ -217,9 +148,8 @@ Item {
|
||||
onTriggered: {
|
||||
if (root.isPlaying)
|
||||
root.phase += 0.03 * frameTime * 60;
|
||||
startCap.y = waveY(waveClip.startX) - startCap.height / 2;
|
||||
endCap.y = waveY(waveClip.endX) - endCap.height / 2;
|
||||
actualEndCap.y = waveY(waveClip.gapEndX) - actualEndCap.height / 2;
|
||||
startCap.y = root.midY - startCap.height / 2 + root.currentAmp * Math.sin((waveClip.startX / root.wavelength) * 2 * Math.PI + root.phase);
|
||||
endCap.y = root.midY - endCap.height / 2 + root.currentAmp * Math.sin((waveClip.endX / root.wavelength) * 2 * Math.PI + root.phase);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -86,13 +86,13 @@ def create_poeditor_json(translations):
|
||||
references.append(ref)
|
||||
|
||||
contexts = sorted(data['contexts']) if data['contexts'] else []
|
||||
comment = " | ".join(contexts) if contexts else ""
|
||||
context_str = " | ".join(contexts) if contexts else term
|
||||
|
||||
entry = {
|
||||
"term": term,
|
||||
"context": term,
|
||||
"context": context_str,
|
||||
"reference": ", ".join(references),
|
||||
"comment": comment
|
||||
"comment": ""
|
||||
}
|
||||
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
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
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
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
@@ -2112,26 +2112,6 @@
|
||||
],
|
||||
"icon": "history"
|
||||
},
|
||||
{
|
||||
"section": "rememberLastQuery",
|
||||
"label": "Remember Last Query",
|
||||
"tabIndex": 9,
|
||||
"category": "Launcher",
|
||||
"keywords": [
|
||||
"autofill",
|
||||
"drawer",
|
||||
"last",
|
||||
"launcher",
|
||||
"menu",
|
||||
"opened",
|
||||
"query",
|
||||
"remember",
|
||||
"remembered",
|
||||
"search",
|
||||
"start"
|
||||
],
|
||||
"description": "Autofill last remembered query when opened"
|
||||
},
|
||||
{
|
||||
"section": "searchAppActions",
|
||||
"label": "Search App Actions",
|
||||
@@ -2398,47 +2378,6 @@
|
||||
],
|
||||
"icon": "schedule"
|
||||
},
|
||||
{
|
||||
"section": "blurEnabled",
|
||||
"label": "Background Blur",
|
||||
"tabIndex": 10,
|
||||
"category": "Theme & Colors",
|
||||
"keywords": [
|
||||
"alert",
|
||||
"alerts",
|
||||
"appearance",
|
||||
"background",
|
||||
"bars",
|
||||
"behind",
|
||||
"blur",
|
||||
"colors",
|
||||
"compositor",
|
||||
"config",
|
||||
"configuration",
|
||||
"configure",
|
||||
"frosted",
|
||||
"glass",
|
||||
"look",
|
||||
"modals",
|
||||
"notif",
|
||||
"notifications",
|
||||
"notifs",
|
||||
"panel",
|
||||
"popouts",
|
||||
"requires",
|
||||
"scheme",
|
||||
"setup",
|
||||
"statusbar",
|
||||
"style",
|
||||
"support",
|
||||
"taskbar",
|
||||
"theme",
|
||||
"topbar",
|
||||
"transparency"
|
||||
],
|
||||
"icon": "blur_on",
|
||||
"description": "Blur the background behind bars, popouts, modals, and notifications. Requires compositor support and configuration."
|
||||
},
|
||||
{
|
||||
"section": "barElevationEnabled",
|
||||
"label": "Bar Shadows",
|
||||
@@ -2466,49 +2405,6 @@
|
||||
],
|
||||
"description": "Shadow elevation on bars and panels"
|
||||
},
|
||||
{
|
||||
"section": "blurBorderColor",
|
||||
"label": "Blur Border Color",
|
||||
"tabIndex": 10,
|
||||
"category": "Theme & Colors",
|
||||
"keywords": [
|
||||
"appearance",
|
||||
"around",
|
||||
"blur",
|
||||
"blurred",
|
||||
"border",
|
||||
"color",
|
||||
"colors",
|
||||
"colour",
|
||||
"edge",
|
||||
"hue",
|
||||
"look",
|
||||
"outline",
|
||||
"scheme",
|
||||
"style",
|
||||
"surfaces",
|
||||
"theme",
|
||||
"tint"
|
||||
],
|
||||
"description": "Border color around blurred surfaces"
|
||||
},
|
||||
{
|
||||
"section": "blurBorderOpacity",
|
||||
"label": "Blur Border Opacity",
|
||||
"tabIndex": 10,
|
||||
"category": "Theme & Colors",
|
||||
"keywords": [
|
||||
"appearance",
|
||||
"blur",
|
||||
"border",
|
||||
"colors",
|
||||
"look",
|
||||
"opacity",
|
||||
"scheme",
|
||||
"style",
|
||||
"theme"
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "niriLayoutBorderSize",
|
||||
"label": "Border Size",
|
||||
@@ -4488,6 +4384,27 @@
|
||||
],
|
||||
"description": "Automatically lock the screen when DMS starts"
|
||||
},
|
||||
{
|
||||
"section": "lockBeforeSuspend",
|
||||
"label": "Lock before suspend",
|
||||
"tabIndex": 11,
|
||||
"category": "Lock Screen",
|
||||
"keywords": [
|
||||
"automatic",
|
||||
"automatically",
|
||||
"before",
|
||||
"lock",
|
||||
"login",
|
||||
"password",
|
||||
"prepares",
|
||||
"screen",
|
||||
"security",
|
||||
"sleep",
|
||||
"suspend",
|
||||
"system"
|
||||
],
|
||||
"description": "Automatically lock the screen when the system prepares to suspend"
|
||||
},
|
||||
{
|
||||
"section": "lockScreenNotificationMode",
|
||||
"label": "Notification Display",
|
||||
@@ -5101,26 +5018,6 @@
|
||||
],
|
||||
"description": "Play sounds for system events"
|
||||
},
|
||||
{
|
||||
"section": "soundLogin",
|
||||
"label": "Login",
|
||||
"tabIndex": 15,
|
||||
"category": "Sounds",
|
||||
"keywords": [
|
||||
"after",
|
||||
"audio",
|
||||
"boot",
|
||||
"effects",
|
||||
"logging",
|
||||
"login",
|
||||
"play",
|
||||
"sfx",
|
||||
"sound",
|
||||
"sounds",
|
||||
"startup"
|
||||
],
|
||||
"description": "Play sound after logging in"
|
||||
},
|
||||
{
|
||||
"section": "soundNewNotification",
|
||||
"label": "New Notification",
|
||||
@@ -6510,27 +6407,6 @@
|
||||
"icon": "schedule",
|
||||
"description": "Gradually fade the screen before locking with a configurable grace period"
|
||||
},
|
||||
{
|
||||
"section": "lockBeforeSuspend",
|
||||
"label": "Lock before suspend",
|
||||
"tabIndex": 21,
|
||||
"category": "Power & Sleep",
|
||||
"keywords": [
|
||||
"automatically",
|
||||
"before",
|
||||
"energy",
|
||||
"lock",
|
||||
"power",
|
||||
"prepares",
|
||||
"screen",
|
||||
"security",
|
||||
"shutdown",
|
||||
"sleep",
|
||||
"suspend",
|
||||
"system"
|
||||
],
|
||||
"description": "Automatically lock the screen when the system prepares to suspend"
|
||||
},
|
||||
{
|
||||
"section": "fadeToLockGracePeriod",
|
||||
"label": "Lock fade grace period",
|
||||
|
||||
@@ -1182,13 +1182,6 @@
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Applying authentication changes…",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Apps",
|
||||
"translation": "",
|
||||
@@ -1385,34 +1378,6 @@
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Authentication changes applied.",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Authentication changes apply automatically.",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Authentication changes apply automatically. Fingerprint-only login may not unlock Keyring.",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Authentication changes need sudo. Opening terminal so you can use password or fingerprint.",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Authentication error - try again",
|
||||
"translation": "",
|
||||
@@ -1553,13 +1518,6 @@
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Autofill last remembered query when opened",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Automatic Color Mode",
|
||||
"translation": "",
|
||||
@@ -1714,13 +1672,6 @@
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Background Blur",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Background Opacity",
|
||||
"translation": "",
|
||||
@@ -1728,13 +1679,6 @@
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Background authentication sync failed. Trying terminal mode.",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Background image",
|
||||
"translation": "",
|
||||
@@ -1931,20 +1875,6 @@
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Blur Border Color",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Blur Border Opacity",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Blur Wallpaper Layer",
|
||||
"translation": "",
|
||||
@@ -1959,13 +1889,6 @@
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Blur the background behind bars, popouts, modals, and notifications. Requires compositor support and configuration.",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Blur wallpaper when niri overview is open",
|
||||
"translation": "",
|
||||
@@ -2015,13 +1938,6 @@
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Border color around blurred surfaces",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Border with BG",
|
||||
"translation": "",
|
||||
@@ -2373,7 +2289,7 @@
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Check sync status on demand. Sync copies your theme, settings, and wallpaper configuration to the login screen. Authentication changes apply automatically.",
|
||||
"term": "Check sync status on demand. Sync copies your theme, settings, PAM config, and wallpaper to the login screen in one step. Must run Sync to apply changes.",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
@@ -3628,7 +3544,7 @@
|
||||
{
|
||||
"term": "Custom",
|
||||
"translation": "",
|
||||
"context": "blur border color | shadow color option | theme category option",
|
||||
"context": "shadow color option | theme category option",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
@@ -3801,7 +3717,7 @@
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "DMS greeter needs: greetd, dms-greeter. Fingerprint: fprintd, pam_fprintd. Security keys: pam_u2f. Add your user to the greeter group. Authentication changes apply automatically and may open a terminal when sudo authentication is required.",
|
||||
"term": "DMS greeter needs: greetd, dms-greeter. Fingerprint: fprintd, pam_fprintd. Security keys: pam_u2f. Add your user to the greeter group. Sync checks sudo first and opens a terminal when interactive authentication is required.",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
@@ -4872,7 +4788,7 @@
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Enable fingerprint or security key for DMS Greeter. Authentication changes apply automatically.",
|
||||
"term": "Enable fingerprint or security key for DMS Greeter. Run Sync to apply and configure PAM.",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
@@ -4920,13 +4836,6 @@
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Enabled, but no prints are enrolled yet. Authentication changes apply automatically once you enroll fingerprints.",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Enabled, but no prints are enrolled yet. Enroll fingerprints and run Sync.",
|
||||
"translation": "",
|
||||
@@ -4935,7 +4844,7 @@
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Enabled, but no registered security key was found yet. Authentication changes apply automatically once your key is registered or your U2F config is updated.",
|
||||
"term": "Enabled, but no prints are enrolled yet. Enroll fingerprints to use it.",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
@@ -4948,6 +4857,13 @@
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Enabled, but no registered security key was found yet. Register a key or update your U2F config.",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Enabled, but security-key availability could not be confirmed.",
|
||||
"translation": "",
|
||||
@@ -5893,27 +5809,6 @@
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Fingerprint error",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Fingerprint error: %1",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Fingerprint not recognized (%1/%2). Please try again or use password.",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and enroll later.",
|
||||
"translation": "",
|
||||
@@ -7244,13 +7139,6 @@
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Incorrect password - try again",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Indicator Style",
|
||||
"translation": "",
|
||||
@@ -7314,13 +7202,6 @@
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Insert your security key...",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Install",
|
||||
"translation": "",
|
||||
@@ -8049,13 +7930,6 @@
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Lock screen authentication changes apply automatically and may open a terminal when sudo authentication is required.",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Locked",
|
||||
"translation": "",
|
||||
@@ -8441,13 +8315,6 @@
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Maximum fingerprint attempts reached. Please use password.",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Maximum number of clipboard entries to keep",
|
||||
"translation": "",
|
||||
@@ -10264,7 +10131,7 @@
|
||||
{
|
||||
"term": "Outline",
|
||||
"translation": "",
|
||||
"context": "blur border color | outline color",
|
||||
"context": "outline color",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
@@ -11195,7 +11062,7 @@
|
||||
{
|
||||
"term": "Primary",
|
||||
"translation": "",
|
||||
"context": "blur border color | button color option | color option | primary color | shadow color option | tile color option",
|
||||
"context": "button color option | color option | primary color | shadow color option | tile color option",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
@@ -11612,13 +11479,6 @@
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Remember Last Query",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Remember last session",
|
||||
"translation": "",
|
||||
@@ -11745,13 +11605,6 @@
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Requires a newer version of Quickshell",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Requires night mode support",
|
||||
"translation": "",
|
||||
@@ -11997,6 +11850,20 @@
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Run Sync to apply.",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Run Sync to apply. Fingerprint-only login may not unlock GNOME Keyring.",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Run User Templates",
|
||||
"translation": "",
|
||||
@@ -12392,7 +12259,7 @@
|
||||
{
|
||||
"term": "Secondary",
|
||||
"translation": "",
|
||||
"context": "blur border color | button color option | color option | secondary color | tile color option",
|
||||
"context": "button color option | color option | secondary color | tile color option",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
@@ -14188,13 +14055,6 @@
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Terminal fallback failed. Install a supported terminal emulator or run 'dms auth sync' manually.",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Terminal fallback failed. Install one of the supported terminal emulators or run 'dms greeter sync' manually.",
|
||||
"translation": "",
|
||||
@@ -14202,13 +14062,6 @@
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Terminal fallback opened. Complete authentication setup there; it will close automatically when done.",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Terminal fallback opened. Complete sync there; it will close automatically when done.",
|
||||
"translation": "",
|
||||
@@ -14223,13 +14076,6 @@
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Terminal opened. Complete authentication setup there; it will close automatically when done.",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Terminal opened. Complete sync authentication there; it will close automatically when done.",
|
||||
"translation": "",
|
||||
@@ -14282,7 +14128,7 @@
|
||||
{
|
||||
"term": "Text Color",
|
||||
"translation": "",
|
||||
"context": "blur border color | shadow color option",
|
||||
"context": "shadow color option",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
@@ -14643,13 +14489,6 @@
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Too many attempts - locked out",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Too many failed attempts - account may be locked",
|
||||
"translation": "",
|
||||
@@ -14734,13 +14573,6 @@
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Touch your security key...",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Transform",
|
||||
"translation": "",
|
||||
@@ -14846,6 +14678,20 @@
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Type to search",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Type to search apps",
|
||||
"translation": "",
|
||||
"context": "",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Type to search files",
|
||||
"translation": "",
|
||||
|
||||
Reference in New Issue
Block a user