mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-06 20:42:07 -04:00
Compare commits
42 Commits
frame
...
cc858c5557
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc858c5557 | ||
|
|
b49730a01b | ||
|
|
4335403d19 | ||
|
|
e3483bf88a | ||
|
|
c29082c6cf | ||
|
|
771c0f3cff | ||
|
|
2e42cfd7c4 | ||
|
|
8d72d86256 | ||
|
|
93f37456b6 | ||
|
|
5f4f1b0b22 | ||
|
|
48a4ee1ed5 | ||
|
|
8419c560b1 | ||
|
|
2b2b6f7a55 | ||
|
|
117d248cc7 | ||
|
|
f61438e11f | ||
|
|
8f78163941 | ||
|
|
f894d338fc | ||
|
|
f2df53afcd | ||
|
|
4179fcee83 | ||
|
|
a0c9af1ee7 | ||
|
|
049266271a | ||
|
|
0eabda3164 | ||
|
|
32c063aab8 | ||
|
|
37f92677cf | ||
|
|
13e8130858 | ||
|
|
f6e590a518 | ||
|
|
3194fc3fbe | ||
|
|
3318864ece | ||
|
|
e224417593 | ||
|
|
3f7f6c5d2c | ||
|
|
0b88055742 | ||
|
|
2b0826e397 | ||
|
|
7db04c9660 | ||
|
|
14d1e1d985 | ||
|
|
903ab1e61d | ||
|
|
5982655539 | ||
|
|
1021a210cf | ||
|
|
e34edb15bb | ||
|
|
61ee5f4336 | ||
|
|
ce2a92ec27 | ||
|
|
66ce79b9bf | ||
|
|
30dd640314 |
@@ -1,26 +1,13 @@
|
||||
repos:
|
||||
- repo: local
|
||||
- repo: https://github.com/golangci/golangci-lint
|
||||
rev: v2.10.1
|
||||
hooks:
|
||||
- id: golangci-lint-fmt
|
||||
name: golangci-lint-fmt
|
||||
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 fmt
|
||||
language: system
|
||||
require_serial: true
|
||||
types: [go]
|
||||
pass_filenames: false
|
||||
- id: golangci-lint-full
|
||||
name: golangci-lint-full
|
||||
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 run --fix
|
||||
language: system
|
||||
require_serial: true
|
||||
types: [go]
|
||||
pass_filenames: false
|
||||
- id: golangci-lint-config-verify
|
||||
name: golangci-lint-config-verify
|
||||
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 config verify
|
||||
language: system
|
||||
files: \.golangci\.(?:yml|yaml|toml|json)
|
||||
pass_filenames: false
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: go-test
|
||||
name: go test
|
||||
entry: go test ./...
|
||||
|
||||
@@ -82,7 +82,7 @@ func (ds *DoctorStatus) OKCount() int {
|
||||
}
|
||||
|
||||
var (
|
||||
quickshellVersionRegex = regexp.MustCompile(`quickshell (\d+\.\d+\.\d+)`)
|
||||
quickshellVersionRegex = regexp.MustCompile(`(?i)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,10 +820,14 @@ func checkOptionalDependencies() []checkResult {
|
||||
results = append(results, checkImageFormatPlugins()...)
|
||||
|
||||
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
|
||||
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
|
||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], "", optionalFeaturesURL})
|
||||
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})
|
||||
} else {
|
||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, or alacritty", optionalFeaturesURL})
|
||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, foot or alacritty", optionalFeaturesURL})
|
||||
}
|
||||
|
||||
networkResult, err := network.DetectNetworkStack()
|
||||
|
||||
@@ -109,16 +109,41 @@ func updateArchLinux() error {
|
||||
}
|
||||
|
||||
var packageName string
|
||||
if isArchPackageInstalled("dms-shell-bin") {
|
||||
packageName = "dms-shell-bin"
|
||||
var isAUR bool
|
||||
if isArchPackageInstalled("dms-shell") {
|
||||
packageName = "dms-shell"
|
||||
} 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: Neither dms-shell-bin nor dms-shell-git package found.")
|
||||
fmt.Println("Info: No dms-shell package found.")
|
||||
fmt.Println("Info: Falling back to git-based update method...")
|
||||
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,6 +5,7 @@ package main
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
@@ -30,7 +31,9 @@ func init() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
if os.Geteuid() == 0 {
|
||||
clipboard.MaybeServeAndExit()
|
||||
|
||||
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
|
||||
log.Fatal("This program should not be run as root. Exiting.")
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ package main
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
@@ -27,7 +28,9 @@ func init() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
if os.Geteuid() == 0 {
|
||||
clipboard.MaybeServeAndExit()
|
||||
|
||||
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
|
||||
log.Fatal("This program should not be run as root. Exiting.")
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,22 @@ 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,7 +1,6 @@
|
||||
package clipboard
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -13,66 +12,142 @@ 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 CopyReader(bytes.NewReader(data), mimeType, false, false)
|
||||
return copyForkCached(data, mimeType, false)
|
||||
}
|
||||
|
||||
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
||||
if foreground {
|
||||
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 serveClipboard(data, mimeType, pasteOnce)
|
||||
}
|
||||
return CopyReader(bytes.NewReader(data), mimeType, foreground, pasteOnce)
|
||||
return copyForkCached(data, mimeType, pasteOnce)
|
||||
}
|
||||
|
||||
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
|
||||
if !foreground {
|
||||
return copyFork(data, mimeType, pasteOnce)
|
||||
if foreground {
|
||||
buf, err := io.ReadAll(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read source: %w", err)
|
||||
}
|
||||
return serveClipboard(buf, mimeType, pasteOnce)
|
||||
}
|
||||
return copyServeReader(data, mimeType, pasteOnce)
|
||||
return copyFork(data, mimeType, pasteOnce)
|
||||
}
|
||||
|
||||
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
||||
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:]...)
|
||||
func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd {
|
||||
cmd := exec.Command(os.Args[0])
|
||||
cmd.Stderr = nil
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||
cmd.Env = append(os.Environ(), "DMS_CLIP_FORKED=1")
|
||||
cmd.Env = append(os.Environ(),
|
||||
envServe+"=1",
|
||||
envMime+"="+mimeType,
|
||||
)
|
||||
if pasteOnce {
|
||||
cmd.Env = append(cmd.Env, envPasteOnce+"=1")
|
||||
}
|
||||
cmd.Env = append(cmd.Env, extra...)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func waitReady(cmd *exec.Cmd) error {
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
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
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("start: %w", err)
|
||||
}
|
||||
return waitReady(cmd)
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -83,50 +158,22 @@ 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)
|
||||
var buf [1]byte
|
||||
if _, err := stdout.Read(buf[:]); err != nil {
|
||||
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func signalReady() {
|
||||
if os.Getenv("DMS_CLIP_FORKED") == "" {
|
||||
if os.Getenv(envServe) == "" {
|
||||
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{}
|
||||
|
||||
@@ -147,7 +194,7 @@ func createClipboardCacheFile() (*os.File, error) {
|
||||
return os.CreateTemp("", "dms-clipboard-*")
|
||||
}
|
||||
|
||||
func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOnce bool) error {
|
||||
func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
||||
display, err := wlclient.Connect("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("wayland connect: %w", err)
|
||||
@@ -189,12 +236,10 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
|
||||
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")
|
||||
}
|
||||
@@ -233,18 +278,12 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
|
||||
|
||||
cancelled := make(chan struct{})
|
||||
pasted := make(chan struct{}, 1)
|
||||
sendErr := make(chan error, 1)
|
||||
|
||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||
defer syscall.Close(e.Fd)
|
||||
_ = syscall.SetNonblock(e.Fd, false)
|
||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||
defer file.Close()
|
||||
if err := writeTo(file); err != nil {
|
||||
select {
|
||||
case sendErr <- err:
|
||||
default:
|
||||
}
|
||||
}
|
||||
_, _ = file.Write(data)
|
||||
select {
|
||||
case pasted <- struct{}{}:
|
||||
default:
|
||||
@@ -266,8 +305,6 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
|
||||
select {
|
||||
case <-cancelled:
|
||||
return nil
|
||||
case err := <-sendErr:
|
||||
return err
|
||||
case <-pasted:
|
||||
if pasteOnce {
|
||||
return nil
|
||||
@@ -521,12 +558,10 @@ 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")
|
||||
}
|
||||
@@ -554,12 +589,12 @@ func copyMultiServe(offers []Offer, pasteOnce bool) error {
|
||||
pasted := make(chan struct{}, 1)
|
||||
|
||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||
defer syscall.Close(e.Fd)
|
||||
_ = syscall.SetNonblock(e.Fd, false)
|
||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||
defer file.Close()
|
||||
|
||||
if data, ok := offerMap[e.MimeType]; ok {
|
||||
file.Write(data)
|
||||
_, _ = file.Write(data)
|
||||
}
|
||||
|
||||
select {
|
||||
|
||||
@@ -39,11 +39,10 @@ type LayerSurface struct {
|
||||
wlSurface *client.Surface
|
||||
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
|
||||
viewport *wp_viewporter.WpViewport
|
||||
wlPool *client.ShmPool
|
||||
wlBuffer *client.Buffer
|
||||
bufferBusy bool
|
||||
oldPool *client.ShmPool
|
||||
oldBuffer *client.Buffer
|
||||
wlPools [2]*client.ShmPool
|
||||
wlBuffers [2]*client.Buffer
|
||||
slotBusy [2]bool
|
||||
needsRedraw bool
|
||||
scopyBuffer *client.Buffer
|
||||
configured bool
|
||||
hidden bool
|
||||
@@ -136,6 +135,7 @@ func (p *Picker) Run() (*Color, error) {
|
||||
break
|
||||
}
|
||||
|
||||
p.flushRedraws()
|
||||
p.checkDone()
|
||||
}
|
||||
|
||||
@@ -164,6 +164,15 @@ func (p *Picker) checkDone() {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Picker) flushRedraws() {
|
||||
for _, ls := range p.surfaces {
|
||||
if !ls.needsRedraw {
|
||||
continue
|
||||
}
|
||||
p.redrawSurface(ls)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Picker) connect() error {
|
||||
display, err := client.Connect("")
|
||||
if err != nil {
|
||||
@@ -507,47 +516,45 @@ 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
|
||||
if ls.hidden {
|
||||
switch {
|
||||
case ls.hidden:
|
||||
renderBuf = ls.state.RedrawScreenOnly()
|
||||
} else {
|
||||
default:
|
||||
renderBuf = ls.state.Redraw()
|
||||
}
|
||||
if renderBuf == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if ls.oldBuffer != nil {
|
||||
ls.oldBuffer.Destroy()
|
||||
ls.oldBuffer = nil
|
||||
}
|
||||
if ls.oldPool != nil {
|
||||
ls.oldPool.Destroy()
|
||||
ls.oldPool = nil
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
ls.slotBusy[slot] = true
|
||||
|
||||
logicalW, logicalH := ls.state.LogicalSize()
|
||||
if logicalW == 0 || logicalH == 0 {
|
||||
@@ -566,7 +573,7 @@ func (p *Picker) redrawSurface(ls *LayerSurface) {
|
||||
}
|
||||
_ = ls.wlSurface.SetBufferScale(bufferScale)
|
||||
}
|
||||
_ = ls.wlSurface.Attach(wlBuffer, 0, 0)
|
||||
_ = ls.wlSurface.Attach(ls.wlBuffers[slot], 0, 0)
|
||||
_ = ls.wlSurface.Damage(0, 0, int32(logicalW), int32(logicalH))
|
||||
_ = ls.wlSurface.Commit()
|
||||
|
||||
@@ -634,7 +641,7 @@ func (p *Picker) setupPointerHandlers() {
|
||||
}
|
||||
|
||||
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
|
||||
p.redrawSurface(p.activeSurface)
|
||||
p.activeSurface.needsRedraw = true
|
||||
})
|
||||
|
||||
p.pointer.SetLeaveHandler(func(e client.PointerLeaveEvent) {
|
||||
@@ -655,7 +662,7 @@ func (p *Picker) setupPointerHandlers() {
|
||||
return
|
||||
}
|
||||
p.activeSurface.state.OnPointerMotion(e.SurfaceX, e.SurfaceY)
|
||||
p.redrawSurface(p.activeSurface)
|
||||
p.activeSurface.needsRedraw = true
|
||||
})
|
||||
|
||||
p.pointer.SetButtonHandler(func(e client.PointerButtonEvent) {
|
||||
@@ -679,17 +686,13 @@ func (p *Picker) cleanup() {
|
||||
if ls.scopyBuffer != nil {
|
||||
ls.scopyBuffer.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()
|
||||
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.viewport != nil {
|
||||
ls.viewport.Destroy()
|
||||
|
||||
@@ -274,6 +274,12 @@ 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%
|
||||
bind = SUPER CTRL, F, resizeactive, exact 100% 100%
|
||||
|
||||
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
||||
bindmd = SUPER, mouse:272, Move window, movewindow
|
||||
|
||||
@@ -94,6 +94,7 @@ windowrule = tile on, match:class ^(gnome-control-center)$
|
||||
windowrule = tile on, match:class ^(pavucontrol)$
|
||||
windowrule = tile on, match:class ^(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,6 +224,7 @@ 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,11 +242,7 @@ func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMap
|
||||
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
|
||||
}
|
||||
|
||||
if a.packageInstalled("dms-shell-bin") {
|
||||
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
|
||||
}
|
||||
|
||||
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
|
||||
return PackageMapping{Name: "dms-shell", Repository: RepoTypeSystem}
|
||||
}
|
||||
|
||||
func (a *ArchDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||
@@ -328,6 +324,13 @@ func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []d
|
||||
|
||||
systemPkgs, aurPkgs, manualPkgs, variantMap := a.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
||||
|
||||
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{
|
||||
@@ -445,6 +448,37 @@ 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
|
||||
@@ -453,6 +487,9 @@ 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{
|
||||
@@ -540,7 +577,7 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
|
||||
var dmsShell []string
|
||||
|
||||
for _, pkg := range packages {
|
||||
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
||||
if pkg == "dms-shell-git" {
|
||||
dmsShell = append(dmsShell, pkg)
|
||||
} else {
|
||||
isDep := false
|
||||
@@ -621,7 +658,7 @@ func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context,
|
||||
}
|
||||
}
|
||||
|
||||
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
||||
if pkg == "dms-shell-git" {
|
||||
srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
|
||||
depsToRemove := []string{
|
||||
"depends = quickshell",
|
||||
@@ -644,15 +681,7 @@ 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),
|
||||
@@ -739,42 +768,9 @@ 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
|
||||
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
|
||||
}
|
||||
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,6 +31,7 @@ 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"
|
||||
@@ -72,6 +73,7 @@ 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
|
||||
|
||||
@@ -394,6 +396,18 @@ 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 {
|
||||
@@ -1325,6 +1339,9 @@ func cleanupManagers() {
|
||||
if themeModeManager != nil {
|
||||
themeModeManager.Close()
|
||||
}
|
||||
if trayRecoveryManager != nil {
|
||||
trayRecoveryManager.Close()
|
||||
}
|
||||
if wlContext != nil {
|
||||
wlContext.Close()
|
||||
}
|
||||
@@ -1610,6 +1627,18 @@ func Start(printDocs bool) error {
|
||||
}()
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-loginctlReady
|
||||
if loginctlManager == nil {
|
||||
return
|
||||
}
|
||||
if err := InitializeTrayRecoveryManager(); err != nil {
|
||||
log.Warnf("TrayRecovery manager unavailable: %v", err)
|
||||
} else {
|
||||
trayRecoveryManager.WatchLoginctl(loginctlManager)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
geoClient := geolocation.NewClient()
|
||||
geoClientInstance = geoClient
|
||||
|
||||
93
core/internal/server/trayrecovery/manager.go
Normal file
93
core/internal/server/trayrecovery/manager.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package trayrecovery
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
const resumeDelay = 3 * time.Second
|
||||
|
||||
type Manager struct {
|
||||
conn *dbus.Conn
|
||||
stopChan chan struct{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewManager() (*Manager, error) {
|
||||
conn, err := dbus.ConnectSessionBus()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to session bus: %w", err)
|
||||
}
|
||||
|
||||
m := &Manager{
|
||||
conn: conn,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
// Run a startup scan after a delay — covers the case where the process
|
||||
// was killed during suspend and restarted by systemd (Type=dbus).
|
||||
// The fresh process never sees the PrepareForSleep true→false transition,
|
||||
// so the loginctl watcher alone is not enough.
|
||||
go m.scheduleRecovery()
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// WatchLoginctl subscribes to loginctl session state changes and triggers
|
||||
// tray recovery after resume from suspend (PrepareForSleep false transition).
|
||||
// This handles the case where the process survives suspend.
|
||||
func (m *Manager) WatchLoginctl(lm *loginctl.Manager) {
|
||||
ch := lm.Subscribe("tray-recovery")
|
||||
m.wg.Add(1)
|
||||
go func() {
|
||||
defer m.wg.Done()
|
||||
defer lm.Unsubscribe("tray-recovery")
|
||||
|
||||
wasSleeping := false
|
||||
for {
|
||||
select {
|
||||
case <-m.stopChan:
|
||||
return
|
||||
case state, ok := <-ch:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if state.PreparingForSleep {
|
||||
wasSleeping = true
|
||||
continue
|
||||
}
|
||||
if wasSleeping {
|
||||
wasSleeping = false
|
||||
go m.scheduleRecovery()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (m *Manager) scheduleRecovery() {
|
||||
select {
|
||||
case <-time.After(resumeDelay):
|
||||
m.recoverTrayItems()
|
||||
case <-m.stopChan:
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Close() {
|
||||
select {
|
||||
case <-m.stopChan:
|
||||
return
|
||||
default:
|
||||
close(m.stopChan)
|
||||
}
|
||||
m.wg.Wait()
|
||||
if m.conn != nil {
|
||||
m.conn.Close()
|
||||
}
|
||||
log.Info("TrayRecovery manager closed")
|
||||
}
|
||||
262
core/internal/server/trayrecovery/recovery.go
Normal file
262
core/internal/server/trayrecovery/recovery.go
Normal file
@@ -0,0 +1,262 @@
|
||||
package trayrecovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
sniWatcherDest = "org.kde.StatusNotifierWatcher"
|
||||
sniWatcherPath = "/StatusNotifierWatcher"
|
||||
sniWatcherIface = "org.kde.StatusNotifierWatcher"
|
||||
sniItemIface = "org.kde.StatusNotifierItem"
|
||||
dbusIface = "org.freedesktop.DBus"
|
||||
propsIface = "org.freedesktop.DBus.Properties"
|
||||
probeTimeout = 300 * time.Millisecond
|
||||
connProbeTimeout = 150 * time.Millisecond
|
||||
batchSize = 30
|
||||
)
|
||||
|
||||
var excludedPrefixes = []string{
|
||||
"org.freedesktop.",
|
||||
"org.gnome.",
|
||||
"org.kde.StatusNotifier",
|
||||
"com.canonical.AppMenu",
|
||||
"org.mpris.",
|
||||
"org.pipewire.",
|
||||
"org.pulseaudio",
|
||||
"fi.epitaph",
|
||||
"quickshell",
|
||||
"org.kde.quickshell",
|
||||
}
|
||||
|
||||
func (m *Manager) recoverTrayItems() {
|
||||
registeredItems := m.getRegisteredItems()
|
||||
allNames := m.getDBusNames()
|
||||
if allNames == nil {
|
||||
return
|
||||
}
|
||||
|
||||
registeredConnIDs := m.buildRegisteredConnIDs(registeredItems)
|
||||
|
||||
count := len(registeredItems)
|
||||
log.Infof("TrayRecoveryService: scanning DBus for unregistered SNI items (%d already registered)...", count)
|
||||
|
||||
m.scanWellKnownNames(allNames, registeredItems, registeredConnIDs)
|
||||
m.scanConnectionIDs(allNames, registeredItems, registeredConnIDs)
|
||||
}
|
||||
|
||||
func (m *Manager) getRegisteredItems() []string {
|
||||
obj := m.conn.Object(sniWatcherDest, sniWatcherPath)
|
||||
variant, err := obj.GetProperty(sniWatcherIface + ".RegisteredStatusNotifierItems")
|
||||
if err != nil {
|
||||
log.Warnf("TrayRecoveryService: failed to get registered items: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
switch v := variant.Value().(type) {
|
||||
case []string:
|
||||
return v
|
||||
case []any:
|
||||
items := make([]string, 0, len(v))
|
||||
for _, elem := range v {
|
||||
if s, ok := elem.(string); ok {
|
||||
items = append(items, s)
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) getDBusNames() []string {
|
||||
var names []string
|
||||
err := m.conn.BusObject().Call(dbusIface+".ListNames", 0).Store(&names)
|
||||
if err != nil {
|
||||
log.Warnf("TrayRecoveryService: failed to list bus names: %v", err)
|
||||
return nil
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func (m *Manager) getNameOwner(name string) string {
|
||||
var owner string
|
||||
err := m.conn.BusObject().Call(dbusIface+".GetNameOwner", 0, name).Store(&owner)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return owner
|
||||
}
|
||||
|
||||
// buildRegisteredConnIDs resolves every registered SNI item (well-known name
|
||||
// or :1.xxx connection ID) to a canonical connection ID. This prevents
|
||||
// duplicates in both directions.
|
||||
func (m *Manager) buildRegisteredConnIDs(registeredItems []string) map[string]bool {
|
||||
connIDs := make(map[string]bool, len(registeredItems))
|
||||
for _, item := range registeredItems {
|
||||
name := extractName(item)
|
||||
if strings.HasPrefix(name, ":1.") {
|
||||
connIDs[name] = true
|
||||
} else {
|
||||
owner := m.getNameOwner(name)
|
||||
if owner != "" {
|
||||
connIDs[owner] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return connIDs
|
||||
}
|
||||
|
||||
// scanWellKnownNames probes well-known names (e.g. DinoX, nm-applet) for
|
||||
// unregistered SNI items and re-registers them.
|
||||
func (m *Manager) scanWellKnownNames(allNames []string, registeredItems []string, registeredConnIDs map[string]bool) {
|
||||
registeredRaw := strings.Join(registeredItems, "\n")
|
||||
|
||||
for _, name := range allNames {
|
||||
if strings.HasPrefix(name, ":") {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(registeredRaw, name) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip if this name's connection ID is already in the registered set
|
||||
// (handles the case where the app registered via connection ID instead)
|
||||
connForName := m.getNameOwner(name)
|
||||
if connForName != "" && registeredConnIDs[connForName] {
|
||||
continue
|
||||
}
|
||||
|
||||
if isExcludedName(name) {
|
||||
continue
|
||||
}
|
||||
|
||||
short := shortName(name)
|
||||
objectPaths := []string{
|
||||
"/StatusNotifierItem",
|
||||
"/org/ayatana/NotificationItem/" + short,
|
||||
}
|
||||
|
||||
for _, objPath := range objectPaths {
|
||||
if m.probeSNI(name, objPath, probeTimeout) {
|
||||
m.registerSNI(name)
|
||||
// Update set so the connection-ID section won't double-register this app
|
||||
if connForName != "" {
|
||||
registeredConnIDs[connForName] = true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// scanConnectionIDs probes all :1.xxx connections in parallel for unregistered
|
||||
// SNI items (e.g. Vesktop, Electron apps). Most non-SNI connections return an
|
||||
// error instantly, so this is fast.
|
||||
func (m *Manager) scanConnectionIDs(allNames []string, registeredItems []string, registeredConnIDs map[string]bool) {
|
||||
registeredRaw := strings.Join(registeredItems, "\n")
|
||||
registeredLower := strings.ToLower(registeredRaw)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
sem := make(chan struct{}, batchSize)
|
||||
|
||||
for _, name := range allNames {
|
||||
if !strings.HasPrefix(name, ":1.") {
|
||||
continue
|
||||
}
|
||||
if registeredConnIDs[name] {
|
||||
continue
|
||||
}
|
||||
|
||||
sem <- struct{}{}
|
||||
wg.Add(1)
|
||||
go func(conn string) {
|
||||
defer wg.Done()
|
||||
defer func() { <-sem }()
|
||||
|
||||
sniID := m.getSNIId(conn, connProbeTimeout)
|
||||
if sniID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip if an item with the same Id is already registered (case-insensitive)
|
||||
if strings.Contains(registeredLower, strings.ToLower(sniID)) {
|
||||
return
|
||||
}
|
||||
|
||||
m.registerSNI(conn)
|
||||
log.Infof("TrayRecovery: re-registered %s (Id: %s)", conn, sniID)
|
||||
}(name)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (m *Manager) probeSNI(dest, path string, timeout time.Duration) bool {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
obj := m.conn.Object(dest, dbus.ObjectPath(path))
|
||||
var props map[string]dbus.Variant
|
||||
err := obj.CallWithContext(ctx, propsIface+".GetAll", 0, sniItemIface).Store(&props)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
_, hasID := props["Id"]
|
||||
return hasID
|
||||
}
|
||||
|
||||
func (m *Manager) getSNIId(dest string, timeout time.Duration) string {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
obj := m.conn.Object(dest, "/StatusNotifierItem")
|
||||
var variant dbus.Variant
|
||||
err := obj.CallWithContext(ctx, propsIface+".Get", 0, sniItemIface, "Id").Store(&variant)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
id, _ := variant.Value().(string)
|
||||
return id
|
||||
}
|
||||
|
||||
func (m *Manager) registerSNI(name string) {
|
||||
obj := m.conn.Object(sniWatcherDest, sniWatcherPath)
|
||||
call := obj.Call(sniWatcherIface+".RegisterStatusNotifierItem", 0, name)
|
||||
if call.Err != nil {
|
||||
log.Warnf("TrayRecovery: failed to register %s: %v", name, call.Err)
|
||||
return
|
||||
}
|
||||
log.Infof("TrayRecovery: re-registered %s", name)
|
||||
}
|
||||
|
||||
func extractName(item string) string {
|
||||
if idx := strings.IndexByte(item, '/'); idx != -1 {
|
||||
return item[:idx]
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
func shortName(name string) string {
|
||||
parts := strings.Split(name, ".")
|
||||
if len(parts) > 0 {
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func isExcludedName(name string) bool {
|
||||
for _, prefix := range excludedPrefixes {
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -139,7 +139,7 @@ func dmsPackageName(distroID string, dependencies []deps.Dependency) string {
|
||||
if isGit {
|
||||
return "dms-shell-git"
|
||||
}
|
||||
return "dms-shell-bin"
|
||||
return "dms-shell"
|
||||
case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE:
|
||||
if isGit {
|
||||
return "dms-git"
|
||||
|
||||
179
quickshell/Common/AnimVariants.qml
Normal file
179
quickshell/Common/AnimVariants.qml
Normal file
@@ -0,0 +1,179 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
|
||||
// AnimVariants — Central tuning for animation and Motion Effects variants
|
||||
// (Material/Fluent/Dynamic) (Standard/Directional/Depth)
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property list<real> variantEnterCurve: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return Anims.expressiveDefaultSpatial;
|
||||
switch (SettingsData.animationVariant) {
|
||||
case 1:
|
||||
return Anims.standardDecel;
|
||||
case 2:
|
||||
return Anims.expressiveFastSpatial;
|
||||
default:
|
||||
return Anims.expressiveDefaultSpatial;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property list<real> variantExitCurve: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return Anims.emphasized;
|
||||
switch (SettingsData.animationVariant) {
|
||||
case 1:
|
||||
return Anims.standard;
|
||||
case 2:
|
||||
return Anims.emphasized;
|
||||
default:
|
||||
return Anims.emphasized;
|
||||
}
|
||||
}
|
||||
|
||||
// Modal-specific entry curve
|
||||
readonly property list<real> variantModalEnterCurve: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return Anims.expressiveDefaultSpatial;
|
||||
if (isDirectionalEffect) {
|
||||
if (SettingsData.animationVariant === 1)
|
||||
return Anims.standardDecel;
|
||||
if (SettingsData.animationVariant === 2)
|
||||
return Anims.expressiveFastSpatial;
|
||||
}
|
||||
return variantEnterCurve;
|
||||
}
|
||||
|
||||
readonly property list<real> variantModalExitCurve: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return Anims.emphasized;
|
||||
if (isDirectionalEffect) {
|
||||
if (SettingsData.animationVariant === 1)
|
||||
return Anims.emphasizedAccel;
|
||||
if (SettingsData.animationVariant === 2)
|
||||
return Anims.emphasizedAccel;
|
||||
}
|
||||
return variantExitCurve;
|
||||
}
|
||||
|
||||
// Popout-specific entry curve
|
||||
readonly property list<real> variantPopoutEnterCurve: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return Anims.expressiveDefaultSpatial;
|
||||
if (isDirectionalEffect) {
|
||||
if (SettingsData.animationVariant === 1)
|
||||
return Anims.standardDecel;
|
||||
if (SettingsData.animationVariant === 2)
|
||||
return Anims.expressiveFastSpatial;
|
||||
return Anims.standardDecel;
|
||||
}
|
||||
return variantEnterCurve;
|
||||
}
|
||||
|
||||
readonly property list<real> variantPopoutExitCurve: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return Anims.emphasized;
|
||||
if (isDirectionalEffect) {
|
||||
if (SettingsData.animationVariant === 1)
|
||||
return Anims.emphasizedAccel;
|
||||
if (SettingsData.animationVariant === 2)
|
||||
return Anims.emphasizedAccel;
|
||||
}
|
||||
return variantExitCurve;
|
||||
}
|
||||
|
||||
readonly property real variantEnterDurationFactor: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return 1.0;
|
||||
switch (SettingsData.animationVariant) {
|
||||
case 1:
|
||||
return 0.9;
|
||||
case 2:
|
||||
return 1.08;
|
||||
default:
|
||||
return 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real variantExitDurationFactor: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return 1.0;
|
||||
switch (SettingsData.animationVariant) {
|
||||
case 1:
|
||||
return 0.85;
|
||||
case 2:
|
||||
return 0.92;
|
||||
default:
|
||||
return 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
// Fluent: opacity at ~55% of duration; Material/Dynamic: 1:1 with position
|
||||
readonly property real variantOpacityDurationScale: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return 1.0;
|
||||
return SettingsData.animationVariant === 1 ? 0.55 : 1.0;
|
||||
}
|
||||
|
||||
function variantDuration(baseDuration, entering) {
|
||||
const factor = entering ? variantEnterDurationFactor : variantExitDurationFactor;
|
||||
return Math.max(0, Math.round(baseDuration * factor));
|
||||
}
|
||||
|
||||
function variantExitCleanupPadding() {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return 50;
|
||||
switch (SettingsData.motionEffect) {
|
||||
case 1:
|
||||
return 8;
|
||||
case 2:
|
||||
return 24;
|
||||
default:
|
||||
return 50;
|
||||
}
|
||||
}
|
||||
|
||||
function variantCloseInterval(baseDuration) {
|
||||
return variantDuration(baseDuration, false) + variantExitCleanupPadding();
|
||||
}
|
||||
|
||||
readonly property bool isDirectionalEffect: isConnectedEffect
|
||||
|| (typeof SettingsData !== "undefined" && SettingsData.motionEffect === 1)
|
||||
readonly property bool isDepthEffect: typeof SettingsData !== "undefined" && SettingsData.motionEffect === 2
|
||||
readonly property bool isConnectedEffect: typeof SettingsData !== "undefined"
|
||||
&& SettingsData.frameEnabled
|
||||
&& SettingsData.motionEffect === 1
|
||||
&& SettingsData.directionalAnimationMode === 3
|
||||
|
||||
readonly property real effectScaleCollapsed: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return 0.96;
|
||||
switch (SettingsData.motionEffect) {
|
||||
case 1:
|
||||
return 1.0;
|
||||
case 2:
|
||||
return 0.88;
|
||||
default:
|
||||
return 0.96;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real effectAnimOffset: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return 16;
|
||||
switch (SettingsData.motionEffect) {
|
||||
case 1:
|
||||
return 144;
|
||||
case 2:
|
||||
return 56;
|
||||
default:
|
||||
return 16;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,4 +22,9 @@ Singleton {
|
||||
readonly property var standard: [0.20, 0.00, 0.00, 1.00, 1.00, 1.00]
|
||||
readonly property var standardDecel: [0.00, 0.00, 0.00, 1.00, 1.00, 1.00]
|
||||
readonly property var standardAccel: [0.30, 0.00, 1.00, 1.00, 1.00, 1.00]
|
||||
|
||||
// Used by AnimVariants for variant/effect logic
|
||||
readonly property var expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
|
||||
readonly property var expressiveFastSpatial: [0.34, 1.5, 0.2, 1.0, 1.0, 1.0]
|
||||
readonly property var expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
|
||||
}
|
||||
|
||||
204
quickshell/Common/ConnectedModeState.qml
Normal file
204
quickshell/Common/ConnectedModeState.qml
Normal file
@@ -0,0 +1,204 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property var emptyDockState: ({
|
||||
"reveal": false,
|
||||
"barSide": "bottom",
|
||||
"bodyX": 0,
|
||||
"bodyY": 0,
|
||||
"bodyW": 0,
|
||||
"bodyH": 0,
|
||||
"slideX": 0,
|
||||
"slideY": 0
|
||||
})
|
||||
|
||||
// Popout state (updated by DankPopout when connectedFrameModeActive)
|
||||
property string popoutOwnerId: ""
|
||||
property bool popoutVisible: false
|
||||
property string popoutBarSide: "top"
|
||||
property real popoutBodyX: 0
|
||||
property real popoutBodyY: 0
|
||||
property real popoutBodyW: 0
|
||||
property real popoutBodyH: 0
|
||||
property real popoutAnimX: 0
|
||||
property real popoutAnimY: 0
|
||||
property string popoutScreen: ""
|
||||
|
||||
// Dock state (updated by Dock when connectedFrameModeActive), keyed by screen.name
|
||||
property var dockStates: ({})
|
||||
|
||||
// Dock slide offsets — hot-path updates separated from full geometry state
|
||||
property var dockSlides: ({})
|
||||
|
||||
function hasPopoutOwner(claimId) {
|
||||
return !!claimId && popoutOwnerId === claimId;
|
||||
}
|
||||
|
||||
function claimPopout(claimId, state) {
|
||||
if (!claimId)
|
||||
return false;
|
||||
|
||||
popoutOwnerId = claimId;
|
||||
return updatePopout(claimId, state);
|
||||
}
|
||||
|
||||
function updatePopout(claimId, state) {
|
||||
if (!hasPopoutOwner(claimId) || !state)
|
||||
return false;
|
||||
|
||||
if (state.visible !== undefined)
|
||||
popoutVisible = !!state.visible;
|
||||
if (state.barSide !== undefined)
|
||||
popoutBarSide = state.barSide || "top";
|
||||
if (state.bodyX !== undefined)
|
||||
popoutBodyX = Number(state.bodyX);
|
||||
if (state.bodyY !== undefined)
|
||||
popoutBodyY = Number(state.bodyY);
|
||||
if (state.bodyW !== undefined)
|
||||
popoutBodyW = Number(state.bodyW);
|
||||
if (state.bodyH !== undefined)
|
||||
popoutBodyH = Number(state.bodyH);
|
||||
if (state.animX !== undefined)
|
||||
popoutAnimX = Number(state.animX);
|
||||
if (state.animY !== undefined)
|
||||
popoutAnimY = Number(state.animY);
|
||||
if (state.screen !== undefined)
|
||||
popoutScreen = state.screen || "";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function releasePopout(claimId) {
|
||||
if (!hasPopoutOwner(claimId))
|
||||
return false;
|
||||
|
||||
popoutOwnerId = "";
|
||||
popoutVisible = false;
|
||||
popoutBarSide = "top";
|
||||
popoutBodyX = 0;
|
||||
popoutBodyY = 0;
|
||||
popoutBodyW = 0;
|
||||
popoutBodyH = 0;
|
||||
popoutAnimX = 0;
|
||||
popoutAnimY = 0;
|
||||
popoutScreen = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
function _cloneDockStates() {
|
||||
const next = {};
|
||||
for (const screenName in dockStates)
|
||||
next[screenName] = dockStates[screenName];
|
||||
return next;
|
||||
}
|
||||
|
||||
function _normalizeDockState(state) {
|
||||
return {
|
||||
"reveal": !!(state && state.reveal),
|
||||
"barSide": state && state.barSide ? state.barSide : "bottom",
|
||||
"bodyX": Number(state && state.bodyX !== undefined ? state.bodyX : 0),
|
||||
"bodyY": Number(state && state.bodyY !== undefined ? state.bodyY : 0),
|
||||
"bodyW": Number(state && state.bodyW !== undefined ? state.bodyW : 0),
|
||||
"bodyH": Number(state && state.bodyH !== undefined ? state.bodyH : 0),
|
||||
"slideX": Number(state && state.slideX !== undefined ? state.slideX : 0),
|
||||
"slideY": Number(state && state.slideY !== undefined ? state.slideY : 0)
|
||||
};
|
||||
}
|
||||
|
||||
function setDockState(screenName, state) {
|
||||
if (!screenName || !state)
|
||||
return false;
|
||||
|
||||
const next = _cloneDockStates();
|
||||
next[screenName] = _normalizeDockState(state);
|
||||
dockStates = next;
|
||||
return true;
|
||||
}
|
||||
|
||||
function clearDockState(screenName) {
|
||||
if (!screenName || !dockStates[screenName])
|
||||
return false;
|
||||
|
||||
const next = _cloneDockStates();
|
||||
delete next[screenName];
|
||||
dockStates = next;
|
||||
|
||||
// Also clear corresponding slide
|
||||
if (dockSlides[screenName]) {
|
||||
const nextSlides = {};
|
||||
for (const k in dockSlides)
|
||||
nextSlides[k] = dockSlides[k];
|
||||
delete nextSlides[screenName];
|
||||
dockSlides = nextSlides;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function setDockSlide(screenName, x, y) {
|
||||
if (!screenName)
|
||||
return false;
|
||||
const next = {};
|
||||
for (const k in dockSlides)
|
||||
next[k] = dockSlides[k];
|
||||
next[screenName] = { "x": Number(x), "y": Number(y) };
|
||||
dockSlides = next;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ─── Notification state (per screen, updated by NotificationSurface) ──────
|
||||
|
||||
readonly property var emptyNotificationState: ({
|
||||
"visible": false,
|
||||
"barSide": "top",
|
||||
"bodyX": 0,
|
||||
"bodyY": 0,
|
||||
"bodyW": 0,
|
||||
"bodyH": 0
|
||||
})
|
||||
|
||||
property var notificationStates: ({})
|
||||
|
||||
function _cloneNotificationStates() {
|
||||
const next = {};
|
||||
for (const screenName in notificationStates)
|
||||
next[screenName] = notificationStates[screenName];
|
||||
return next;
|
||||
}
|
||||
|
||||
function _normalizeNotificationState(state) {
|
||||
return {
|
||||
"visible": !!(state && state.visible),
|
||||
"barSide": state && state.barSide ? state.barSide : "top",
|
||||
"bodyX": Number(state && state.bodyX !== undefined ? state.bodyX : 0),
|
||||
"bodyY": Number(state && state.bodyY !== undefined ? state.bodyY : 0),
|
||||
"bodyW": Number(state && state.bodyW !== undefined ? state.bodyW : 0),
|
||||
"bodyH": Number(state && state.bodyH !== undefined ? state.bodyH : 0)
|
||||
};
|
||||
}
|
||||
|
||||
function setNotificationState(screenName, state) {
|
||||
if (!screenName || !state)
|
||||
return false;
|
||||
|
||||
const next = _cloneNotificationStates();
|
||||
next[screenName] = _normalizeNotificationState(state);
|
||||
notificationStates = next;
|
||||
return true;
|
||||
}
|
||||
|
||||
function clearNotificationState(screenName) {
|
||||
if (!screenName || !notificationStates[screenName])
|
||||
return false;
|
||||
|
||||
const next = _cloneNotificationStates();
|
||||
delete next[screenName];
|
||||
notificationStates = next;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,13 @@ Item {
|
||||
|
||||
property color targetColor: "white"
|
||||
property real targetRadius: Theme.cornerRadius
|
||||
property real topLeftRadius: targetRadius
|
||||
property real topRightRadius: targetRadius
|
||||
property real bottomLeftRadius: targetRadius
|
||||
property real bottomRightRadius: targetRadius
|
||||
property color borderColor: "transparent"
|
||||
property real borderWidth: 0
|
||||
property bool useCustomSource: false
|
||||
|
||||
property bool shadowEnabled: Theme.elevationEnabled
|
||||
property real shadowBlurPx: level && level.blurPx !== undefined ? level.blurPx : 0
|
||||
@@ -46,7 +51,11 @@ Item {
|
||||
Rectangle {
|
||||
id: sourceRect
|
||||
anchors.fill: parent
|
||||
radius: root.targetRadius
|
||||
visible: !root.useCustomSource
|
||||
topLeftRadius: root.topLeftRadius
|
||||
topRightRadius: root.topRightRadius
|
||||
bottomLeftRadius: root.bottomLeftRadius
|
||||
bottomRightRadius: root.bottomRightRadius
|
||||
color: root.targetColor
|
||||
border.color: root.borderColor
|
||||
border.width: root.borderWidth
|
||||
|
||||
@@ -124,6 +124,8 @@ 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"
|
||||
|
||||
@@ -37,6 +37,18 @@ Singleton {
|
||||
Custom
|
||||
}
|
||||
|
||||
enum AnimationVariant {
|
||||
Material,
|
||||
Fluent,
|
||||
Dynamic
|
||||
}
|
||||
|
||||
enum AnimationEffect {
|
||||
Standard, // 0 — M3: scale-in, rises from below
|
||||
Directional, // 1 — pure large slide, no scale
|
||||
Depth // 2 — medium slide with deep depth scale pop
|
||||
}
|
||||
|
||||
enum SuspendBehavior {
|
||||
Suspend,
|
||||
Hibernate,
|
||||
@@ -168,6 +180,12 @@ Singleton {
|
||||
property int modalCustomAnimationDuration: 150
|
||||
property bool enableRippleEffects: true
|
||||
onEnableRippleEffectsChanged: saveSettings()
|
||||
property int animationVariant: SettingsData.AnimationVariant.Material
|
||||
onAnimationVariantChanged: saveSettings()
|
||||
property int motionEffect: SettingsData.AnimationEffect.Standard
|
||||
onMotionEffectChanged: saveSettings()
|
||||
property int directionalAnimationMode: 0
|
||||
onDirectionalAnimationModeChanged: saveSettings()
|
||||
property bool m3ElevationEnabled: true
|
||||
onM3ElevationEnabledChanged: saveSettings()
|
||||
property int m3ElevationIntensity: 12
|
||||
@@ -186,6 +204,7 @@ Singleton {
|
||||
onPopoutElevationEnabledChanged: saveSettings()
|
||||
property bool barElevationEnabled: true
|
||||
onBarElevationEnabledChanged: saveSettings()
|
||||
|
||||
property bool blurEnabled: false
|
||||
onBlurEnabledChanged: saveSettings()
|
||||
property string blurBorderColor: "outline"
|
||||
@@ -198,6 +217,50 @@ 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()
|
||||
property int previousDirectionalMode: 1
|
||||
onPreviousDirectionalModeChanged: saveSettings()
|
||||
property var connectedFrameBarStyleBackups: ({})
|
||||
onConnectedFrameBarStyleBackupsChanged: saveSettings()
|
||||
readonly property bool connectedFrameModeActive: frameEnabled
|
||||
&& motionEffect === SettingsData.AnimationEffect.Directional
|
||||
&& directionalAnimationMode === 3
|
||||
onConnectedFrameModeActiveChanged: {
|
||||
if (_loading)
|
||||
return;
|
||||
if (connectedFrameModeActive) {
|
||||
_captureConnectedFrameBarStyleBackups(barConfigs, true);
|
||||
_enforceConnectedModeBarStyleReset();
|
||||
} else {
|
||||
_restoreConnectedFrameBarStyleBackups();
|
||||
}
|
||||
}
|
||||
|
||||
readonly property color effectiveFrameColor: {
|
||||
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,6 +364,7 @@ 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
|
||||
@@ -434,6 +498,7 @@ 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
|
||||
@@ -1283,6 +1348,7 @@ Singleton {
|
||||
_loading = false;
|
||||
}
|
||||
loadPluginSettings();
|
||||
Qt.callLater(() => _reconcileConnectedFrameBarStyles());
|
||||
}
|
||||
|
||||
property var _pendingMigration: null
|
||||
@@ -1396,6 +1462,149 @@ Singleton {
|
||||
pluginSettingsFile.setText(JSON.stringify(pluginSettings, null, 2));
|
||||
}
|
||||
|
||||
function _connectedFrameBarStyleSnapshot(config) {
|
||||
return {
|
||||
"shadowIntensity": config?.shadowIntensity ?? 0,
|
||||
"squareCorners": config?.squareCorners ?? false,
|
||||
"gothCornersEnabled": config?.gothCornersEnabled ?? false,
|
||||
"borderEnabled": config?.borderEnabled ?? false
|
||||
};
|
||||
}
|
||||
|
||||
function _hasConnectedFrameBarStyleBackups() {
|
||||
return connectedFrameBarStyleBackups && Object.keys(connectedFrameBarStyleBackups).length > 0;
|
||||
}
|
||||
|
||||
function _captureConnectedFrameBarStyleBackups(configs, overwriteExisting) {
|
||||
if (!Array.isArray(configs))
|
||||
return;
|
||||
|
||||
const nextBackups = JSON.parse(JSON.stringify(connectedFrameBarStyleBackups || {}));
|
||||
const validIds = {};
|
||||
let changed = false;
|
||||
|
||||
for (let i = 0; i < configs.length; i++) {
|
||||
const config = configs[i];
|
||||
if (!config?.id)
|
||||
continue;
|
||||
validIds[config.id] = true;
|
||||
|
||||
if (!overwriteExisting && nextBackups[config.id] !== undefined)
|
||||
continue;
|
||||
|
||||
const snapshot = _connectedFrameBarStyleSnapshot(config);
|
||||
if (JSON.stringify(nextBackups[config.id]) !== JSON.stringify(snapshot)) {
|
||||
nextBackups[config.id] = snapshot;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (overwriteExisting) {
|
||||
for (const barId in nextBackups) {
|
||||
if (validIds[barId])
|
||||
continue;
|
||||
delete nextBackups[barId];
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed)
|
||||
connectedFrameBarStyleBackups = nextBackups;
|
||||
}
|
||||
|
||||
function _restoreConnectedFrameBarStyleBackups() {
|
||||
if (!_hasConnectedFrameBarStyleBackups())
|
||||
return;
|
||||
|
||||
const backups = connectedFrameBarStyleBackups || {};
|
||||
const configs = JSON.parse(JSON.stringify(barConfigs));
|
||||
let changed = false;
|
||||
|
||||
for (let i = 0; i < configs.length; i++) {
|
||||
const backup = backups[configs[i].id];
|
||||
if (!backup)
|
||||
continue;
|
||||
for (const key in backup) {
|
||||
if (configs[i][key] === backup[key])
|
||||
continue;
|
||||
configs[i][key] = backup[key];
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed)
|
||||
barConfigs = configs;
|
||||
connectedFrameBarStyleBackups = ({});
|
||||
if (changed)
|
||||
updateBarConfigs();
|
||||
}
|
||||
|
||||
function _reconcileConnectedFrameBarStyles() {
|
||||
if (connectedFrameModeActive) {
|
||||
if (!_hasConnectedFrameBarStyleBackups())
|
||||
_captureConnectedFrameBarStyleBackups(barConfigs, true);
|
||||
_enforceConnectedModeBarStyleReset();
|
||||
return;
|
||||
}
|
||||
_restoreConnectedFrameBarStyleBackups();
|
||||
}
|
||||
|
||||
function _sanitizeBarConfigForConnectedFrame(config) {
|
||||
if (!connectedFrameModeActive || !config)
|
||||
return config;
|
||||
|
||||
let changed = false;
|
||||
const sanitized = Object.assign({}, config);
|
||||
|
||||
if ((sanitized.shadowIntensity ?? 0) !== 0) {
|
||||
sanitized.shadowIntensity = 0;
|
||||
changed = true;
|
||||
}
|
||||
if (sanitized.squareCorners ?? false) {
|
||||
sanitized.squareCorners = false;
|
||||
changed = true;
|
||||
}
|
||||
if (sanitized.gothCornersEnabled ?? false) {
|
||||
sanitized.gothCornersEnabled = false;
|
||||
changed = true;
|
||||
}
|
||||
if (sanitized.borderEnabled ?? false) {
|
||||
sanitized.borderEnabled = false;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed ? sanitized : config;
|
||||
}
|
||||
|
||||
function _sanitizeBarConfigsForConnectedFrame(configs) {
|
||||
if (!connectedFrameModeActive || !Array.isArray(configs))
|
||||
return {
|
||||
"configs": configs,
|
||||
"changed": false
|
||||
};
|
||||
|
||||
let changed = false;
|
||||
const sanitizedConfigs = configs.map(config => {
|
||||
const sanitized = _sanitizeBarConfigForConnectedFrame(config);
|
||||
if (sanitized !== config)
|
||||
changed = true;
|
||||
return sanitized;
|
||||
});
|
||||
|
||||
return {
|
||||
"configs": changed ? sanitizedConfigs : configs,
|
||||
"changed": changed
|
||||
};
|
||||
}
|
||||
|
||||
function _enforceConnectedModeBarStyleReset() {
|
||||
const result = _sanitizeBarConfigsForConnectedFrame(barConfigs);
|
||||
if (!result.changed)
|
||||
return;
|
||||
barConfigs = result.configs;
|
||||
updateBarConfigs();
|
||||
}
|
||||
|
||||
function detectAvailableIconThemes() {
|
||||
const xdgDataDirs = Quickshell.env("XDG_DATA_DIRS") || "";
|
||||
const localData = Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation));
|
||||
@@ -1543,35 +1752,37 @@ Singleton {
|
||||
const spacing = barSpacing !== undefined ? barSpacing : (defaultBar?.spacing ?? 4);
|
||||
const position = barPosition !== undefined ? barPosition : (defaultBar?.position ?? SettingsData.Position.Top);
|
||||
const rawBottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : (defaultBar?.bottomGap ?? 0)) : (defaultBar?.bottomGap ?? 0);
|
||||
const bottomGap = Math.max(0, rawBottomGap);
|
||||
const isConnected = connectedFrameModeActive;
|
||||
const bottomGap = isConnected ? 0 : Math.max(0, rawBottomGap);
|
||||
|
||||
const useAutoGaps = (barConfig && barConfig.popupGapsAuto !== undefined) ? barConfig.popupGapsAuto : (defaultBar?.popupGapsAuto ?? true);
|
||||
const manualGapValue = (barConfig && barConfig.popupGapsManual !== undefined) ? barConfig.popupGapsManual : (defaultBar?.popupGapsManual ?? 4);
|
||||
const popupGap = useAutoGaps ? Math.max(4, spacing) : manualGapValue;
|
||||
const popupGap = isConnected ? 0 : (useAutoGaps ? Math.max(4, spacing) : manualGapValue);
|
||||
const edgeSpacing = isConnected ? 0 : spacing;
|
||||
|
||||
switch (position) {
|
||||
case SettingsData.Position.Left:
|
||||
return {
|
||||
"x": barThickness + spacing + popupGap,
|
||||
"x": barThickness + edgeSpacing + popupGap,
|
||||
"y": relativeY,
|
||||
"width": widgetWidth
|
||||
};
|
||||
case SettingsData.Position.Right:
|
||||
return {
|
||||
"x": (screen?.width || 0) - (barThickness + spacing + popupGap),
|
||||
"x": (screen?.width || 0) - (barThickness + edgeSpacing + popupGap),
|
||||
"y": relativeY,
|
||||
"width": widgetWidth
|
||||
};
|
||||
case SettingsData.Position.Bottom:
|
||||
return {
|
||||
"x": relativeX,
|
||||
"y": (screen?.height || 0) - (barThickness + spacing + bottomGap + popupGap),
|
||||
"y": (screen?.height || 0) - (barThickness + edgeSpacing + bottomGap + popupGap),
|
||||
"width": widgetWidth
|
||||
};
|
||||
default:
|
||||
return {
|
||||
"x": relativeX,
|
||||
"y": barThickness + spacing + bottomGap + popupGap,
|
||||
"y": barThickness + edgeSpacing + bottomGap + popupGap,
|
||||
"width": widgetWidth
|
||||
};
|
||||
}
|
||||
@@ -1665,7 +1876,9 @@ Singleton {
|
||||
const screenWidth = screen.width;
|
||||
const screenHeight = screen.height;
|
||||
const position = barPosition !== undefined ? barPosition : (defaultBar?.position ?? SettingsData.Position.Top);
|
||||
const bottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : (defaultBar?.bottomGap ?? 0)) : (defaultBar?.bottomGap ?? 0);
|
||||
const isConnected = connectedFrameModeActive;
|
||||
const rawBottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : (defaultBar?.bottomGap ?? 0)) : (defaultBar?.bottomGap ?? 0);
|
||||
const bottomGap = isConnected ? 0 : rawBottomGap;
|
||||
|
||||
let topOffset = 0;
|
||||
let bottomOffset = 0;
|
||||
@@ -1687,7 +1900,7 @@ Singleton {
|
||||
const otherSpacing = other.spacing !== undefined ? other.spacing : (defaultBar?.spacing ?? 4);
|
||||
const otherPadding = other.innerPadding !== undefined ? other.innerPadding : (defaultBar?.innerPadding ?? 4);
|
||||
const otherThickness = Math.max(26 + otherPadding * 0.6, Theme.barHeight - 4 - (8 - otherPadding)) + otherSpacing + wingSize;
|
||||
const otherBottomGap = other.bottomGap !== undefined ? other.bottomGap : (defaultBar?.bottomGap ?? 0);
|
||||
const otherBottomGap = isConnected ? 0 : (other.bottomGap !== undefined ? other.bottomGap : (defaultBar?.bottomGap ?? 0));
|
||||
|
||||
switch (other.position) {
|
||||
case SettingsData.Position.Top:
|
||||
@@ -1778,7 +1991,9 @@ Singleton {
|
||||
function addBarConfig(config) {
|
||||
const configs = JSON.parse(JSON.stringify(barConfigs));
|
||||
configs.push(config);
|
||||
barConfigs = configs;
|
||||
if (connectedFrameModeActive)
|
||||
_captureConnectedFrameBarStyleBackups(configs, false);
|
||||
barConfigs = _sanitizeBarConfigsForConnectedFrame(configs).configs;
|
||||
updateBarConfigs();
|
||||
}
|
||||
|
||||
@@ -1790,7 +2005,7 @@ Singleton {
|
||||
const positionChanged = updates.position !== undefined && configs[index].position !== updates.position;
|
||||
|
||||
Object.assign(configs[index], updates);
|
||||
barConfigs = configs;
|
||||
barConfigs = _sanitizeBarConfigsForConnectedFrame(configs).configs;
|
||||
updateBarConfigs();
|
||||
|
||||
if (positionChanged) {
|
||||
@@ -1844,6 +2059,11 @@ Singleton {
|
||||
return;
|
||||
const configs = barConfigs.filter(cfg => cfg.id !== barId);
|
||||
barConfigs = configs;
|
||||
if (connectedFrameBarStyleBackups?.[barId] !== undefined) {
|
||||
const nextBackups = JSON.parse(JSON.stringify(connectedFrameBarStyleBackups || {}));
|
||||
delete nextBackups[barId];
|
||||
connectedFrameBarStyleBackups = nextBackups;
|
||||
}
|
||||
updateBarConfigs();
|
||||
}
|
||||
|
||||
@@ -1938,6 +2158,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);
|
||||
|
||||
@@ -960,6 +960,40 @@ Singleton {
|
||||
"expressiveEffects": [0.34, 0.8, 0.34, 1, 1, 1]
|
||||
}
|
||||
|
||||
// Delegates to AnimVariants.qml for curves, timing, scale, and offsets.
|
||||
readonly property list<real> variantEnterCurve: AnimVariants.variantEnterCurve
|
||||
readonly property list<real> variantExitCurve: AnimVariants.variantExitCurve
|
||||
readonly property list<real> variantModalEnterCurve: AnimVariants.variantModalEnterCurve
|
||||
readonly property list<real> variantModalExitCurve: AnimVariants.variantModalExitCurve
|
||||
readonly property list<real> variantPopoutEnterCurve: AnimVariants.variantPopoutEnterCurve
|
||||
readonly property list<real> variantPopoutExitCurve: AnimVariants.variantPopoutExitCurve
|
||||
readonly property real variantEnterDurationFactor: AnimVariants.variantEnterDurationFactor
|
||||
readonly property real variantExitDurationFactor: AnimVariants.variantExitDurationFactor
|
||||
readonly property real variantOpacityDurationScale: AnimVariants.variantOpacityDurationScale
|
||||
readonly property bool isDirectionalEffect: AnimVariants.isDirectionalEffect
|
||||
readonly property bool isDepthEffect: AnimVariants.isDepthEffect
|
||||
readonly property bool isConnectedEffect: AnimVariants.isConnectedEffect
|
||||
readonly property real connectedCornerRadius: {
|
||||
if (typeof SettingsData === "undefined") return 12;
|
||||
return SettingsData.connectedFrameModeActive ? SettingsData.frameRounding : cornerRadius;
|
||||
}
|
||||
readonly property color connectedSurfaceColor: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return withAlpha(surfaceContainer, popupTransparency);
|
||||
return isConnectedEffect
|
||||
? Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity)
|
||||
: withAlpha(surfaceContainer, popupTransparency);
|
||||
}
|
||||
readonly property real connectedSurfaceRadius: isConnectedEffect ? connectedCornerRadius : cornerRadius
|
||||
readonly property bool connectedSurfaceBlurEnabled: (typeof SettingsData === "undefined")
|
||||
? true
|
||||
: (!isConnectedEffect || SettingsData.frameBlurEnabled)
|
||||
readonly property real effectScaleCollapsed: AnimVariants.effectScaleCollapsed
|
||||
readonly property real effectAnimOffset: AnimVariants.effectAnimOffset
|
||||
function variantDuration(baseDuration, entering) { return AnimVariants.variantDuration(baseDuration, entering); }
|
||||
function variantExitCleanupPadding() { return AnimVariants.variantExitCleanupPadding(); }
|
||||
function variantCloseInterval(baseDuration) { return AnimVariants.variantCloseInterval(baseDuration); }
|
||||
|
||||
readonly property var animationPresetDurations: {
|
||||
"none": 0,
|
||||
"short": 250,
|
||||
@@ -1125,7 +1159,13 @@ Singleton {
|
||||
property real iconSizeLarge: 32
|
||||
|
||||
property real panelTransparency: 0.85
|
||||
property real popupTransparency: typeof SettingsData !== "undefined" && SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 1.0
|
||||
property real popupTransparency: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return 1.0;
|
||||
if (isConnectedEffect)
|
||||
return SettingsData.frameOpacity !== undefined ? SettingsData.frameOpacity : 1.0;
|
||||
return SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 1.0;
|
||||
}
|
||||
|
||||
function screenTransition() {
|
||||
if (CompositorService.isNiri) {
|
||||
@@ -1824,6 +1864,12 @@ Singleton {
|
||||
return Qt.rgba(c.r, c.g, c.b, a);
|
||||
}
|
||||
|
||||
function popupLayerColor(baseColor) {
|
||||
if (isConnectedEffect)
|
||||
return connectedSurfaceColor;
|
||||
return withAlpha(baseColor, popupTransparency);
|
||||
}
|
||||
|
||||
function blendAlpha(c, a) {
|
||||
return Qt.rgba(c.r, c.g, c.b, c.a * a);
|
||||
}
|
||||
|
||||
@@ -75,6 +75,8 @@ var SPEC = {
|
||||
|
||||
vpnLastConnected: { def: "" },
|
||||
|
||||
lastPlayerIdentity: { def: "" },
|
||||
|
||||
deviceMaxVolumes: { def: {} },
|
||||
hiddenOutputDeviceNames: { def: [] },
|
||||
hiddenInputDeviceNames: { def: [] },
|
||||
|
||||
@@ -49,6 +49,10 @@ var SPEC = {
|
||||
modalAnimationSpeed: { def: 1 },
|
||||
modalCustomAnimationDuration: { def: 150 },
|
||||
enableRippleEffects: { def: true },
|
||||
animationVariant: { def: 0 },
|
||||
motionEffect: { def: 0 },
|
||||
directionalAnimationMode: { def: 0 },
|
||||
previousDirectionalMode: { def: 1 },
|
||||
m3ElevationEnabled: { def: true },
|
||||
m3ElevationIntensity: { def: 12 },
|
||||
m3ElevationOpacity: { def: 30 },
|
||||
@@ -140,6 +144,7 @@ var SPEC = {
|
||||
workspaceNameIcons: { def: {} },
|
||||
waveProgressEnabled: { def: true },
|
||||
scrollTitleEnabled: { def: true },
|
||||
mediaAdaptiveWidthEnabled: { def: true },
|
||||
audioVisualizerEnabled: { def: true },
|
||||
audioScrollMode: { def: "volume" },
|
||||
audioWheelScrollAmount: { def: 5 },
|
||||
@@ -242,6 +247,7 @@ var SPEC = {
|
||||
|
||||
soundsEnabled: { def: true },
|
||||
useSystemSoundTheme: { def: false },
|
||||
soundLogin: { def: false },
|
||||
soundNewNotification: { def: true },
|
||||
soundVolumeChanged: { def: true },
|
||||
soundPluggedIn: { def: true },
|
||||
@@ -441,6 +447,7 @@ var SPEC = {
|
||||
displayProfileAutoSelect: { def: false },
|
||||
displayShowDisconnected: { def: false },
|
||||
displaySnapToEdge: { def: true },
|
||||
connectedFrameBarStyleBackups: { def: {} },
|
||||
|
||||
barConfigs: {
|
||||
def: [{
|
||||
@@ -547,7 +554,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,10 +224,22 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: loginSoundTimer
|
||||
// Half a second delay before playing login sound, otherwise the sound may be cut off
|
||||
// 50 is the minimum that seems to work, but 500 is safer
|
||||
interval: 500
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
AudioService.playLoginSoundIfApplicable();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
dockRecreateDebounce.start();
|
||||
// Force PolkitService singleton to initialize
|
||||
PolkitService.polkitAvailable;
|
||||
loginSoundTimer.start();
|
||||
}
|
||||
|
||||
Loader {
|
||||
|
||||
@@ -369,9 +369,7 @@ Item {
|
||||
}
|
||||
|
||||
function previous(): void {
|
||||
if (MprisController.activePlayer && MprisController.activePlayer.canGoPrevious) {
|
||||
MprisController.activePlayer.previous();
|
||||
}
|
||||
MprisController.previousOrRewind();
|
||||
}
|
||||
|
||||
function next(): void {
|
||||
|
||||
@@ -122,7 +122,7 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("No recent clipboard entries found")
|
||||
text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No recent clipboard entries found") : I18n.tr("Connecting to clipboard service…")
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
@@ -181,7 +181,7 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("No saved clipboard entries")
|
||||
text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No saved clipboard entries") : I18n.tr("Connecting to clipboard service…")
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
|
||||
@@ -60,18 +60,23 @@ DankModal {
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (!clipboardAvailable) {
|
||||
ToastService.showError(I18n.tr("Clipboard service not available"));
|
||||
return;
|
||||
}
|
||||
open();
|
||||
activeImageLoads = 0;
|
||||
shouldHaveFocus = true;
|
||||
ClipboardService.reset();
|
||||
ClipboardService.refresh();
|
||||
keyboardController.reset();
|
||||
|
||||
Qt.callLater(function () {
|
||||
if (clipboardAvailable) {
|
||||
if (Theme.isConnectedEffect) {
|
||||
Qt.callLater(() => {
|
||||
if (clipboardHistoryModal.shouldBeVisible)
|
||||
ClipboardService.refresh();
|
||||
});
|
||||
} else {
|
||||
ClipboardService.refresh();
|
||||
}
|
||||
}
|
||||
if (contentLoader.item?.searchField) {
|
||||
contentLoader.item.searchField.text = "";
|
||||
contentLoader.item.searchField.forceActiveFocus();
|
||||
|
||||
@@ -50,14 +50,9 @@ DankPopout {
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (!clipboardAvailable) {
|
||||
ToastService.showError(I18n.tr("Clipboard service not available"));
|
||||
return;
|
||||
}
|
||||
open();
|
||||
activeImageLoads = 0;
|
||||
ClipboardService.reset();
|
||||
ClipboardService.refresh();
|
||||
keyboardController.reset();
|
||||
|
||||
Qt.callLater(function () {
|
||||
@@ -122,10 +117,18 @@ DankPopout {
|
||||
onBackgroundClicked: hide()
|
||||
|
||||
onShouldBeVisibleChanged: {
|
||||
if (!shouldBeVisible) {
|
||||
if (!shouldBeVisible)
|
||||
return;
|
||||
if (clipboardAvailable) {
|
||||
if (Theme.isConnectedEffect) {
|
||||
Qt.callLater(() => {
|
||||
if (root.shouldBeVisible)
|
||||
ClipboardService.refresh();
|
||||
});
|
||||
} else {
|
||||
ClipboardService.refresh();
|
||||
}
|
||||
}
|
||||
ClipboardService.refresh();
|
||||
keyboardController.reset();
|
||||
Qt.callLater(function () {
|
||||
if (contentLoader.item?.searchField) {
|
||||
|
||||
@@ -26,15 +26,22 @@ Item {
|
||||
property bool closeOnEscapeKey: true
|
||||
property bool closeOnBackgroundClick: true
|
||||
property string animationType: "scale"
|
||||
property int animationDuration: Theme.modalAnimationDuration
|
||||
property real animationScaleCollapsed: 0.96
|
||||
property real animationOffset: Theme.spacingL
|
||||
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
|
||||
property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
readonly property bool connectedMotionParity: Theme.isConnectedEffect
|
||||
property int animationDuration: connectedMotionParity ? Theme.popoutAnimationDuration : Theme.modalAnimationDuration
|
||||
property real animationScaleCollapsed: Theme.effectScaleCollapsed
|
||||
property real animationOffset: Theme.effectAnimOffset
|
||||
property list<real> animationEnterCurve: connectedMotionParity ? Theme.variantPopoutEnterCurve : Theme.variantModalEnterCurve
|
||||
property list<real> animationExitCurve: connectedMotionParity ? Theme.variantPopoutExitCurve : Theme.variantModalExitCurve
|
||||
property color backgroundColor: Theme.surfaceContainer
|
||||
property color borderColor: Theme.outlineMedium
|
||||
property real borderWidth: 0
|
||||
property real cornerRadius: Theme.cornerRadius
|
||||
readonly property bool connectedSurfaceOverride: Theme.isConnectedEffect
|
||||
readonly property color effectiveBackgroundColor: connectedSurfaceOverride ? Theme.connectedSurfaceColor : backgroundColor
|
||||
readonly property color effectiveBorderColor: connectedSurfaceOverride ? "transparent" : borderColor
|
||||
readonly property real effectiveBorderWidth: connectedSurfaceOverride ? 0 : borderWidth
|
||||
readonly property real effectiveCornerRadius: connectedSurfaceOverride ? Theme.connectedSurfaceRadius : cornerRadius
|
||||
readonly property bool effectiveBlurEnabled: Theme.connectedSurfaceBlurEnabled
|
||||
property bool enableShadow: true
|
||||
property alias modalFocusScope: focusScope
|
||||
property bool shouldBeVisible: false
|
||||
@@ -45,11 +52,13 @@ Item {
|
||||
property bool keepPopoutsOpen: false
|
||||
property var customKeyboardFocus: null
|
||||
property bool useOverlayLayer: false
|
||||
property real frozenMotionOffsetX: 0
|
||||
property real frozenMotionOffsetY: 0
|
||||
readonly property alias contentWindow: contentWindow
|
||||
readonly property alias clickCatcher: clickCatcher
|
||||
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
||||
readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground
|
||||
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackground
|
||||
readonly property bool useSingleWindow: CompositorService.isHyprland
|
||||
|
||||
signal opened
|
||||
signal dialogClosed
|
||||
@@ -59,33 +68,34 @@ Item {
|
||||
|
||||
function open() {
|
||||
closeTimer.stop();
|
||||
const focusedScreen = CompositorService.getFocusedScreen();
|
||||
const screenChanged = focusedScreen && contentWindow.screen !== focusedScreen;
|
||||
if (focusedScreen) {
|
||||
if (screenChanged)
|
||||
contentWindow.visible = false;
|
||||
contentWindow.screen = focusedScreen;
|
||||
if (!useSingleWindow) {
|
||||
if (screenChanged)
|
||||
clickCatcher.visible = false;
|
||||
clickCatcher.screen = focusedScreen;
|
||||
}
|
||||
}
|
||||
if (screenChanged) {
|
||||
Qt.callLater(() => root._finishOpen());
|
||||
} else {
|
||||
_finishOpen();
|
||||
}
|
||||
}
|
||||
animationsEnabled = false;
|
||||
frozenMotionOffsetX = modalContainer ? modalContainer.offsetX : 0;
|
||||
frozenMotionOffsetY = modalContainer ? modalContainer.offsetY : animationOffset;
|
||||
|
||||
function _finishOpen() {
|
||||
const focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen) {
|
||||
contentWindow.screen = focusedScreen;
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.screen = focusedScreen;
|
||||
}
|
||||
|
||||
if (Theme.isDirectionalEffect || root.useBackground) {
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.visible = true;
|
||||
contentWindow.visible = true;
|
||||
}
|
||||
ModalManager.openModal(root);
|
||||
shouldBeVisible = true;
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.visible = true;
|
||||
contentWindow.visible = true;
|
||||
shouldHaveFocus = false;
|
||||
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
|
||||
|
||||
Qt.callLater(() => {
|
||||
animationsEnabled = true;
|
||||
shouldBeVisible = true;
|
||||
if (!useSingleWindow && !clickCatcher.visible)
|
||||
clickCatcher.visible = true;
|
||||
if (!contentWindow.visible)
|
||||
contentWindow.visible = true;
|
||||
shouldHaveFocus = false;
|
||||
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
|
||||
});
|
||||
}
|
||||
|
||||
function close() {
|
||||
@@ -146,7 +156,7 @@ Item {
|
||||
|
||||
Timer {
|
||||
id: closeTimer
|
||||
interval: animationDuration + 50
|
||||
interval: Theme.variantCloseInterval(animationDuration)
|
||||
onTriggered: {
|
||||
if (shouldBeVisible)
|
||||
return;
|
||||
@@ -160,7 +170,19 @@ Item {
|
||||
readonly property var shadowLevel: Theme.elevationLevel3
|
||||
readonly property real shadowFallbackOffset: 6
|
||||
readonly property real shadowRenderPadding: (root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
|
||||
readonly property real shadowMotionPadding: animationType === "slide" ? 30 : Math.max(0, animationOffset)
|
||||
readonly property real shadowMotionPadding: {
|
||||
if (Theme.isConnectedEffect)
|
||||
return 0;
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0 && Theme.isDirectionalEffect)
|
||||
return 0; // Wayland native overlap mask
|
||||
if (animationType === "slide")
|
||||
return 30;
|
||||
if (Theme.isDirectionalEffect)
|
||||
return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.9);
|
||||
if (Theme.isDepthEffect)
|
||||
return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.35);
|
||||
return Math.max(0, animationOffset);
|
||||
}
|
||||
readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
|
||||
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
|
||||
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
|
||||
@@ -220,9 +242,26 @@ Item {
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.closeOnBackgroundClick && root.shouldBeVisible
|
||||
enabled: !root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible
|
||||
onClicked: root.backgroundClicked()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
color: "black"
|
||||
opacity: (!root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
|
||||
NumberAnimation {
|
||||
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
@@ -232,12 +271,13 @@ Item {
|
||||
|
||||
WindowBlur {
|
||||
targetWindow: contentWindow
|
||||
blurEnabled: root.effectiveBlurEnabled
|
||||
readonly property real s: Math.min(1, modalContainer.scaleValue)
|
||||
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr)
|
||||
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
|
||||
blurWidth: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.width * s : 0
|
||||
blurHeight: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.height * s : 0
|
||||
blurRadius: root.cornerRadius
|
||||
blurWidth: (root.shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.width * s : 0
|
||||
blurHeight: (root.shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.height * s : 0
|
||||
blurRadius: root.effectiveCornerRadius
|
||||
}
|
||||
|
||||
WlrLayershell.namespace: root.layerNamespace
|
||||
@@ -275,9 +315,12 @@ Item {
|
||||
bottom: root.useSingleWindow
|
||||
}
|
||||
|
||||
readonly property real actualMarginLeft: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
|
||||
readonly property real actualMarginTop: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
|
||||
|
||||
WlrLayershell.margins {
|
||||
left: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
|
||||
top: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
|
||||
left: actualMarginLeft
|
||||
top: actualMarginTop
|
||||
right: 0
|
||||
bottom: 0
|
||||
}
|
||||
@@ -307,13 +350,14 @@ Item {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
color: "black"
|
||||
opacity: root.useBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
|
||||
visible: root.useBackground
|
||||
opacity: (root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: root.animationDuration
|
||||
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
|
||||
NumberAnimation {
|
||||
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
@@ -321,8 +365,8 @@ Item {
|
||||
|
||||
Item {
|
||||
id: modalContainer
|
||||
x: root.useSingleWindow ? root.alignedX : shadowBuffer
|
||||
y: root.useSingleWindow ? root.alignedY : shadowBuffer
|
||||
x: (root.useSingleWindow ? root.alignedX : (root.alignedX - contentWindow.actualMarginLeft)) + Theme.snap(animX, root.dpr)
|
||||
y: (root.useSingleWindow ? root.alignedY : (root.alignedY - contentWindow.actualMarginTop)) + Theme.snap(animY, root.dpr)
|
||||
|
||||
width: root.alignedWidth
|
||||
height: root.alignedHeight
|
||||
@@ -338,45 +382,117 @@ Item {
|
||||
}
|
||||
|
||||
readonly property bool slide: root.animationType === "slide"
|
||||
readonly property real offsetX: slide ? 15 : 0
|
||||
readonly property real offsetY: slide ? -30 : root.animationOffset
|
||||
|
||||
property real animX: 0
|
||||
property real animY: 0
|
||||
property real scaleValue: root.animationScaleCollapsed
|
||||
|
||||
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
|
||||
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onShouldBeVisibleChanged() {
|
||||
modalContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetX, root.dpr);
|
||||
modalContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetY, root.dpr);
|
||||
modalContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed;
|
||||
readonly property bool directionalEffect: Theme.isDirectionalEffect
|
||||
readonly property bool depthEffect: Theme.isDepthEffect
|
||||
readonly property real directionalTravel: Math.max(root.animationOffset, Math.max(root.alignedWidth, root.alignedHeight) * 0.8)
|
||||
readonly property real depthTravel: Math.max(root.animationOffset * 0.8, 36)
|
||||
readonly property real customAnchorX: root.alignedX + root.alignedWidth * 0.5
|
||||
readonly property real customAnchorY: root.alignedY + root.alignedHeight * 0.5
|
||||
readonly property real customDistLeft: customAnchorX
|
||||
readonly property real customDistRight: root.screenWidth - customAnchorX
|
||||
readonly property real customDistTop: customAnchorY
|
||||
readonly property real customDistBottom: root.screenHeight - customAnchorY
|
||||
readonly property real offsetX: {
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
|
||||
return 0;
|
||||
if (slide && !directionalEffect && !depthEffect)
|
||||
return 15;
|
||||
if (directionalEffect) {
|
||||
switch (root.positioning) {
|
||||
case "top-right":
|
||||
return 0;
|
||||
case "custom":
|
||||
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
|
||||
return -directionalTravel;
|
||||
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
|
||||
return directionalTravel;
|
||||
return 0;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if (depthEffect) {
|
||||
switch (root.positioning) {
|
||||
case "top-right":
|
||||
return 0;
|
||||
case "custom":
|
||||
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
|
||||
return -depthTravel;
|
||||
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
|
||||
return depthTravel;
|
||||
return 0;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
readonly property real offsetY: {
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
|
||||
return 0;
|
||||
if (slide && !directionalEffect && !depthEffect)
|
||||
return -30;
|
||||
if (directionalEffect) {
|
||||
switch (root.positioning) {
|
||||
case "top-right":
|
||||
return -Math.max(directionalTravel * 0.65, 96);
|
||||
case "custom":
|
||||
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
|
||||
return -directionalTravel;
|
||||
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
|
||||
return directionalTravel;
|
||||
return 0;
|
||||
default:
|
||||
// Default to sliding down from top when centered
|
||||
return -Math.max(directionalTravel, root.screenHeight * 0.24);
|
||||
}
|
||||
}
|
||||
if (depthEffect) {
|
||||
switch (root.positioning) {
|
||||
case "top-right":
|
||||
return -depthTravel * 0.75;
|
||||
case "custom":
|
||||
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
|
||||
return -depthTravel;
|
||||
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
|
||||
return depthTravel;
|
||||
return depthTravel * 0.45;
|
||||
default:
|
||||
return -depthTravel;
|
||||
}
|
||||
}
|
||||
return root.animationOffset;
|
||||
}
|
||||
|
||||
property real animX: root.shouldBeVisible ? 0 : root.frozenMotionOffsetX
|
||||
property real animY: root.shouldBeVisible ? 0 : root.frozenMotionOffsetY
|
||||
|
||||
readonly property real computedScaleCollapsed: (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) ? 0.0 : root.animationScaleCollapsed
|
||||
property real scaleValue: root.shouldBeVisible ? 1.0 : computedScaleCollapsed
|
||||
|
||||
Behavior on animX {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: root.animationDuration
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on animY {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: root.animationDuration
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scaleValue {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: root.animationDuration
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
@@ -392,15 +508,14 @@ Item {
|
||||
id: animatedContent
|
||||
anchors.fill: parent
|
||||
clip: false
|
||||
opacity: root.shouldBeVisible ? 1 : 0
|
||||
opacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (root.shouldBeVisible ? 1 : 0)
|
||||
scale: modalContainer.scaleValue
|
||||
x: Theme.snap(modalContainer.animX, root.dpr) + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5
|
||||
y: Theme.snap(modalContainer.animY, root.dpr) + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5
|
||||
transformOrigin: Item.Center
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled
|
||||
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
|
||||
NumberAnimation {
|
||||
duration: animationDuration
|
||||
duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
@@ -411,19 +526,19 @@ Item {
|
||||
anchors.fill: parent
|
||||
level: root.shadowLevel
|
||||
fallbackOffset: root.shadowFallbackOffset
|
||||
targetRadius: root.cornerRadius
|
||||
targetColor: root.backgroundColor
|
||||
borderColor: root.borderColor
|
||||
borderWidth: root.borderWidth
|
||||
targetRadius: root.effectiveCornerRadius
|
||||
targetColor: root.effectiveBackgroundColor
|
||||
borderColor: root.effectiveBorderColor
|
||||
borderWidth: root.effectiveBorderWidth
|
||||
shadowEnabled: root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: root.cornerRadius
|
||||
radius: root.effectiveCornerRadius
|
||||
color: "transparent"
|
||||
border.color: BlurService.borderColor
|
||||
border.width: BlurService.borderWidth
|
||||
border.color: root.connectedSurfaceOverride ? "transparent" : BlurService.borderColor
|
||||
border.width: root.connectedSurfaceOverride ? 0 : BlurService.borderWidth
|
||||
z: 100
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
@@ -14,16 +15,24 @@ Item {
|
||||
property bool spotlightOpen: false
|
||||
property bool keyboardActive: false
|
||||
property bool contentVisible: false
|
||||
readonly property bool launcherMotionVisible: Theme.isConnectedEffect ? _motionActive : (Theme.isDirectionalEffect ? spotlightOpen : _motionActive)
|
||||
property var spotlightContent: launcherContentLoader.item
|
||||
property bool openedFromOverview: false
|
||||
property bool isClosing: false
|
||||
property bool _windowEnabled: true
|
||||
property bool _pendingInitialize: false
|
||||
property string _pendingQuery: ""
|
||||
property string _pendingMode: ""
|
||||
readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose
|
||||
|
||||
// Animation state — matches DankPopout/DankModal pattern
|
||||
property bool animationsEnabled: true
|
||||
property bool _motionActive: false
|
||||
property real _frozenMotionX: 0
|
||||
property real _frozenMotionY: 0
|
||||
|
||||
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
||||
readonly property var effectiveScreen: launcherWindow.screen
|
||||
readonly property var effectiveScreen: contentWindow.screen
|
||||
readonly property real screenWidth: effectiveScreen?.width ?? 1920
|
||||
readonly property real screenHeight: effectiveScreen?.height ?? 1080
|
||||
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
|
||||
@@ -57,8 +66,12 @@ Item {
|
||||
readonly property real modalX: (screenWidth - modalWidth) / 2
|
||||
readonly property real modalY: (screenHeight - modalHeight) / 2
|
||||
|
||||
readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
readonly property real cornerRadius: Theme.cornerRadius
|
||||
readonly property bool connectedSurfaceOverride: Theme.isConnectedEffect
|
||||
readonly property int launcherAnimationDuration: Theme.isConnectedEffect ? Theme.popoutAnimationDuration : Theme.modalAnimationDuration
|
||||
readonly property list<real> launcherEnterCurve: Theme.isConnectedEffect ? Theme.variantPopoutEnterCurve : Theme.variantModalEnterCurve
|
||||
readonly property list<real> launcherExitCurve: Theme.isConnectedEffect ? Theme.variantPopoutExitCurve : Theme.variantModalExitCurve
|
||||
readonly property color backgroundColor: connectedSurfaceOverride ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
readonly property real cornerRadius: connectedSurfaceOverride ? Theme.connectedSurfaceRadius : Theme.cornerRadius
|
||||
readonly property color borderColor: {
|
||||
if (!SettingsData.dankLauncherV2BorderEnabled)
|
||||
return Theme.outlineMedium;
|
||||
@@ -76,6 +89,37 @@ Item {
|
||||
}
|
||||
}
|
||||
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
|
||||
readonly property color effectiveBorderColor: connectedSurfaceOverride ? "transparent" : borderColor
|
||||
readonly property int effectiveBorderWidth: connectedSurfaceOverride ? 0 : borderWidth
|
||||
readonly property bool effectiveBlurEnabled: Theme.connectedSurfaceBlurEnabled
|
||||
|
||||
// Shadow padding for the content window (render padding only, no motion padding)
|
||||
readonly property var shadowLevel: Theme.elevationLevel3
|
||||
readonly property real shadowFallbackOffset: 6
|
||||
readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
|
||||
readonly property real shadowPad: Theme.snap(shadowRenderPadding, dpr)
|
||||
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
|
||||
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
|
||||
readonly property real alignedX: Theme.snap(modalX, dpr)
|
||||
readonly property real alignedY: Theme.snap(modalY, dpr)
|
||||
|
||||
// For directional/depth: window extends from screen top (content slides within)
|
||||
// For standard: small window tightly around the modal + shadow padding
|
||||
readonly property bool _needsExtendedWindow: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) || Theme.isDepthEffect
|
||||
// Content window geometry
|
||||
readonly property real _cwMarginLeft: Theme.snap(alignedX - shadowPad, dpr)
|
||||
readonly property real _cwMarginTop: _needsExtendedWindow ? 0 : Theme.snap(alignedY - shadowPad, dpr)
|
||||
readonly property real _cwWidth: alignedWidth + shadowPad * 2
|
||||
readonly property real _cwHeight: {
|
||||
if (Theme.isDirectionalEffect && !Theme.isConnectedEffect)
|
||||
return screenHeight + shadowPad;
|
||||
if (Theme.isDepthEffect)
|
||||
return alignedY + alignedHeight + shadowPad;
|
||||
return alignedHeight + shadowPad * 2;
|
||||
}
|
||||
// Where the content container sits inside the content window
|
||||
readonly property real _ccX: shadowPad
|
||||
readonly property real _ccY: _needsExtendedWindow ? alignedY : shadowPad
|
||||
|
||||
signal dialogClosed
|
||||
|
||||
@@ -96,18 +140,11 @@ Item {
|
||||
if (!spotlightContent)
|
||||
return;
|
||||
contentVisible = true;
|
||||
spotlightContent.searchField.forceActiveFocus();
|
||||
|
||||
var targetQuery = "";
|
||||
|
||||
if (query) {
|
||||
targetQuery = query;
|
||||
} else if (SettingsData.rememberLastQuery) {
|
||||
targetQuery = SessionData.launcherLastQuery || "";
|
||||
}
|
||||
// NOTE: forceActiveFocus() is deliberately NOT called here.
|
||||
// It is deferred to after animation starts to avoid compositor IPC stalls.
|
||||
|
||||
if (spotlightContent.searchField) {
|
||||
spotlightContent.searchField.text = targetQuery;
|
||||
spotlightContent.searchField.text = query;
|
||||
}
|
||||
if (spotlightContent.controller) {
|
||||
var targetMode = mode || SessionData.launcherLastMode || "all";
|
||||
@@ -122,10 +159,12 @@ Item {
|
||||
spotlightContent.controller.collapsedSections = {};
|
||||
spotlightContent.controller.selectedFlatIndex = 0;
|
||||
spotlightContent.controller.selectedItem = null;
|
||||
spotlightContent.controller.historyIndex = -1;
|
||||
spotlightContent.controller.searchQuery = targetQuery;
|
||||
|
||||
spotlightContent.controller.performSearch();
|
||||
if (query) {
|
||||
spotlightContent.controller.setSearchQuery(query);
|
||||
} else {
|
||||
spotlightContent.controller.searchQuery = "";
|
||||
spotlightContent.controller.performSearch();
|
||||
}
|
||||
}
|
||||
if (spotlightContent.resetScroll) {
|
||||
spotlightContent.resetScroll();
|
||||
@@ -135,47 +174,59 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
function _finishShow(query, mode) {
|
||||
spotlightOpen = true;
|
||||
function _openCommon(query, mode) {
|
||||
closeCleanupTimer.stop();
|
||||
isClosing = false;
|
||||
openedFromOverview = false;
|
||||
|
||||
keyboardActive = true;
|
||||
// Disable animations so the snap is instant
|
||||
animationsEnabled = false;
|
||||
|
||||
// Freeze the collapsed offsets (they depend on height which could change)
|
||||
_frozenMotionX = contentContainer ? contentContainer.collapsedMotionX : 0;
|
||||
_frozenMotionY = contentContainer ? contentContainer.collapsedMotionY : (Theme.isDirectionalEffect ? Math.max(root.screenHeight - root._ccY + root.shadowPad, Theme.effectAnimOffset * 1.1) : -Theme.effectAnimOffset);
|
||||
|
||||
var focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen) {
|
||||
backgroundWindow.screen = focusedScreen;
|
||||
contentWindow.screen = focusedScreen;
|
||||
}
|
||||
|
||||
// _motionActive = false ensures motionX/Y snap to frozen collapsed position
|
||||
_motionActive = false;
|
||||
|
||||
// Make windows visible but do NOT request keyboard focus yet
|
||||
ModalManager.openModal(root);
|
||||
spotlightOpen = true;
|
||||
backgroundWindow.visible = true;
|
||||
contentWindow.visible = true;
|
||||
if (useHyprlandFocusGrab)
|
||||
focusGrab.active = true;
|
||||
|
||||
// Load content and initialize (but no forceActiveFocus — that's deferred)
|
||||
_ensureContentLoadedAndInitialize(query || "", mode || "");
|
||||
|
||||
// Frame 1: enable animations and trigger enter motion
|
||||
Qt.callLater(() => {
|
||||
root.animationsEnabled = true;
|
||||
root._motionActive = true;
|
||||
|
||||
// Frame 2: request keyboard focus + activate search field
|
||||
// Double-deferred to avoid compositor IPC competing with animation frames
|
||||
Qt.callLater(() => {
|
||||
root.keyboardActive = true;
|
||||
if (root.spotlightContent && root.spotlightContent.searchField)
|
||||
root.spotlightContent.searchField.forceActiveFocus();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function show() {
|
||||
closeCleanupTimer.stop();
|
||||
|
||||
var focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
|
||||
spotlightOpen = false;
|
||||
isClosing = false;
|
||||
launcherWindow.screen = focusedScreen;
|
||||
Qt.callLater(() => root._finishShow("", ""));
|
||||
return;
|
||||
}
|
||||
|
||||
_finishShow("", "");
|
||||
_openCommon("", "");
|
||||
}
|
||||
|
||||
function showWithQuery(query) {
|
||||
closeCleanupTimer.stop();
|
||||
|
||||
var focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
|
||||
spotlightOpen = false;
|
||||
isClosing = false;
|
||||
launcherWindow.screen = focusedScreen;
|
||||
Qt.callLater(() => root._finishShow(query, ""));
|
||||
return;
|
||||
}
|
||||
|
||||
_finishShow(query, "");
|
||||
_openCommon(query, "");
|
||||
}
|
||||
|
||||
function hide() {
|
||||
@@ -183,13 +234,17 @@ Item {
|
||||
return;
|
||||
openedFromOverview = false;
|
||||
isClosing = true;
|
||||
contentVisible = false;
|
||||
// For directional effects, defer contentVisible=false so content stays rendered during exit slide
|
||||
if (!Theme.isDirectionalEffect)
|
||||
contentVisible = false;
|
||||
|
||||
// Trigger exit animation — Behaviors will animate motionX/Y to frozen collapsed position
|
||||
_motionActive = false;
|
||||
|
||||
keyboardActive = false;
|
||||
spotlightOpen = false;
|
||||
focusGrab.active = false;
|
||||
ModalManager.closeModal(root);
|
||||
|
||||
closeCleanupTimer.start();
|
||||
}
|
||||
|
||||
@@ -198,27 +253,7 @@ Item {
|
||||
}
|
||||
|
||||
function showWithMode(mode) {
|
||||
closeCleanupTimer.stop();
|
||||
|
||||
var focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
|
||||
spotlightOpen = false;
|
||||
isClosing = false;
|
||||
launcherWindow.screen = focusedScreen;
|
||||
Qt.callLater(() => root._finishShow("", mode));
|
||||
return;
|
||||
}
|
||||
|
||||
spotlightOpen = true;
|
||||
isClosing = false;
|
||||
openedFromOverview = false;
|
||||
|
||||
keyboardActive = true;
|
||||
ModalManager.openModal(root);
|
||||
if (useHyprlandFocusGrab)
|
||||
focusGrab.active = true;
|
||||
|
||||
_ensureContentLoadedAndInitialize("", mode);
|
||||
_openCommon("", mode);
|
||||
}
|
||||
|
||||
function toggleWithMode(mode) {
|
||||
@@ -239,10 +274,13 @@ Item {
|
||||
|
||||
Timer {
|
||||
id: closeCleanupTimer
|
||||
interval: Theme.modalAnimationDuration + 50
|
||||
interval: Theme.variantCloseInterval(root.launcherAnimationDuration)
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
isClosing = false;
|
||||
contentVisible = false;
|
||||
contentWindow.visible = false;
|
||||
backgroundWindow.visible = false;
|
||||
if (root.unloadContentOnClose)
|
||||
launcherContentLoader.active = false;
|
||||
dialogClosed();
|
||||
@@ -251,7 +289,6 @@ Item {
|
||||
|
||||
Connections {
|
||||
target: spotlightContent?.controller ?? null
|
||||
|
||||
function onModeChanged(mode) {
|
||||
if (spotlightContent.controller.autoSwitchedToFiles)
|
||||
return;
|
||||
@@ -261,7 +298,7 @@ Item {
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: focusGrab
|
||||
windows: [launcherWindow]
|
||||
windows: [contentWindow]
|
||||
active: false
|
||||
|
||||
onCleared: {
|
||||
@@ -286,55 +323,53 @@ Item {
|
||||
if (Quickshell.screens.length === 0)
|
||||
return;
|
||||
|
||||
const screenName = launcherWindow.screen?.name;
|
||||
if (screenName) {
|
||||
const screen = contentWindow.screen;
|
||||
const screenName = screen?.name;
|
||||
|
||||
let needsReset = !screen || !screenName;
|
||||
if (!needsReset) {
|
||||
needsReset = true;
|
||||
for (let i = 0; i < Quickshell.screens.length; i++) {
|
||||
if (Quickshell.screens[i].name === screenName)
|
||||
return;
|
||||
if (Quickshell.screens[i].name === screenName) {
|
||||
needsReset = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (spotlightOpen)
|
||||
hide();
|
||||
if (!needsReset)
|
||||
return;
|
||||
|
||||
const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0];
|
||||
if (newScreen)
|
||||
launcherWindow.screen = newScreen;
|
||||
if (!newScreen)
|
||||
return;
|
||||
|
||||
root._windowEnabled = false;
|
||||
backgroundWindow.screen = newScreen;
|
||||
contentWindow.screen = newScreen;
|
||||
Qt.callLater(() => {
|
||||
root._windowEnabled = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Background window: fullscreen, handles darkening + click-to-dismiss ──
|
||||
PanelWindow {
|
||||
id: launcherWindow
|
||||
visible: spotlightOpen || isClosing
|
||||
id: backgroundWindow
|
||||
visible: false
|
||||
color: "transparent"
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
WindowBlur {
|
||||
targetWindow: launcherWindow
|
||||
readonly property real s: Math.min(1, modalContainer.scale)
|
||||
blurX: root.modalX + root.modalWidth * (1 - s) * 0.5
|
||||
blurY: root.modalY + root.modalHeight * (1 - s) * 0.5
|
||||
blurWidth: (contentVisible && modalContainer.opacity > 0) ? root.modalWidth * s : 0
|
||||
blurHeight: (contentVisible && modalContainer.opacity > 0) ? root.modalHeight * s : 0
|
||||
blurRadius: root.cornerRadius
|
||||
}
|
||||
WlrLayershell.namespace: "dms:spotlight:bg"
|
||||
WlrLayershell.layer: WlrLayershell.Top
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
WlrLayershell.namespace: "dms:spotlight"
|
||||
WlrLayershell.layer: {
|
||||
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
||||
case "bottom":
|
||||
console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
|
||||
return WlrLayershell.Top;
|
||||
case "background":
|
||||
console.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
|
||||
return WlrLayershell.Top;
|
||||
case "overlay":
|
||||
return WlrLayershell.Overlay;
|
||||
default:
|
||||
return WlrLayershell.Top;
|
||||
}
|
||||
WlrLayershell.margins {
|
||||
top: contentContainer.dockTop ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 0 ? Theme.px(42, root.dpr) : 0)
|
||||
bottom: contentContainer.dockBottom ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 1 ? Theme.px(42, root.dpr) : 0)
|
||||
left: contentContainer.dockLeft ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 2 ? Theme.px(42, root.dpr) : 0)
|
||||
right: contentContainer.dockRight ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 3 ? Theme.px(42, root.dpr) : 0)
|
||||
}
|
||||
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
@@ -344,11 +379,11 @@ Item {
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: spotlightOpen ? fullScreenMask : null
|
||||
item: (spotlightOpen || isClosing) ? bgFullScreenMask : null
|
||||
}
|
||||
|
||||
Item {
|
||||
id: fullScreenMask
|
||||
id: bgFullScreenMask
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
@@ -356,13 +391,14 @@ Item {
|
||||
id: backgroundDarken
|
||||
anchors.fill: parent
|
||||
color: "black"
|
||||
opacity: contentVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
|
||||
visible: contentVisible || opacity > 0
|
||||
opacity: launcherMotionVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
|
||||
visible: launcherMotionVisible || opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
|
||||
DankAnim {
|
||||
duration: Theme.modalAnimationDuration
|
||||
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
duration: Math.round(Theme.variantDuration(root.launcherAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.bezierCurve: launcherMotionVisible ? root.launcherEnterCurve : root.launcherExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -370,96 +406,236 @@ Item {
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: spotlightOpen
|
||||
onClicked: mouse => {
|
||||
var contentX = modalContainer.x;
|
||||
var contentY = modalContainer.y;
|
||||
var contentW = modalContainer.width;
|
||||
var contentH = modalContainer.height;
|
||||
onClicked: root.hide()
|
||||
}
|
||||
}
|
||||
|
||||
if (mouse.x < contentX || mouse.x > contentX + contentW || mouse.y < contentY || mouse.y > contentY + contentH) {
|
||||
root.hide();
|
||||
}
|
||||
// ── Content window: SMALL, positioned with margins — only renders the modal area ──
|
||||
PanelWindow {
|
||||
id: contentWindow
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
WindowBlur {
|
||||
targetWindow: contentWindow
|
||||
blurEnabled: root.effectiveBlurEnabled
|
||||
readonly property real s: Math.min(1, contentContainer.scaleValue)
|
||||
blurX: root._ccX + root.alignedWidth * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr)
|
||||
blurY: root._ccY + root.alignedHeight * (1 - s) * 0.5 + Theme.snap(contentContainer.animY, root.dpr)
|
||||
blurWidth: (root.spotlightOpen || root.isClosing) && contentWrapper.opacity > 0 ? root.alignedWidth * s : 0
|
||||
blurHeight: (root.spotlightOpen || root.isClosing) && contentWrapper.opacity > 0 ? root.alignedHeight * s : 0
|
||||
blurRadius: root.cornerRadius
|
||||
}
|
||||
|
||||
WlrLayershell.namespace: "dms:spotlight"
|
||||
WlrLayershell.layer: {
|
||||
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
||||
case "bottom":
|
||||
console.error("DankLauncherV2Modal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
|
||||
return WlrLayershell.Top;
|
||||
case "background":
|
||||
console.error("DankLauncherV2Modal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
|
||||
return WlrLayershell.Top;
|
||||
case "overlay":
|
||||
return WlrLayershell.Overlay;
|
||||
default:
|
||||
return WlrLayershell.Top;
|
||||
}
|
||||
}
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
|
||||
|
||||
anchors {
|
||||
left: true
|
||||
top: true
|
||||
}
|
||||
|
||||
WlrLayershell.margins {
|
||||
left: root._cwMarginLeft
|
||||
top: root._cwMarginTop
|
||||
}
|
||||
|
||||
implicitWidth: root._cwWidth
|
||||
implicitHeight: root._cwHeight
|
||||
|
||||
mask: Region {
|
||||
item: contentInputMask
|
||||
}
|
||||
|
||||
Item {
|
||||
id: modalContainer
|
||||
x: root.modalX
|
||||
y: root.modalY
|
||||
width: root.modalWidth
|
||||
height: root.modalHeight
|
||||
visible: contentVisible || opacity > 0
|
||||
|
||||
opacity: contentVisible ? 1 : 0
|
||||
scale: contentVisible ? 1 : 0.96
|
||||
transformOrigin: Item.Center
|
||||
|
||||
Behavior on opacity {
|
||||
DankAnim {
|
||||
duration: Theme.modalAnimationDuration
|
||||
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
DankAnim {
|
||||
duration: Theme.modalAnimationDuration
|
||||
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
ElevationShadow {
|
||||
id: launcherShadowLayer
|
||||
anchors.fill: parent
|
||||
level: Theme.elevationLevel3
|
||||
fallbackOffset: 6
|
||||
targetColor: root.backgroundColor
|
||||
borderColor: root.borderColor
|
||||
borderWidth: root.borderWidth
|
||||
targetRadius: root.cornerRadius
|
||||
shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: mouse => mouse.accepted = true
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: keyboardActive
|
||||
|
||||
Loader {
|
||||
id: launcherContentLoader
|
||||
anchors.fill: parent
|
||||
active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
|
||||
asynchronous: false
|
||||
sourceComponent: LauncherContent {
|
||||
focus: true
|
||||
parentModal: root
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (root._pendingInitialize) {
|
||||
root._initializeAndShow(root._pendingQuery, root._pendingMode);
|
||||
root._pendingInitialize = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
root.hide();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: root.cornerRadius
|
||||
color: "transparent"
|
||||
border.color: BlurService.borderColor
|
||||
border.width: BlurService.borderWidth
|
||||
}
|
||||
id: contentInputMask
|
||||
visible: false
|
||||
x: contentContainer.x + contentWrapper.x
|
||||
y: contentContainer.y + contentWrapper.y
|
||||
width: root.alignedWidth
|
||||
height: root.alignedHeight
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentContainer
|
||||
|
||||
// For directional/depth: contentContainer is at alignedY from window top (window starts at screen top)
|
||||
// For standard: contentContainer is at shadowPad from window top (window starts near modal)
|
||||
x: root._ccX
|
||||
y: root._ccY
|
||||
width: root.alignedWidth
|
||||
height: root.alignedHeight
|
||||
|
||||
readonly property int dockEdge: typeof SettingsData !== "undefined" ? SettingsData.dockPosition : 1
|
||||
readonly property bool dockTop: dockEdge === 0
|
||||
readonly property bool dockBottom: dockEdge === 1
|
||||
readonly property bool dockLeft: dockEdge === 2
|
||||
readonly property bool dockRight: dockEdge === 3
|
||||
|
||||
readonly property real dockThickness: typeof SettingsData !== "undefined" && SettingsData.showDock ? Theme.px(SettingsData.dockIconSize + (SettingsData.dockMargin * 2) + SettingsData.dockSpacing + 8, root.dpr) : Theme.px(60, root.dpr)
|
||||
|
||||
readonly property bool directionalEffect: Theme.isDirectionalEffect
|
||||
readonly property bool depthEffect: Theme.isDepthEffect
|
||||
readonly property real collapsedMotionX: {
|
||||
if (directionalEffect) {
|
||||
if (dockLeft)
|
||||
return -(root._ccX + root.alignedWidth + Theme.effectAnimOffset);
|
||||
if (dockRight)
|
||||
return root.screenWidth - root._ccX + Theme.effectAnimOffset;
|
||||
}
|
||||
if (depthEffect)
|
||||
return Theme.effectAnimOffset * 0.25;
|
||||
return 0;
|
||||
}
|
||||
readonly property real collapsedMotionY: {
|
||||
if (directionalEffect) {
|
||||
if (dockTop)
|
||||
return -(root._ccY + root.alignedHeight + Theme.effectAnimOffset);
|
||||
if (dockBottom)
|
||||
return root.screenHeight - root._ccY + root.shadowPad + Theme.effectAnimOffset;
|
||||
return 0;
|
||||
}
|
||||
if (depthEffect)
|
||||
return -Math.max(Theme.effectAnimOffset * 0.85, 34);
|
||||
return -Math.max((root.shadowPad || 0) + Theme.effectAnimOffset, 40);
|
||||
}
|
||||
|
||||
// Declarative bindings — snap applied at render layer (contentWrapper x/y)
|
||||
property real animX: root._motionActive ? 0 : root._frozenMotionX
|
||||
property real animY: root._motionActive ? 0 : root._frozenMotionY
|
||||
property real scaleValue: root._motionActive ? 1.0 : (Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed))
|
||||
|
||||
Behavior on animX {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: Theme.variantDuration(root.launcherAnimationDuration, root._motionActive)
|
||||
easing.bezierCurve: root._motionActive ? root.launcherEnterCurve : root.launcherExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on animY {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: Theme.variantDuration(root.launcherAnimationDuration, root._motionActive)
|
||||
easing.bezierCurve: root._motionActive ? root.launcherEnterCurve : root.launcherExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scaleValue {
|
||||
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2))
|
||||
DankAnim {
|
||||
duration: Theme.variantDuration(root.launcherAnimationDuration, root._motionActive)
|
||||
easing.bezierCurve: root._motionActive ? root.launcherEnterCurve : root.launcherExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: directionalClipMask
|
||||
readonly property bool shouldClip: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0
|
||||
readonly property real clipOversize: 2000
|
||||
|
||||
clip: shouldClip
|
||||
|
||||
x: shouldClip ? (contentContainer.dockRight ? -clipOversize : (contentContainer.dockLeft ? contentContainer.dockThickness - root._ccX : -clipOversize)) : 0
|
||||
y: shouldClip ? (contentContainer.dockBottom ? -clipOversize : (contentContainer.dockTop ? contentContainer.dockThickness - root._ccY : -clipOversize)) : 0
|
||||
|
||||
width: shouldClip ? parent.width + clipOversize + (contentContainer.dockRight ? (root.screenWidth - contentContainer.dockThickness - root._ccX - parent.width) : (contentContainer.dockLeft ? clipOversize : clipOversize)) : parent.width
|
||||
height: shouldClip ? parent.height + clipOversize + (contentContainer.dockBottom ? (root.screenHeight - contentContainer.dockThickness - root._ccY - parent.height) : (contentContainer.dockTop ? clipOversize : clipOversize)) : parent.height
|
||||
|
||||
Item {
|
||||
id: aligner
|
||||
x: directionalClipMask.x !== 0 ? -directionalClipMask.x : 0
|
||||
y: directionalClipMask.y !== 0 ? -directionalClipMask.y : 0
|
||||
width: contentContainer.width
|
||||
height: contentContainer.height
|
||||
|
||||
// Shadow mirrors contentWrapper position/scale/opacity
|
||||
ElevationShadow {
|
||||
id: launcherShadowLayer
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
opacity: contentWrapper.opacity
|
||||
scale: contentWrapper.scale
|
||||
x: contentWrapper.x
|
||||
y: contentWrapper.y
|
||||
level: root.shadowLevel
|
||||
fallbackOffset: root.shadowFallbackOffset
|
||||
targetColor: root.backgroundColor
|
||||
borderColor: root.effectiveBorderColor
|
||||
borderWidth: root.effectiveBorderWidth
|
||||
targetRadius: root.cornerRadius
|
||||
shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||
}
|
||||
|
||||
// contentWrapper moves inside static contentContainer — DankPopout pattern
|
||||
Item {
|
||||
id: contentWrapper
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
opacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (launcherMotionVisible ? 1 : 0)
|
||||
visible: opacity > 0
|
||||
scale: contentContainer.scaleValue
|
||||
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
||||
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
|
||||
DankAnim {
|
||||
duration: Math.round(Theme.variantDuration(root.launcherAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.bezierCurve: launcherMotionVisible ? root.launcherEnterCurve : root.launcherExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: mouse => mouse.accepted = true
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: keyboardActive
|
||||
|
||||
Loader {
|
||||
id: launcherContentLoader
|
||||
anchors.fill: parent
|
||||
active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
|
||||
asynchronous: false
|
||||
sourceComponent: LauncherContent {
|
||||
focus: true
|
||||
parentModal: root
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (root._pendingInitialize) {
|
||||
root._initializeAndShow(root._pendingQuery, root._pendingMode);
|
||||
root._pendingInitialize = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
root.hide();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
} // contentWrapper
|
||||
} // aligner
|
||||
} // directionalClipMask
|
||||
} // contentContainer
|
||||
} // PanelWindow
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ FocusScope {
|
||||
editCommentField.text = existing?.comment || "";
|
||||
editEnvVarsField.text = existing?.envVars || "";
|
||||
editExtraFlagsField.text = existing?.extraFlags || "";
|
||||
editDgpuToggle.checked = existing?.launchOnDgpu || false;
|
||||
editMode = true;
|
||||
Qt.callLater(() => editNameField.forceActiveFocus());
|
||||
}
|
||||
@@ -65,8 +64,6 @@ FocusScope {
|
||||
override.envVars = editEnvVarsField.text.trim();
|
||||
if (editExtraFlagsField.text.trim())
|
||||
override.extraFlags = editExtraFlagsField.text.trim();
|
||||
if (editDgpuToggle.checked)
|
||||
override.launchOnDgpu = true;
|
||||
SessionData.setAppOverride(editAppId, override);
|
||||
closeEditMode();
|
||||
}
|
||||
@@ -89,7 +86,7 @@ FocusScope {
|
||||
|
||||
Controller {
|
||||
id: controller
|
||||
active: root.parentModal?.spotlightOpen ?? true
|
||||
active: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
|
||||
viewModeContext: root.viewModeContext
|
||||
|
||||
onItemExecuted: {
|
||||
@@ -149,18 +146,10 @@ FocusScope {
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_Down:
|
||||
if (hasCtrl) {
|
||||
controller.navigateHistory("down");
|
||||
} else {
|
||||
controller.selectNext();
|
||||
}
|
||||
controller.selectNext();
|
||||
return;
|
||||
case Qt.Key_Up:
|
||||
if (hasCtrl) {
|
||||
controller.navigateHistory("up");
|
||||
} else {
|
||||
controller.selectPrevious();
|
||||
}
|
||||
controller.selectPrevious();
|
||||
return;
|
||||
case Qt.Key_PageDown:
|
||||
controller.selectPageDown(8);
|
||||
@@ -169,10 +158,6 @@ FocusScope {
|
||||
controller.selectPageUp(8);
|
||||
return;
|
||||
case Qt.Key_Right:
|
||||
if (hasCtrl) {
|
||||
controller.cycleMode();
|
||||
return;
|
||||
}
|
||||
if (controller.getCurrentSectionViewMode() !== "list") {
|
||||
controller.selectRight();
|
||||
return;
|
||||
@@ -180,25 +165,12 @@ FocusScope {
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_Left:
|
||||
if (hasCtrl) {
|
||||
const reverse = true;
|
||||
controller.cycleMode(reverse);
|
||||
return;
|
||||
}
|
||||
if (controller.getCurrentSectionViewMode() !== "list") {
|
||||
controller.selectLeft();
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_H:
|
||||
if (hasCtrl) {
|
||||
const reverse = true;
|
||||
controller.cycleMode(reverse);
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_J:
|
||||
if (hasCtrl) {
|
||||
controller.selectNext();
|
||||
@@ -213,13 +185,6 @@ FocusScope {
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_L:
|
||||
if (hasCtrl) {
|
||||
controller.cycleMode();
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_N:
|
||||
if (hasCtrl) {
|
||||
controller.selectNextSection();
|
||||
@@ -235,19 +200,13 @@ FocusScope {
|
||||
event.accepted = false;
|
||||
return;
|
||||
case Qt.Key_Tab:
|
||||
if (hasCtrl && actionPanel.hasActions) {
|
||||
if (actionPanel.hasActions) {
|
||||
actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show();
|
||||
return;
|
||||
}
|
||||
controller.selectNext();
|
||||
return;
|
||||
case Qt.Key_Backtab:
|
||||
if (hasCtrl && actionPanel.expanded) {
|
||||
const reverse = true;
|
||||
actionPanel.expanded ? actionPanel.cycleAction(reverse) : actionPanel.show();
|
||||
return;
|
||||
}
|
||||
controller.selectPrevious();
|
||||
if (actionPanel.expanded)
|
||||
actionPanel.hide();
|
||||
return;
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
@@ -311,7 +270,7 @@ FocusScope {
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: !editMode && !(root.parentModal?.isClosing ?? false)
|
||||
visible: !editMode
|
||||
|
||||
Item {
|
||||
id: footerBar
|
||||
@@ -429,7 +388,7 @@ FocusScope {
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Ctrl-Tab " + I18n.tr("actions")
|
||||
text: "Tab " + I18n.tr("actions")
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Theme.surfaceVariantText
|
||||
visible: actionPanel.hasActions
|
||||
@@ -503,7 +462,7 @@ FocusScope {
|
||||
showClearButton: true
|
||||
textColor: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
enabled: root.parentModal ? root.parentModal.spotlightOpen : true
|
||||
enabled: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
|
||||
placeholderText: ""
|
||||
ignoreUpDownKeys: true
|
||||
ignoreTabKeys: true
|
||||
@@ -737,6 +696,14 @@ FocusScope {
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2)
|
||||
opacity: {
|
||||
if (!root.parentModal)
|
||||
return 1;
|
||||
if (Theme.isDirectionalEffect && root.parentModal.isClosing)
|
||||
return 1;
|
||||
return root.parentModal.isClosing ? 0 : 1;
|
||||
}
|
||||
|
||||
ResultsList {
|
||||
id: resultsList
|
||||
anchors.fill: parent
|
||||
@@ -769,7 +736,6 @@ FocusScope {
|
||||
}
|
||||
function onSearchQueryRequested(query) {
|
||||
searchField.text = query;
|
||||
searchField.cursorPosition = query.length;
|
||||
}
|
||||
function onModeChanged() {
|
||||
extFilterField.text = "";
|
||||
@@ -980,15 +946,6 @@ FocusScope {
|
||||
keyNavigationBacktab: editEnvVarsField
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: editDgpuToggle
|
||||
width: parent.width
|
||||
text: I18n.tr("Launch on dGPU by default")
|
||||
visible: SessionService.nvidiaCommand.length > 0
|
||||
checked: false
|
||||
onToggled: checked => editDgpuToggle.checked = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -8,9 +8,6 @@ DankPopout {
|
||||
|
||||
layerNamespace: "dms:app-launcher"
|
||||
|
||||
readonly property real screenWidth: screen?.width ?? 1920
|
||||
readonly property real screenHeight: screen?.height ?? 1080
|
||||
|
||||
property string _pendingMode: ""
|
||||
property string _pendingQuery: ""
|
||||
|
||||
@@ -44,35 +41,8 @@ DankPopout {
|
||||
openWithQuery(query);
|
||||
}
|
||||
|
||||
readonly property int _baseWidth: {
|
||||
switch (SettingsData.dankLauncherV2Size) {
|
||||
case "micro":
|
||||
return 500;
|
||||
case "medium":
|
||||
return 720;
|
||||
case "large":
|
||||
return 860;
|
||||
default:
|
||||
return 620;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property int _baseHeight: {
|
||||
switch (SettingsData.dankLauncherV2Size) {
|
||||
case "micro":
|
||||
return 480;
|
||||
case "medium":
|
||||
return 720;
|
||||
case "large":
|
||||
return 860;
|
||||
default:
|
||||
return 600;
|
||||
}
|
||||
}
|
||||
|
||||
popupWidth: Math.min(_baseWidth, screenWidth - 100)
|
||||
popupHeight: Math.min(_baseHeight, screenHeight - 100)
|
||||
|
||||
popupWidth: 560
|
||||
popupHeight: 640
|
||||
triggerWidth: 40
|
||||
positioning: ""
|
||||
contentHandlesKeys: contentLoader.item?.launcherContent?.editMode ?? false
|
||||
@@ -90,7 +60,7 @@ DankPopout {
|
||||
if (!lc)
|
||||
return;
|
||||
|
||||
const query = _pendingQuery || (SettingsData.rememberLastQuery ? SessionData.launcherLastQuery : "") || "";
|
||||
const query = _pendingQuery;
|
||||
const mode = _pendingMode || SessionData.appDrawerLastMode || "apps";
|
||||
_pendingMode = "";
|
||||
_pendingQuery = "";
|
||||
@@ -102,9 +72,12 @@ DankPopout {
|
||||
if (lc.controller) {
|
||||
lc.controller.searchMode = mode;
|
||||
lc.controller.pluginFilter = "";
|
||||
lc.controller.searchQuery = query;
|
||||
|
||||
lc.controller.performSearch();
|
||||
lc.controller.searchQuery = "";
|
||||
if (query) {
|
||||
lc.controller.setSearchQuery(query);
|
||||
} else {
|
||||
lc.controller.performSearch();
|
||||
}
|
||||
}
|
||||
lc.resetScroll?.();
|
||||
lc.actionPanel?.hide();
|
||||
@@ -133,7 +106,7 @@ DankPopout {
|
||||
QtObject {
|
||||
id: modalAdapter
|
||||
property bool spotlightOpen: appDrawerPopout.shouldBeVisible
|
||||
readonly property bool isClosing: !appDrawerPopout.shouldBeVisible
|
||||
property bool isClosing: appDrawerPopout.isClosing
|
||||
|
||||
function hide() {
|
||||
appDrawerPopout.close();
|
||||
|
||||
@@ -136,9 +136,11 @@ DankPopout {
|
||||
z: 5000
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: !Theme.isDirectionalEffect
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.OutCubic
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,6 +998,7 @@ 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
|
||||
@@ -1155,6 +1185,7 @@ Item {
|
||||
if (!notificationCenterLoader.item) {
|
||||
return;
|
||||
}
|
||||
notificationCenterLoader.item.triggerScreen = barWindow.screen;
|
||||
const effectiveBarConfig = topBarContent.barConfig;
|
||||
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
|
||||
if (notificationCenterLoader.item.setBarContext) {
|
||||
@@ -1437,12 +1468,21 @@ 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 (systemUpdateLoader.item && systemUpdateLoader.item.setBarContext) {
|
||||
systemUpdateLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0);
|
||||
if (popout.setBarContext) {
|
||||
popout.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0);
|
||||
}
|
||||
systemUpdateLoader.item?.toggle();
|
||||
if (popout.setTriggerPosition) {
|
||||
const globalPos = visualContent.mapToItem(null, 0, 0);
|
||||
const currentScreen = parentScreen || Screen;
|
||||
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barWindow.effectiveBarThickness, visualWidth, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig);
|
||||
popout.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig);
|
||||
}
|
||||
PopoutManager.requestPopout(popout, undefined, "systemUpdate");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
@@ -644,14 +661,14 @@ PanelWindow {
|
||||
anchors.left: !isVertical ? true : (barPos === SettingsData.Position.Left)
|
||||
anchors.right: !isVertical ? true : (barPos === SettingsData.Position.Right)
|
||||
|
||||
exclusiveZone: (!(barConfig?.visible ?? true) || topBarCore.autoHide) ? -1 : (barWindow.effectiveBarThickness + effectiveSpacing + (barConfig?.bottomGap ?? 0))
|
||||
exclusiveZone: (!(barConfig?.visible ?? true) || topBarCore.autoHide) ? -1 : (barWindow.effectiveBarThickness + effectiveSpacing + (Theme.isConnectedEffect ? 0 : (barConfig?.bottomGap ?? 0)))
|
||||
|
||||
Item {
|
||||
id: inputMask
|
||||
|
||||
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,4 +1,5 @@
|
||||
import QtQuick
|
||||
import Quickshell.Services.UPower
|
||||
import qs.Common
|
||||
import qs.Modules.Plugins
|
||||
import qs.Services
|
||||
@@ -10,6 +11,8 @@ BasePill {
|
||||
property bool batteryPopupVisible: false
|
||||
property var popoutTarget: null
|
||||
|
||||
property real touchpadAccumulator: 0
|
||||
|
||||
readonly property int barPosition: {
|
||||
switch (axis?.edge) {
|
||||
case "top":
|
||||
@@ -119,5 +122,44 @@ 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: "88%"
|
||||
text: "100%"
|
||||
}
|
||||
|
||||
StyledTextMetrics {
|
||||
|
||||
@@ -17,7 +17,7 @@ BasePill {
|
||||
property int availableWidth: 400
|
||||
readonly property int maxNormalWidth: 456
|
||||
readonly property int maxCompactWidth: 288
|
||||
readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
|
||||
property Toplevel activeWindow: null
|
||||
property var activeDesktopEntry: null
|
||||
property bool isHovered: mouseArea.containsMouse
|
||||
property bool isAutoHideBar: false
|
||||
@@ -38,10 +38,44 @@ 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,7 +19,8 @@ BasePill {
|
||||
readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser
|
||||
property bool compactMode: false
|
||||
property var widgetData: null
|
||||
readonly property int textWidth: {
|
||||
readonly property bool adaptiveWidthEnabled: SettingsData.mediaAdaptiveWidthEnabled
|
||||
readonly property int maxTextWidth: {
|
||||
const size = widgetData?.mediaSize !== undefined ? widgetData.mediaSize : SettingsData.mediaSize;
|
||||
switch (size) {
|
||||
case 0:
|
||||
@@ -36,10 +37,7 @@ BasePill {
|
||||
if (isVerticalOrientation) {
|
||||
return widgetThickness - horizontalPadding * 2;
|
||||
}
|
||||
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);
|
||||
return 0;
|
||||
}
|
||||
readonly property int currentContentHeight: {
|
||||
if (!isVerticalOrientation) {
|
||||
@@ -99,7 +97,7 @@ BasePill {
|
||||
|
||||
if (isMouseWheelY) {
|
||||
if (deltaY > 0) {
|
||||
activePlayer.previous();
|
||||
MprisController.previousOrRewind();
|
||||
} else {
|
||||
activePlayer.next();
|
||||
}
|
||||
@@ -107,7 +105,7 @@ BasePill {
|
||||
scrollAccumulatorY += deltaY;
|
||||
if (Math.abs(scrollAccumulatorY) >= touchpadThreshold) {
|
||||
if (scrollAccumulatorY > 0) {
|
||||
activePlayer.previous();
|
||||
MprisController.previousOrRewind();
|
||||
} else {
|
||||
activePlayer.next();
|
||||
}
|
||||
@@ -119,7 +117,28 @@ BasePill {
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
implicitWidth: root.playerAvailable ? root.currentContentWidth : 0
|
||||
id: contentRoot
|
||||
readonly property real measuredTextWidth: {
|
||||
if (!root.playerAvailable || root.maxTextWidth <= 0 || !textContainer.visible)
|
||||
return 0;
|
||||
// Preserve the fixed-width text slot even if metadata is briefly empty.
|
||||
if (!root.adaptiveWidthEnabled)
|
||||
return root.maxTextWidth;
|
||||
if (textContainer.displayText.length === 0)
|
||||
return 0;
|
||||
const rawWidth = mediaText.contentWidth;
|
||||
if (!isFinite(rawWidth) || rawWidth <= 0)
|
||||
return 0;
|
||||
return Math.min(root.maxTextWidth, Math.ceil(rawWidth));
|
||||
}
|
||||
readonly property int horizontalContentWidth: {
|
||||
const controlsWidth = 20 + Theme.spacingXS + 24 + Theme.spacingXS + 20;
|
||||
const audioVizWidth = 20;
|
||||
const baseWidth = audioVizWidth + Theme.spacingXS + controlsWidth;
|
||||
return baseWidth + (measuredTextWidth > 0 ? measuredTextWidth + Theme.spacingXS : 0);
|
||||
}
|
||||
|
||||
implicitWidth: root.playerAvailable ? (root.isVerticalOrientation ? root.currentContentWidth : horizontalContentWidth) : 0
|
||||
implicitHeight: root.playerAvailable ? root.currentContentHeight : 0
|
||||
opacity: root.playerAvailable ? 1 : 0
|
||||
|
||||
@@ -132,8 +151,9 @@ BasePill {
|
||||
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +234,7 @@ BasePill {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
activePlayer.togglePlaying();
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
activePlayer.previous();
|
||||
MprisController.previousOrRewind();
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
activePlayer.next();
|
||||
}
|
||||
@@ -269,7 +289,7 @@ BasePill {
|
||||
}
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: textWidth
|
||||
width: contentRoot.measuredTextWidth
|
||||
height: root.widgetThickness
|
||||
visible: {
|
||||
const size = widgetData?.mediaSize !== undefined ? widgetData.mediaSize : SettingsData.mediaSize;
|
||||
@@ -278,50 +298,95 @@ BasePill {
|
||||
clip: true
|
||||
color: "transparent"
|
||||
|
||||
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();
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: scrollAnimation
|
||||
running: mediaText.needsScrolling && textContainer.visible
|
||||
loops: Animation.Infinite
|
||||
Item {
|
||||
id: textClip
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
PauseAnimation {
|
||||
duration: 2000
|
||||
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();
|
||||
}
|
||||
|
||||
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: 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
|
||||
}
|
||||
}
|
||||
|
||||
PauseAnimation {
|
||||
duration: 2000
|
||||
}
|
||||
SequentialAnimation {
|
||||
id: textChangeAnimation
|
||||
|
||||
NumberAnimation {
|
||||
target: mediaText
|
||||
property: "scrollOffset"
|
||||
to: 0
|
||||
duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60)
|
||||
easing.type: Easing.Linear
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -370,11 +435,7 @@ BasePill {
|
||||
anchors.fill: parent
|
||||
enabled: root.playerAvailable
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (activePlayer) {
|
||||
activePlayer.previous();
|
||||
}
|
||||
}
|
||||
onClicked: MprisController.previousOrRewind()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -271,7 +271,7 @@ BasePill {
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (isFocused) {
|
||||
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.2);
|
||||
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.45);
|
||||
}
|
||||
return mouseArea.containsMouse ? 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.2);
|
||||
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.45);
|
||||
}
|
||||
return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
|
||||
}
|
||||
|
||||
@@ -16,8 +16,11 @@ 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()) : [];
|
||||
@@ -40,6 +43,76 @@ 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");
|
||||
@@ -78,6 +151,13 @@ 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)
|
||||
@@ -103,6 +183,7 @@ 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
|
||||
|
||||
@@ -198,10 +279,11 @@ BasePill {
|
||||
id: rowComp
|
||||
Row {
|
||||
spacing: 0
|
||||
layoutDirection: root.reverseInlineHorizontal ? Qt.RightToLeft : Qt.LeftToRight
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.mainBarItems
|
||||
values: root.displayedMainBarItems
|
||||
objectProp: "key"
|
||||
}
|
||||
|
||||
@@ -209,29 +291,7 @@ BasePill {
|
||||
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 "";
|
||||
}
|
||||
property string iconSource: root.trayIconSourceFor(trayItem)
|
||||
|
||||
width: root.trayItemSize
|
||||
height: root.barThickness
|
||||
@@ -371,7 +431,8 @@ BasePill {
|
||||
}
|
||||
if (!delegateRoot.trayItem.hasMenu)
|
||||
return;
|
||||
root.menuOpen = false;
|
||||
if (root.useOverflowPopup)
|
||||
root.menuOpen = false;
|
||||
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
|
||||
}
|
||||
|
||||
@@ -380,8 +441,8 @@ BasePill {
|
||||
const distance = Math.abs(mouse.x - dragHandler.dragStartPos.x);
|
||||
if (distance > 5) {
|
||||
dragHandler.dragging = true;
|
||||
root.draggedIndex = index;
|
||||
root.dropTargetIndex = index;
|
||||
root.draggedIndex = root.reverseInlineHorizontal ? (root.mainBarItems.length - 1 - index) : index;
|
||||
root.dropTargetIndex = root.draggedIndex;
|
||||
}
|
||||
}
|
||||
if (!dragHandler.dragging)
|
||||
@@ -391,7 +452,8 @@ BasePill {
|
||||
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));
|
||||
const visualTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
|
||||
const newTargetIndex = root.reverseInlineHorizontal ? (root.mainBarItems.length - 1 - visualTargetIndex) : visualTargetIndex;
|
||||
if (newTargetIndex !== root.dropTargetIndex) {
|
||||
root.dropTargetIndex = newTargetIndex;
|
||||
}
|
||||
@@ -407,7 +469,8 @@ BasePill {
|
||||
root.callContextMenuFallback(delegateRoot.trayItem.id, Math.round(gp.x), Math.round(gp.y));
|
||||
return;
|
||||
}
|
||||
root.menuOpen = false;
|
||||
if (root.useOverflowPopup)
|
||||
root.menuOpen = false;
|
||||
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
|
||||
}
|
||||
}
|
||||
@@ -429,7 +492,7 @@ BasePill {
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: root.menuOpen ? "expand_less" : "expand_more"
|
||||
name: root.toggleIconName()
|
||||
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
color: Theme.widgetTextColor
|
||||
}
|
||||
@@ -451,6 +514,301 @@ BasePill {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.displayedInlineExpandedItems
|
||||
objectProp: "key"
|
||||
}
|
||||
|
||||
delegate: inlineExpandedTrayItemDelegate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: inlineExpandedTrayItemDelegate
|
||||
|
||||
Item {
|
||||
property var trayItem: modelData.item
|
||||
property string itemKey: modelData.key
|
||||
property string iconSource: root.trayIconSourceFor(trayItem)
|
||||
|
||||
width: root.isVerticalOrientation ? root.barThickness : (root.inlineExpanded ? root.trayItemSize : 0)
|
||||
height: root.isVerticalOrientation ? (root.inlineExpanded ? root.trayItemSize : 0) : root.barThickness
|
||||
visible: width > 0 || height > 0
|
||||
|
||||
Behavior on width {
|
||||
enabled: !root.isVerticalOrientation
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
enabled: root.isVerticalOrientation
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: inlineVisualContent
|
||||
width: root.trayItemSize
|
||||
height: root.trayItemSize
|
||||
x: root.isVerticalOrientation ? Math.round((parent.width - width) / 2) : (root.reverseInlineHorizontal ? parent.width - width : 0)
|
||||
y: root.isVerticalOrientation ? (root.reverseInlineVertical ? parent.height - height : 0) : Math.round((parent.height - height) / 2)
|
||||
radius: Theme.cornerRadius
|
||||
color: inlineTrayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||
opacity: root.inlineExpanded ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
IconImage {
|
||||
id: inlineIconImg
|
||||
anchors.centerIn: parent
|
||||
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
source: iconSource
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
mipmap: true
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: !inlineIconImg.visible
|
||||
text: {
|
||||
const itemId = trayItem?.id || "";
|
||||
if (!itemId)
|
||||
return "?";
|
||||
return itemId.charAt(0).toUpperCase();
|
||||
}
|
||||
font.pixelSize: 10
|
||||
color: Theme.widgetTextColor
|
||||
}
|
||||
|
||||
DankRipple {
|
||||
id: inlineItemRipple
|
||||
cornerRadius: Theme.cornerRadius
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: inlineTrayItemArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: root.inlineExpanded
|
||||
|
||||
onPressed: mouse => {
|
||||
const pos = mapToItem(inlineVisualContent, mouse.x, mouse.y);
|
||||
inlineItemRipple.trigger(pos.x, pos.y);
|
||||
}
|
||||
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
root.activateInlineTrayItem(trayItem, inlineVisualContent);
|
||||
return;
|
||||
}
|
||||
if (mouse.button !== Qt.RightButton)
|
||||
return;
|
||||
root.openInlineTrayContextMenu(trayItem, inlineTrayItemArea, mouse, inlineVisualContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: verticalMainTrayItemDelegate
|
||||
|
||||
Item {
|
||||
property var trayItem: modelData.item
|
||||
property string itemKey: modelData.key
|
||||
property string iconSource: root.trayIconSourceFor(trayItem)
|
||||
|
||||
width: root.barThickness
|
||||
height: root.trayItemSize
|
||||
z: dragHandler.dragging ? 100 : 0
|
||||
|
||||
property real shiftOffset: {
|
||||
if (root.draggedIndex < 0)
|
||||
return 0;
|
||||
if (index === root.draggedIndex)
|
||||
return 0;
|
||||
const dragIdx = root.draggedIndex;
|
||||
const dropIdx = root.dropTargetIndex;
|
||||
const shiftAmount = root.trayItemSize;
|
||||
if (dropIdx < 0)
|
||||
return 0;
|
||||
if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
|
||||
return -shiftAmount;
|
||||
if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx)
|
||||
return shiftAmount;
|
||||
return 0;
|
||||
}
|
||||
|
||||
transform: Translate {
|
||||
y: shiftOffset
|
||||
Behavior on y {
|
||||
enabled: !root.suppressShiftAnimation
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: dragHandler
|
||||
anchors.fill: parent
|
||||
property bool dragging: false
|
||||
property point dragStartPos: Qt.point(0, 0)
|
||||
property real dragAxisOffset: 0
|
||||
property bool longPressing: false
|
||||
|
||||
Timer {
|
||||
id: longPressTimer
|
||||
interval: 400
|
||||
repeat: false
|
||||
onTriggered: dragHandler.longPressing = true
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: visualContent
|
||||
width: root.trayItemSize
|
||||
height: root.trayItemSize
|
||||
anchors.centerIn: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: trayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||
border.width: dragHandler.dragging ? 2 : 0
|
||||
border.color: Theme.primary
|
||||
opacity: dragHandler.dragging ? 0.8 : 1.0
|
||||
|
||||
transform: Translate {
|
||||
y: dragHandler.dragging ? dragHandler.dragAxisOffset : 0
|
||||
}
|
||||
|
||||
IconImage {
|
||||
id: iconImg
|
||||
anchors.centerIn: parent
|
||||
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
source: iconSource
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
mipmap: true
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: !iconImg.visible
|
||||
text: {
|
||||
const itemId = trayItem?.id || "";
|
||||
if (!itemId)
|
||||
return "?";
|
||||
return itemId.charAt(0).toUpperCase();
|
||||
}
|
||||
font.pixelSize: 10
|
||||
color: Theme.widgetTextColor
|
||||
}
|
||||
|
||||
DankRipple {
|
||||
id: itemRipple
|
||||
cornerRadius: Theme.cornerRadius
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: trayItemArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
cursorShape: dragHandler.longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
|
||||
|
||||
onPressed: mouse => {
|
||||
const pos = mapToItem(visualContent, mouse.x, mouse.y);
|
||||
itemRipple.trigger(pos.x, pos.y);
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
dragHandler.dragStartPos = Qt.point(mouse.x, mouse.y);
|
||||
longPressTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
onReleased: mouse => {
|
||||
longPressTimer.stop();
|
||||
const wasDragging = dragHandler.dragging;
|
||||
const didReorder = wasDragging && root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex;
|
||||
|
||||
if (didReorder) {
|
||||
root.suppressShiftAnimation = true;
|
||||
root.moveTrayItemInFullOrder(root.draggedIndex, root.dropTargetIndex);
|
||||
Qt.callLater(() => root.suppressShiftAnimation = false);
|
||||
}
|
||||
|
||||
dragHandler.longPressing = false;
|
||||
dragHandler.dragging = false;
|
||||
dragHandler.dragAxisOffset = 0;
|
||||
root.draggedIndex = -1;
|
||||
root.dropTargetIndex = -1;
|
||||
|
||||
if (wasDragging || mouse.button !== Qt.LeftButton)
|
||||
return;
|
||||
|
||||
if (!trayItem)
|
||||
return;
|
||||
if (!trayItem.onlyMenu) {
|
||||
trayItem.activate();
|
||||
return;
|
||||
}
|
||||
if (!trayItem.hasMenu)
|
||||
return;
|
||||
if (root.useOverflowPopup)
|
||||
root.menuOpen = false;
|
||||
root.showForTrayItem(trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
|
||||
}
|
||||
|
||||
onPositionChanged: mouse => {
|
||||
if (dragHandler.longPressing && !dragHandler.dragging) {
|
||||
const distance = Math.abs(mouse.y - dragHandler.dragStartPos.y);
|
||||
if (distance > 5) {
|
||||
dragHandler.dragging = true;
|
||||
root.draggedIndex = index;
|
||||
root.dropTargetIndex = root.draggedIndex;
|
||||
}
|
||||
}
|
||||
if (!dragHandler.dragging)
|
||||
return;
|
||||
|
||||
const axisOffset = mouse.y - dragHandler.dragStartPos.y;
|
||||
dragHandler.dragAxisOffset = axisOffset;
|
||||
const itemSize = root.trayItemSize;
|
||||
const slotOffset = Math.round(axisOffset / itemSize);
|
||||
const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
|
||||
if (newTargetIndex !== root.dropTargetIndex) {
|
||||
root.dropTargetIndex = newTargetIndex;
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: mouse => {
|
||||
if (dragHandler.dragging)
|
||||
return;
|
||||
if (mouse.button !== Qt.RightButton)
|
||||
return;
|
||||
root.openInlineTrayContextMenu(trayItem, trayItemArea, mouse, visualContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,219 +817,23 @@ BasePill {
|
||||
Column {
|
||||
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.mainBarItems
|
||||
values: root.reverseInlineVertical ? [] : root.displayedMainBarItems
|
||||
objectProp: "key"
|
||||
}
|
||||
delegate: verticalMainTrayItemDelegate
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.reverseInlineVertical ? root.displayedInlineExpandedItems : []
|
||||
objectProp: "key"
|
||||
}
|
||||
delegate: inlineExpandedTrayItemDelegate
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -689,14 +851,7 @@ BasePill {
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: {
|
||||
const edge = root.axis?.edge;
|
||||
if (edge === "left") {
|
||||
return root.menuOpen ? "chevron_left" : "chevron_right";
|
||||
} else {
|
||||
return root.menuOpen ? "chevron_right" : "chevron_left";
|
||||
}
|
||||
}
|
||||
name: root.toggleIconName()
|
||||
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
color: Theme.widgetTextColor
|
||||
}
|
||||
@@ -718,6 +873,22 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -733,7 +904,7 @@ BasePill {
|
||||
blurRadius: Theme.cornerRadius
|
||||
}
|
||||
|
||||
visible: root.menuOpen
|
||||
visible: root.useOverflowPopup && root.menuOpen
|
||||
screen: root.parentScreen
|
||||
WlrLayershell.layer: WlrLayershell.Top
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
@@ -749,13 +920,14 @@ BasePill {
|
||||
|
||||
HyprlandFocusGrab {
|
||||
windows: [overflowMenu]
|
||||
active: CompositorService.useHyprlandFocusGrab && root.menuOpen
|
||||
active: CompositorService.useHyprlandFocusGrab && root.useOverflowPopup && root.menuOpen
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: PopoutManager
|
||||
function onPopoutOpening() {
|
||||
root.menuOpen = false;
|
||||
if (root.useOverflowPopup)
|
||||
root.menuOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1021,30 +1193,7 @@ BasePill {
|
||||
|
||||
delegate: Rectangle {
|
||||
property var trayItem: modelData
|
||||
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 "";
|
||||
}
|
||||
property string iconSource: root.trayIconSourceFor(trayItem)
|
||||
|
||||
width: root.trayItemSize + 4
|
||||
height: root.trayItemSize + 4
|
||||
@@ -1313,7 +1462,8 @@ BasePill {
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
updatePosition();
|
||||
root.menuOpen = false;
|
||||
if (root.useOverflowPopup)
|
||||
root.menuOpen = false;
|
||||
PopoutManager.closeAllPopouts();
|
||||
ModalManager.closeAllModalsExcept(null);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ DankPopout {
|
||||
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
|
||||
triggerWidth: 80
|
||||
screen: triggerScreen
|
||||
shouldBeVisible: dashVisible
|
||||
|
||||
property bool __focusArmed: false
|
||||
property bool __contentReady: false
|
||||
@@ -100,7 +99,7 @@ DankPopout {
|
||||
if (currentPlayer && currentPlayer !== player && currentPlayer.canPause) {
|
||||
currentPlayer.pause();
|
||||
}
|
||||
MprisController.activePlayer = player;
|
||||
MprisController.setActivePlayer(player);
|
||||
root.__hideDropdowns();
|
||||
}
|
||||
onDeviceSelected: device => {
|
||||
|
||||
@@ -44,6 +44,43 @@ Item {
|
||||
|
||||
property int __volumeHoverCount: 0
|
||||
|
||||
readonly property bool directionalEffect: Theme.isDirectionalEffect
|
||||
readonly property bool depthEffect: Theme.isDepthEffect
|
||||
|
||||
function panelMotionX(panelWidth, active) {
|
||||
if (active)
|
||||
return 0;
|
||||
if (directionalEffect) {
|
||||
const travel = Math.max(Theme.effectAnimOffset, panelWidth * 0.85);
|
||||
return isRightEdge ? -travel : travel;
|
||||
}
|
||||
if (depthEffect) {
|
||||
const travel = Math.max(Theme.effectAnimOffset * 0.7, panelWidth * 0.32);
|
||||
return isRightEdge ? -travel : travel;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function panelMotionY(panelType, panelHeight, active) {
|
||||
if (active)
|
||||
return 0;
|
||||
if (directionalEffect) {
|
||||
if (panelType === 2)
|
||||
return panelHeight * 0.08;
|
||||
if (panelType === 3)
|
||||
return -panelHeight * 0.08;
|
||||
return 0;
|
||||
}
|
||||
if (depthEffect) {
|
||||
if (panelType === 2)
|
||||
return panelHeight * 0.04;
|
||||
if (panelType === 3)
|
||||
return -panelHeight * 0.04;
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function volumeAreaEntered() {
|
||||
__volumeHoverCount++;
|
||||
panelEntered();
|
||||
@@ -62,30 +99,47 @@ Item {
|
||||
visible: dropdownType === 1 && volumeAvailable
|
||||
width: 60
|
||||
height: 180
|
||||
x: isRightEdge ? anchorPos.x : anchorPos.x - width
|
||||
y: anchorPos.y - height / 2
|
||||
x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 1)
|
||||
y: anchorPos.y - height / 2 + panelMotionY(1, height, dropdownType === 1)
|
||||
radius: Theme.cornerRadius * 2
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||
border.width: 1
|
||||
|
||||
opacity: dropdownType === 1 ? 1 : 0
|
||||
scale: dropdownType === 1 ? 1 : 0.96
|
||||
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : 0)
|
||||
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : Theme.effectScaleCollapsed)
|
||||
transformOrigin: isRightEdge ? Item.Left : Item.Right
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
enabled: !Theme.isDirectionalEffect
|
||||
DankAnim {
|
||||
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1) * Theme.variantOpacityDurationScale)
|
||||
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
enabled: !Theme.isDirectionalEffect
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,33 +251,50 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
id: audioDevicesPanel
|
||||
visible: dropdownType === 2
|
||||
visible: dropdownType === 2 && activePlayer !== null
|
||||
width: 280
|
||||
height: Math.max(200, Math.min(280, availableDevices.length * 50 + 100))
|
||||
x: isRightEdge ? anchorPos.x : anchorPos.x - width
|
||||
y: anchorPos.y - height / 2
|
||||
x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 2)
|
||||
y: anchorPos.y - height / 2 + panelMotionY(2, height, dropdownType === 2)
|
||||
radius: Theme.cornerRadius * 2
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
|
||||
border.width: 2
|
||||
|
||||
opacity: dropdownType === 2 ? 1 : 0
|
||||
scale: dropdownType === 2 ? 1 : 0.96
|
||||
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : 0)
|
||||
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : Theme.effectScaleCollapsed)
|
||||
transformOrigin: isRightEdge ? Item.Left : Item.Right
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
enabled: !Theme.isDirectionalEffect
|
||||
DankAnim {
|
||||
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2) * Theme.variantOpacityDurationScale)
|
||||
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
enabled: !Theme.isDirectionalEffect
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,30 +425,47 @@ Item {
|
||||
visible: dropdownType === 3
|
||||
width: 240
|
||||
height: Math.max(180, Math.min(240, (allPlayers?.length || 0) * 50 + 80))
|
||||
x: isRightEdge ? anchorPos.x : anchorPos.x - width
|
||||
y: anchorPos.y - height / 2
|
||||
x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 3)
|
||||
y: anchorPos.y - height / 2 + panelMotionY(3, height, dropdownType === 3)
|
||||
radius: Theme.cornerRadius * 2
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
|
||||
border.width: 2
|
||||
|
||||
opacity: dropdownType === 3 ? 1 : 0
|
||||
scale: dropdownType === 3 ? 1 : 0.96
|
||||
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : 0)
|
||||
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : Theme.effectScaleCollapsed)
|
||||
transformOrigin: isRightEdge ? Item.Left : Item.Right
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
enabled: !Theme.isDirectionalEffect
|
||||
DankAnim {
|
||||
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3) * Theme.variantOpacityDurationScale)
|
||||
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
enabled: !Theme.isDirectionalEffect
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -487,17 +487,7 @@ Item {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (!activePlayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activePlayer.position > 8 && activePlayer.canSeek) {
|
||||
activePlayer.position = 0;
|
||||
} else {
|
||||
activePlayer.previous();
|
||||
}
|
||||
}
|
||||
onClicked: MprisController.previousOrRewind()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,14 +145,7 @@ Card {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (!activePlayer) return
|
||||
if (activePlayer.position > 8 && activePlayer.canSeek) {
|
||||
activePlayer.position = 0
|
||||
} else {
|
||||
activePlayer.previous()
|
||||
}
|
||||
}
|
||||
onClicked: MprisController.previousOrRewind()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,11 +19,12 @@ Variants {
|
||||
|
||||
WindowBlur {
|
||||
targetWindow: dock
|
||||
blurEnabled: dock.effectiveBlurEnabled && !SettingsData.connectedFrameModeActive
|
||||
blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x
|
||||
blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y
|
||||
blurWidth: dock.hasApps && dock.reveal ? dockBackground.width : 0
|
||||
blurHeight: dock.hasApps && dock.reveal ? dockBackground.height : 0
|
||||
blurRadius: Theme.cornerRadius
|
||||
blurRadius: Theme.isConnectedEffect ? Theme.connectedCornerRadius : dock.surfaceRadius
|
||||
}
|
||||
|
||||
WlrLayershell.namespace: "dms:dock"
|
||||
@@ -42,6 +43,23 @@ Variants {
|
||||
property real backgroundTransparency: SettingsData.dockTransparency
|
||||
property bool groupByApp: SettingsData.dockGroupByApp
|
||||
readonly property int borderThickness: SettingsData.dockBorderEnabled ? SettingsData.dockBorderThickness : 0
|
||||
readonly property string connectedBarSide: SettingsData.dockPosition === SettingsData.Position.Top ? "top" : SettingsData.dockPosition === SettingsData.Position.Bottom ? "bottom" : SettingsData.dockPosition === SettingsData.Position.Left ? "left" : "right"
|
||||
readonly property bool connectedBarActiveOnEdge: Theme.isConnectedEffect && !!(dock.screen || modelData) && SettingsData.getActiveBarEdgesForScreen(dock.screen || modelData).includes(connectedBarSide)
|
||||
readonly property real connectedJoinInset: {
|
||||
if (!Theme.isConnectedEffect)
|
||||
return 0;
|
||||
return connectedBarActiveOnEdge ? SettingsData.frameBarSize : SettingsData.frameThickness;
|
||||
}
|
||||
readonly property real surfaceRadius: Theme.connectedSurfaceRadius
|
||||
readonly property color surfaceColor: Theme.isConnectedEffect ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency)
|
||||
readonly property color surfaceBorderColor: Theme.isConnectedEffect ? "transparent" : BlurService.borderColor
|
||||
readonly property real surfaceBorderWidth: Theme.isConnectedEffect ? 0 : BlurService.borderWidth
|
||||
readonly property real surfaceTopLeftRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius
|
||||
readonly property real surfaceTopRightRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius
|
||||
readonly property real surfaceBottomLeftRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius
|
||||
readonly property real surfaceBottomRightRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius
|
||||
readonly property real horizontalConnectorExtent: Theme.isConnectedEffect && !isVertical ? Theme.connectedCornerRadius : 0
|
||||
readonly property real verticalConnectorExtent: Theme.isConnectedEffect && isVertical ? Theme.connectedCornerRadius : 0
|
||||
|
||||
readonly property int hasApps: dockApps.implicitWidth > 0 || dockApps.implicitHeight > 0
|
||||
|
||||
@@ -113,13 +131,102 @@ Variants {
|
||||
return getBarHeight(leftBar);
|
||||
}
|
||||
|
||||
readonly property real dockMargin: SettingsData.dockSpacing
|
||||
readonly property real positionSpacing: barSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin
|
||||
readonly property real dockMargin: SettingsData.dockMargin
|
||||
readonly property bool effectiveBlurEnabled: Theme.connectedSurfaceBlurEnabled
|
||||
readonly property real effectiveDockBottomGap: Theme.isConnectedEffect ? 0 : SettingsData.dockBottomGap
|
||||
readonly property real effectiveDockMargin: Theme.isConnectedEffect ? 0 : SettingsData.dockMargin
|
||||
readonly property real positionSpacing: barSpacing + effectiveDockBottomGap + effectiveDockMargin
|
||||
readonly property real joinedEdgeMargin: Theme.isConnectedEffect ? 0 : (barSpacing + effectiveDockMargin + 1 + dock.borderThickness)
|
||||
readonly property real _dpr: (dock.screen && dock.screen.devicePixelRatio) ? dock.screen.devicePixelRatio : 1
|
||||
function px(v) {
|
||||
return Math.round(v * _dpr) / _dpr;
|
||||
}
|
||||
|
||||
function connectorWidth(spacing) {
|
||||
return dock.isVertical ? (spacing + Theme.connectedCornerRadius) : Theme.connectedCornerRadius;
|
||||
}
|
||||
|
||||
function connectorHeight(spacing) {
|
||||
return dock.isVertical ? Theme.connectedCornerRadius : (spacing + Theme.connectedCornerRadius);
|
||||
}
|
||||
|
||||
function connectorSeamX(baseX, bodyWidth, placement) {
|
||||
if (!dock.isVertical)
|
||||
return placement === "left" ? baseX : baseX + bodyWidth;
|
||||
return SettingsData.dockPosition === SettingsData.Position.Left ? baseX : baseX + bodyWidth;
|
||||
}
|
||||
|
||||
function connectorSeamY(baseY, bodyHeight, placement) {
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Top)
|
||||
return baseY;
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Bottom)
|
||||
return baseY + bodyHeight;
|
||||
return placement === "left" ? baseY : baseY + bodyHeight;
|
||||
}
|
||||
|
||||
function connectorX(baseX, bodyWidth, placement, spacing) {
|
||||
const seamX = connectorSeamX(baseX, bodyWidth, placement);
|
||||
const width = connectorWidth(spacing);
|
||||
if (!dock.isVertical)
|
||||
return placement === "left" ? seamX - width : seamX;
|
||||
return SettingsData.dockPosition === SettingsData.Position.Left ? seamX : seamX - width;
|
||||
}
|
||||
|
||||
function connectorY(baseY, bodyHeight, placement, spacing) {
|
||||
const seamY = connectorSeamY(baseY, bodyHeight, placement);
|
||||
const height = connectorHeight(spacing);
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Top)
|
||||
return seamY;
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Bottom)
|
||||
return seamY - height;
|
||||
return placement === "left" ? seamY - height : seamY;
|
||||
}
|
||||
|
||||
// ─── ConnectedModeState sync ────────────────────────────────────────
|
||||
// Dock window origin in screen-relative coordinates (FrameWindow space).
|
||||
function _dockWindowOriginX() {
|
||||
if (!dock.isVertical)
|
||||
return 0;
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Right)
|
||||
return (dock.screen ? dock.screen.width : 0) - dock.width;
|
||||
return 0;
|
||||
}
|
||||
function _dockWindowOriginY() {
|
||||
if (dock.isVertical)
|
||||
return 0;
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Bottom)
|
||||
return (dock.screen ? dock.screen.height : 0) - dock.height;
|
||||
return 0;
|
||||
}
|
||||
|
||||
readonly property string _dockScreenName: dock.modelData ? dock.modelData.name : (dock.screen ? dock.screen.name : "")
|
||||
|
||||
function _syncDockChromeState() {
|
||||
if (!dock._dockScreenName)
|
||||
return;
|
||||
if (!SettingsData.connectedFrameModeActive) {
|
||||
ConnectedModeState.clearDockState(dock._dockScreenName);
|
||||
return;
|
||||
}
|
||||
|
||||
ConnectedModeState.setDockState(dock._dockScreenName, {
|
||||
"reveal": dock.visible && (dock.reveal || slideXAnimation.running || slideYAnimation.running) && dock.hasApps,
|
||||
"barSide": dock.connectedBarSide,
|
||||
"bodyX": dock._dockWindowOriginX() + dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x,
|
||||
"bodyY": dock._dockWindowOriginY() + dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y,
|
||||
"bodyW": dock.hasApps ? dockBackground.width : 0,
|
||||
"bodyH": dock.hasApps ? dockBackground.height : 0,
|
||||
"slideX": dockSlide.x,
|
||||
"slideY": dockSlide.y
|
||||
});
|
||||
}
|
||||
|
||||
function _syncDockSlide() {
|
||||
if (!dock._dockScreenName || !SettingsData.connectedFrameModeActive)
|
||||
return;
|
||||
ConnectedModeState.setDockSlide(dock._dockScreenName, dockSlide.x, dockSlide.y);
|
||||
}
|
||||
|
||||
property bool contextMenuOpen: (dockVariants.contextMenu && dockVariants.contextMenu.visible && dockVariants.contextMenu.screen === modelData)
|
||||
property bool revealSticky: false
|
||||
|
||||
@@ -130,7 +237,7 @@ Variants {
|
||||
return false;
|
||||
|
||||
const screenName = dock.modelData?.name ?? "";
|
||||
const dockThickness = effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin;
|
||||
const dockThickness = dock.connectedJoinInset + effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin;
|
||||
const screenWidth = dock.screen?.width ?? 0;
|
||||
const screenHeight = dock.screen?.height ?? 0;
|
||||
|
||||
@@ -281,6 +388,23 @@ Variants {
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: Qt.callLater(() => dock._syncDockChromeState())
|
||||
Component.onDestruction: ConnectedModeState.clearDockState(dock._dockScreenName)
|
||||
|
||||
onRevealChanged: dock._syncDockChromeState()
|
||||
onWidthChanged: dock._syncDockChromeState()
|
||||
onHeightChanged: dock._syncDockChromeState()
|
||||
onVisibleChanged: dock._syncDockChromeState()
|
||||
onHasAppsChanged: dock._syncDockChromeState()
|
||||
onConnectedBarSideChanged: dock._syncDockChromeState()
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onConnectedFrameModeActiveChanged() {
|
||||
dock._syncDockChromeState();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onDockTransparencyChanged() {
|
||||
@@ -302,13 +426,13 @@ Variants {
|
||||
return -1;
|
||||
if (barSpacing > 0)
|
||||
return -1;
|
||||
return px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin);
|
||||
return px(connectedJoinInset + effectiveBarHeight + SettingsData.dockSpacing + effectiveDockBottomGap + effectiveDockMargin);
|
||||
}
|
||||
|
||||
property real animationHeadroom: Math.ceil(SettingsData.dockIconSize * 0.35)
|
||||
|
||||
implicitWidth: isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
|
||||
implicitHeight: !isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
|
||||
implicitWidth: isVertical ? (px(connectedJoinInset + effectiveBarHeight + SettingsData.dockSpacing + effectiveDockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
|
||||
implicitHeight: !isVertical ? (px(connectedJoinInset + effectiveBarHeight + SettingsData.dockSpacing + effectiveDockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
|
||||
|
||||
Item {
|
||||
id: maskItem
|
||||
@@ -318,17 +442,17 @@ Variants {
|
||||
x: {
|
||||
const baseX = dockCore.x + dockMouseArea.x;
|
||||
if (isVertical && SettingsData.dockPosition === SettingsData.Position.Right)
|
||||
return baseX - (expanded ? animationHeadroom + borderThickness : 0);
|
||||
return baseX - (expanded ? borderThickness : 0);
|
||||
return baseX - (expanded ? animationHeadroom + borderThickness + dock.horizontalConnectorExtent : 0);
|
||||
return baseX - (expanded ? borderThickness + dock.horizontalConnectorExtent : 0);
|
||||
}
|
||||
y: {
|
||||
const baseY = dockCore.y + dockMouseArea.y;
|
||||
if (!isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom)
|
||||
return baseY - (expanded ? animationHeadroom + borderThickness : 0);
|
||||
return baseY - (expanded ? borderThickness : 0);
|
||||
return baseY - (expanded ? animationHeadroom + borderThickness + dock.verticalConnectorExtent : 0);
|
||||
return baseY - (expanded ? borderThickness + dock.verticalConnectorExtent : 0);
|
||||
}
|
||||
width: dockMouseArea.width + (isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 : 0)
|
||||
height: dockMouseArea.height + (!isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 : 0)
|
||||
width: dockMouseArea.width + (isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.horizontalConnectorExtent * 2 : 0)
|
||||
height: dockMouseArea.height + (!isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.verticalConnectorExtent * 2 : 0)
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
@@ -388,7 +512,7 @@ Variants {
|
||||
const screenHeight = dock.screen ? dock.screen.height : 0;
|
||||
|
||||
const gap = Theme.spacingS;
|
||||
const bgMargin = barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness;
|
||||
const bgMargin = dock.joinedEdgeMargin + dock.connectedJoinInset;
|
||||
const btnW = dock.hoveredButton.width;
|
||||
const btnH = dock.hoveredButton.height;
|
||||
|
||||
@@ -459,11 +583,11 @@ Variants {
|
||||
// Keep the taller hit area regardless of the reveal state to prevent shrinking loop
|
||||
return Math.min(Math.max(dockBackground.height + 64, 200), maxDockHeight);
|
||||
}
|
||||
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1;
|
||||
return dock.reveal ? px(dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin) : 1;
|
||||
}
|
||||
width: {
|
||||
if (dock.isVertical) {
|
||||
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1;
|
||||
return dock.reveal ? px(dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin) : 1;
|
||||
}
|
||||
// Keep the wider hit area regardless of the reveal state to prevent shrinking loop
|
||||
return Math.min(dockBackground.width + 8 + dock.borderThickness, maxDockWidth);
|
||||
@@ -505,7 +629,11 @@ Variants {
|
||||
return 0;
|
||||
if (dock.reveal)
|
||||
return 0;
|
||||
const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + 10;
|
||||
if (Theme.isConnectedEffect) {
|
||||
const retractDist = dockBackground.width + SettingsData.dockSpacing + 10;
|
||||
return SettingsData.dockPosition === SettingsData.Position.Right ? retractDist : -retractDist;
|
||||
}
|
||||
const hideDistance = dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin + 10;
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Right) {
|
||||
return hideDistance;
|
||||
} else {
|
||||
@@ -517,7 +645,11 @@ Variants {
|
||||
return 0;
|
||||
if (dock.reveal)
|
||||
return 0;
|
||||
const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + 10;
|
||||
if (Theme.isConnectedEffect) {
|
||||
const retractDist = dockBackground.height + SettingsData.dockSpacing + 10;
|
||||
return SettingsData.dockPosition === SettingsData.Position.Bottom ? retractDist : -retractDist;
|
||||
}
|
||||
const hideDistance = dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin + 10;
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Bottom) {
|
||||
return hideDistance;
|
||||
} else {
|
||||
@@ -528,18 +660,29 @@ Variants {
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
id: slideXAnimation
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
duration: Theme.isConnectedEffect ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration
|
||||
easing.type: Theme.isConnectedEffect ? Easing.BezierSpline : Easing.OutCubic
|
||||
easing.bezierCurve: Theme.isConnectedEffect ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : []
|
||||
onRunningChanged: if (!running) dock._syncDockChromeState()
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
id: slideYAnimation
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
duration: Theme.isConnectedEffect ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration
|
||||
easing.type: Theme.isConnectedEffect ? Easing.BezierSpline : Easing.OutCubic
|
||||
easing.bezierCurve: Theme.isConnectedEffect ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : []
|
||||
onRunningChanged: if (!running) dock._syncDockChromeState()
|
||||
}
|
||||
}
|
||||
|
||||
onXChanged: {
|
||||
dock._syncDockSlide();
|
||||
}
|
||||
onYChanged: {
|
||||
dock._syncDockSlide();
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -553,33 +696,72 @@ Variants {
|
||||
right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined
|
||||
verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
|
||||
}
|
||||
anchors.topMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Top ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0
|
||||
anchors.bottomMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0
|
||||
anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0
|
||||
anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0
|
||||
anchors.topMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Top ? (dock.connectedJoinInset + dock.joinedEdgeMargin) : 0
|
||||
anchors.bottomMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? (dock.connectedJoinInset + dock.joinedEdgeMargin) : 0
|
||||
anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? (dock.connectedJoinInset + dock.joinedEdgeMargin) : 0
|
||||
anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? (dock.connectedJoinInset + dock.joinedEdgeMargin) : 0
|
||||
|
||||
implicitWidth: dock.isVertical ? (dockApps.implicitHeight + SettingsData.dockSpacing * 2) : (dockApps.implicitWidth + SettingsData.dockSpacing * 2)
|
||||
implicitHeight: dock.isVertical ? (dockApps.implicitWidth + SettingsData.dockSpacing * 2) : (dockApps.implicitHeight + SettingsData.dockSpacing * 2)
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
|
||||
layer.enabled: true
|
||||
// Avoid an offscreen texture seam where the connected dock meets the frame.
|
||||
layer.enabled: !Theme.isConnectedEffect
|
||||
clip: false
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
visible: !SettingsData.connectedFrameModeActive
|
||||
color: dock.surfaceColor
|
||||
topLeftRadius: dock.surfaceTopLeftRadius
|
||||
topRightRadius: dock.surfaceTopRightRadius
|
||||
bottomLeftRadius: dock.surfaceBottomLeftRadius
|
||||
bottomRightRadius: dock.surfaceBottomRightRadius
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: !SettingsData.connectedFrameModeActive
|
||||
color: "transparent"
|
||||
radius: Theme.cornerRadius
|
||||
border.color: BlurService.borderColor
|
||||
border.width: BlurService.borderWidth
|
||||
topLeftRadius: dock.surfaceTopLeftRadius
|
||||
topRightRadius: dock.surfaceTopRightRadius
|
||||
bottomLeftRadius: dock.surfaceBottomLeftRadius
|
||||
bottomRightRadius: dock.surfaceBottomRightRadius
|
||||
border.color: dock.surfaceBorderColor
|
||||
border.width: dock.surfaceBorderWidth
|
||||
z: 100
|
||||
}
|
||||
|
||||
// Sync dockBackground geometry to ConnectedModeState
|
||||
onXChanged: dock._syncDockChromeState()
|
||||
onYChanged: dock._syncDockChromeState()
|
||||
onWidthChanged: dock._syncDockChromeState()
|
||||
onHeightChanged: dock._syncDockChromeState()
|
||||
}
|
||||
|
||||
ConnectedCorner {
|
||||
visible: Theme.isConnectedEffect && dock.reveal && !SettingsData.connectedFrameModeActive
|
||||
barSide: dock.connectedBarSide
|
||||
placement: "left"
|
||||
spacing: 0
|
||||
connectorRadius: Theme.connectedCornerRadius
|
||||
color: dock.surfaceColor
|
||||
dpr: dock._dpr
|
||||
x: Theme.snap(dock.connectorX(dockBackground.x, dockBackground.width, placement, spacing), dock._dpr)
|
||||
y: Theme.snap(dock.connectorY(dockBackground.y, dockBackground.height, placement, spacing), dock._dpr)
|
||||
}
|
||||
|
||||
ConnectedCorner {
|
||||
visible: Theme.isConnectedEffect && dock.reveal && !SettingsData.connectedFrameModeActive
|
||||
barSide: dock.connectedBarSide
|
||||
placement: "right"
|
||||
spacing: 0
|
||||
connectorRadius: Theme.connectedCornerRadius
|
||||
color: dock.surfaceColor
|
||||
dpr: dock._dpr
|
||||
x: Theme.snap(dock.connectorX(dockBackground.x, dockBackground.width, placement, spacing), dock._dpr)
|
||||
y: Theme.snap(dock.connectorY(dockBackground.y, dockBackground.height, placement, spacing), dock._dpr)
|
||||
}
|
||||
|
||||
Shape {
|
||||
@@ -588,12 +770,12 @@ Variants {
|
||||
y: dockBackground.y - borderThickness
|
||||
width: dockBackground.width + borderThickness * 2
|
||||
height: dockBackground.height + borderThickness * 2
|
||||
visible: SettingsData.dockBorderEnabled && dock.hasApps
|
||||
visible: SettingsData.dockBorderEnabled && dock.hasApps && !Theme.isConnectedEffect
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
readonly property real borderThickness: Math.max(1, dock.borderThickness)
|
||||
readonly property real i: borderThickness / 2
|
||||
readonly property real cr: Theme.cornerRadius
|
||||
readonly property real cr: dock.surfaceRadius
|
||||
readonly property real w: dockBackground.width
|
||||
readonly property real h: dockBackground.height
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
54
quickshell/Modules/Frame/FrameBorder.qml
Normal file
54
quickshell/Modules/Frame/FrameBorder.qml
Normal file
@@ -0,0 +1,54 @@
|
||||
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
|
||||
property color borderColor: Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity)
|
||||
|
||||
Rectangle {
|
||||
id: borderRect
|
||||
|
||||
anchors.fill: parent
|
||||
// Bake frameOpacity into the color alpha rather than using the `opacity` property
|
||||
color: root.borderColor
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
698
quickshell/Modules/Frame/FrameWindow.qml
Normal file
698
quickshell/Modules/Frame/FrameWindow.qml
Normal file
@@ -0,0 +1,698 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
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"
|
||||
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)
|
||||
readonly property string _screenName: win.screen ? win.screen.name : ""
|
||||
readonly property var _dockState: ConnectedModeState.dockStates[win._screenName] || ConnectedModeState.emptyDockState
|
||||
readonly property var _dockSlide: ConnectedModeState.dockSlides[win._screenName] || ({
|
||||
"x": 0,
|
||||
"y": 0
|
||||
})
|
||||
readonly property var _notifState: ConnectedModeState.notificationStates[win._screenName] || ConnectedModeState.emptyNotificationState
|
||||
|
||||
// ─── Connected chrome convenience properties ──────────────────────────────
|
||||
readonly property bool _connectedActive: win._frameActive && SettingsData.connectedFrameModeActive
|
||||
readonly property string _barSide: {
|
||||
const edges = win.barEdges;
|
||||
if (edges.includes("top"))
|
||||
return "top";
|
||||
if (edges.includes("bottom"))
|
||||
return "bottom";
|
||||
if (edges.includes("left"))
|
||||
return "left";
|
||||
return "right";
|
||||
}
|
||||
readonly property real _ccr: Theme.connectedCornerRadius
|
||||
readonly property real _effectivePopoutCcr: {
|
||||
const extent = win._popoutArcExtent();
|
||||
const isHoriz = ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom";
|
||||
const crossSize = isHoriz ? _popoutBodyBlurAnchor.width : _popoutBodyBlurAnchor.height;
|
||||
return Math.max(0, Math.min(win._ccr, extent, crossSize / 2));
|
||||
}
|
||||
readonly property color _surfaceColor: Theme.connectedSurfaceColor
|
||||
readonly property real _surfaceOpacity: _surfaceColor.a
|
||||
readonly property color _opaqueSurfaceColor: Qt.rgba(_surfaceColor.r, _surfaceColor.g, _surfaceColor.b, 1)
|
||||
readonly property real _surfaceRadius: Theme.connectedSurfaceRadius
|
||||
readonly property real _seamOverlap: Theme.hairline(win._dpr)
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
// Invisible items providing scene coordinates for blur Region anchors
|
||||
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)
|
||||
}
|
||||
|
||||
Item {
|
||||
id: _popoutBodyBlurAnchor
|
||||
visible: false
|
||||
|
||||
readonly property bool _active: ConnectedModeState.popoutVisible && ConnectedModeState.popoutScreen === win._screenName
|
||||
|
||||
readonly property real _dyClamp: (ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom") ? Math.max(-ConnectedModeState.popoutBodyH, Math.min(ConnectedModeState.popoutAnimY * 1.02, ConnectedModeState.popoutBodyH)) : 0
|
||||
readonly property real _dxClamp: (ConnectedModeState.popoutBarSide === "left" || ConnectedModeState.popoutBarSide === "right") ? Math.max(-ConnectedModeState.popoutBodyW, Math.min(ConnectedModeState.popoutAnimX * 1.02, ConnectedModeState.popoutBodyW)) : 0
|
||||
|
||||
x: _active ? ConnectedModeState.popoutBodyX + (ConnectedModeState.popoutBarSide === "right" ? _dxClamp : 0) : 0
|
||||
y: _active ? ConnectedModeState.popoutBodyY + (ConnectedModeState.popoutBarSide === "bottom" ? _dyClamp : 0) : 0
|
||||
width: _active ? Math.max(0, ConnectedModeState.popoutBodyW - Math.abs(_dxClamp)) : 0
|
||||
height: _active ? Math.max(0, ConnectedModeState.popoutBodyH - Math.abs(_dyClamp)) : 0
|
||||
}
|
||||
|
||||
Item {
|
||||
id: _dockBodyBlurAnchor
|
||||
visible: false
|
||||
|
||||
readonly property bool _active: win._dockState.reveal && win._dockState.bodyW > 0 && win._dockState.bodyH > 0
|
||||
|
||||
x: _active ? win._dockState.bodyX + (win._dockSlide.x || 0) : 0
|
||||
y: _active ? win._dockState.bodyY + (win._dockSlide.y || 0) : 0
|
||||
width: _active ? win._dockState.bodyW : 0
|
||||
height: _active ? win._dockState.bodyH : 0
|
||||
}
|
||||
|
||||
Item {
|
||||
id: _popoutBodyBlurCap
|
||||
opacity: 0
|
||||
|
||||
readonly property string _side: ConnectedModeState.popoutBarSide
|
||||
readonly property real _capThickness: win._popoutBlurCapThickness()
|
||||
readonly property bool _active: _popoutBodyBlurAnchor._active && _capThickness > 0 && _popoutBodyBlurAnchor.width > 0 && _popoutBodyBlurAnchor.height > 0
|
||||
readonly property real _capWidth: (_side === "left" || _side === "right") ? Math.min(_capThickness, _popoutBodyBlurAnchor.width) : _popoutBodyBlurAnchor.width
|
||||
readonly property real _capHeight: (_side === "top" || _side === "bottom") ? Math.min(_capThickness, _popoutBodyBlurAnchor.height) : _popoutBodyBlurAnchor.height
|
||||
|
||||
x: !_active ? 0 : (_side === "right" ? _popoutBodyBlurAnchor.x + _popoutBodyBlurAnchor.width - _capWidth : _popoutBodyBlurAnchor.x)
|
||||
y: !_active ? 0 : (_side === "bottom" ? _popoutBodyBlurAnchor.y + _popoutBodyBlurAnchor.height - _capHeight : _popoutBodyBlurAnchor.y)
|
||||
width: _active ? _capWidth : 0
|
||||
height: _active ? _capHeight : 0
|
||||
}
|
||||
|
||||
Item {
|
||||
id: _dockBodyBlurCap
|
||||
opacity: 0
|
||||
|
||||
readonly property string _side: win._dockState.barSide
|
||||
readonly property bool _active: _dockBodyBlurAnchor._active && _dockBodyBlurAnchor.width > 0 && _dockBodyBlurAnchor.height > 0
|
||||
readonly property real _capWidth: (_side === "left" || _side === "right") ? Math.min(win._dockConnectorRadius(), _dockBodyBlurAnchor.width) : _dockBodyBlurAnchor.width
|
||||
readonly property real _capHeight: (_side === "top" || _side === "bottom") ? Math.min(win._dockConnectorRadius(), _dockBodyBlurAnchor.height) : _dockBodyBlurAnchor.height
|
||||
|
||||
x: !_active ? 0 : (_side === "right" ? _dockBodyBlurAnchor.x + _dockBodyBlurAnchor.width - _capWidth : _dockBodyBlurAnchor.x)
|
||||
y: !_active ? 0 : (_side === "bottom" ? _dockBodyBlurAnchor.y + _dockBodyBlurAnchor.height - _capHeight : _dockBodyBlurAnchor.y)
|
||||
width: _active ? _capWidth : 0
|
||||
height: _active ? _capHeight : 0
|
||||
}
|
||||
|
||||
Item {
|
||||
id: _dockLeftConnectorBlurAnchor
|
||||
opacity: 0
|
||||
|
||||
readonly property bool _active: _dockBodyBlurAnchor._active && win._dockConnectorRadius() > 0
|
||||
readonly property real _w: win._dockConnectorWidth(0)
|
||||
readonly property real _h: win._dockConnectorHeight(0)
|
||||
|
||||
x: _active ? Theme.snap(win._dockConnectorX(_dockBodyBlurAnchor.x, _dockBodyBlurAnchor.width, "left", 0), win._dpr) : 0
|
||||
y: _active ? Theme.snap(win._dockConnectorY(_dockBodyBlurAnchor.y, _dockBodyBlurAnchor.height, "left", 0), win._dpr) : 0
|
||||
width: _active ? _w : 0
|
||||
height: _active ? _h : 0
|
||||
}
|
||||
|
||||
Item {
|
||||
id: _dockRightConnectorBlurAnchor
|
||||
opacity: 0
|
||||
|
||||
readonly property bool _active: _dockBodyBlurAnchor._active && win._dockConnectorRadius() > 0
|
||||
readonly property real _w: win._dockConnectorWidth(0)
|
||||
readonly property real _h: win._dockConnectorHeight(0)
|
||||
|
||||
x: _active ? Theme.snap(win._dockConnectorX(_dockBodyBlurAnchor.x, _dockBodyBlurAnchor.width, "right", 0), win._dpr) : 0
|
||||
y: _active ? Theme.snap(win._dockConnectorY(_dockBodyBlurAnchor.y, _dockBodyBlurAnchor.height, "right", 0), win._dpr) : 0
|
||||
width: _active ? _w : 0
|
||||
height: _active ? _h : 0
|
||||
}
|
||||
|
||||
Item {
|
||||
id: _dockLeftConnectorCutout
|
||||
opacity: 0
|
||||
|
||||
readonly property bool _active: _dockLeftConnectorBlurAnchor.width > 0 && _dockLeftConnectorBlurAnchor.height > 0
|
||||
readonly property string _arcCorner: win._connectorArcCorner(win._dockState.barSide, "left")
|
||||
|
||||
x: _active ? win._connectorCutoutX(_dockLeftConnectorBlurAnchor.x, _dockLeftConnectorBlurAnchor.width, _arcCorner, win._dockConnectorRadius()) : 0
|
||||
y: _active ? win._connectorCutoutY(_dockLeftConnectorBlurAnchor.y, _dockLeftConnectorBlurAnchor.height, _arcCorner, win._dockConnectorRadius()) : 0
|
||||
width: _active ? win._dockConnectorRadius() * 2 : 0
|
||||
height: _active ? win._dockConnectorRadius() * 2 : 0
|
||||
}
|
||||
|
||||
Item {
|
||||
id: _dockRightConnectorCutout
|
||||
opacity: 0
|
||||
|
||||
readonly property bool _active: _dockRightConnectorBlurAnchor.width > 0 && _dockRightConnectorBlurAnchor.height > 0
|
||||
readonly property string _arcCorner: win._connectorArcCorner(win._dockState.barSide, "right")
|
||||
|
||||
x: _active ? win._connectorCutoutX(_dockRightConnectorBlurAnchor.x, _dockRightConnectorBlurAnchor.width, _arcCorner, win._dockConnectorRadius()) : 0
|
||||
y: _active ? win._connectorCutoutY(_dockRightConnectorBlurAnchor.y, _dockRightConnectorBlurAnchor.height, _arcCorner, win._dockConnectorRadius()) : 0
|
||||
width: _active ? win._dockConnectorRadius() * 2 : 0
|
||||
height: _active ? win._dockConnectorRadius() * 2 : 0
|
||||
}
|
||||
|
||||
Item {
|
||||
id: _notifBodyBlurAnchor
|
||||
visible: false
|
||||
|
||||
readonly property bool _active: win._frameActive && win._notifState.visible && win._notifState.bodyW > 0 && win._notifState.bodyH > 0
|
||||
|
||||
x: _active ? Theme.snap(win._notifState.bodyX, win._dpr) : 0
|
||||
y: _active ? Theme.snap(win._notifState.bodyY, win._dpr) : 0
|
||||
width: _active ? Theme.snap(win._notifState.bodyW, win._dpr) : 0
|
||||
height: _active ? Theme.snap(win._notifState.bodyH, win._dpr) : 0
|
||||
}
|
||||
|
||||
Region {
|
||||
id: _staticBlurRegion
|
||||
x: 0
|
||||
y: 0
|
||||
width: win._windowRegionWidth
|
||||
height: win._windowRegionHeight
|
||||
|
||||
// Frame cutout (always active when frame is on)
|
||||
Region {
|
||||
item: _blurCutout
|
||||
intersection: Intersection.Subtract
|
||||
radius: win._blurCutoutRadius
|
||||
}
|
||||
|
||||
// ── Connected popout blur regions ──
|
||||
Region {
|
||||
item: _popoutBodyBlurAnchor
|
||||
radius: win._surfaceRadius
|
||||
}
|
||||
Region {
|
||||
item: _popoutBodyBlurCap
|
||||
}
|
||||
|
||||
// ── Connected dock blur regions ──
|
||||
Region {
|
||||
item: _dockBodyBlurAnchor
|
||||
radius: win._dockBodyBlurRadius()
|
||||
}
|
||||
Region {
|
||||
item: _dockBodyBlurCap
|
||||
}
|
||||
Region {
|
||||
item: _dockLeftConnectorBlurAnchor
|
||||
radius: win._dockConnectorRadius()
|
||||
Region {
|
||||
item: _dockLeftConnectorCutout
|
||||
intersection: Intersection.Subtract
|
||||
radius: win._dockConnectorRadius()
|
||||
}
|
||||
}
|
||||
Region {
|
||||
item: _dockRightConnectorBlurAnchor
|
||||
radius: win._dockConnectorRadius()
|
||||
Region {
|
||||
item: _dockRightConnectorCutout
|
||||
intersection: Intersection.Subtract
|
||||
radius: win._dockConnectorRadius()
|
||||
}
|
||||
}
|
||||
|
||||
Region {
|
||||
item: _notifBodyBlurAnchor
|
||||
radius: win._surfaceRadius
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Connector position helpers (dock) ─────────────────────────────────
|
||||
|
||||
function _dockBodyBlurRadius() {
|
||||
return _dockBodyBlurAnchor._active ? Math.max(0, Math.min(win._surfaceRadius, _dockBodyBlurAnchor.width / 2, _dockBodyBlurAnchor.height / 2)) : win._surfaceRadius;
|
||||
}
|
||||
|
||||
function _dockConnectorRadius() {
|
||||
if (!_dockBodyBlurAnchor._active)
|
||||
return win._ccr;
|
||||
const dockSide = win._dockState.barSide;
|
||||
const thickness = (dockSide === "left" || dockSide === "right") ? _dockBodyBlurAnchor.width : _dockBodyBlurAnchor.height;
|
||||
const bodyRadius = win._dockBodyBlurRadius();
|
||||
const maxConnectorRadius = Math.max(0, thickness - bodyRadius - win._seamOverlap);
|
||||
return Math.max(0, Math.min(win._ccr, bodyRadius, maxConnectorRadius));
|
||||
}
|
||||
|
||||
function _dockConnectorWidth(spacing) {
|
||||
const isVert = win._dockState.barSide === "left" || win._dockState.barSide === "right";
|
||||
const radius = win._dockConnectorRadius();
|
||||
return isVert ? (spacing + radius) : radius;
|
||||
}
|
||||
|
||||
function _dockConnectorHeight(spacing) {
|
||||
const isVert = win._dockState.barSide === "left" || win._dockState.barSide === "right";
|
||||
const radius = win._dockConnectorRadius();
|
||||
return isVert ? radius : (spacing + radius);
|
||||
}
|
||||
|
||||
function _dockConnectorX(baseX, bodyWidth, placement, spacing) {
|
||||
const dockSide = win._dockState.barSide;
|
||||
const isVert = dockSide === "left" || dockSide === "right";
|
||||
const seamX = !isVert ? (placement === "left" ? baseX : baseX + bodyWidth) : (dockSide === "left" ? baseX : baseX + bodyWidth);
|
||||
const w = _dockConnectorWidth(spacing);
|
||||
if (!isVert)
|
||||
return placement === "left" ? seamX - w : seamX;
|
||||
return dockSide === "left" ? seamX : seamX - w;
|
||||
}
|
||||
|
||||
function _dockConnectorY(baseY, bodyHeight, placement, spacing) {
|
||||
const dockSide = win._dockState.barSide;
|
||||
const seamY = dockSide === "top" ? baseY : dockSide === "bottom" ? baseY + bodyHeight : (placement === "left" ? baseY : baseY + bodyHeight);
|
||||
const h = _dockConnectorHeight(spacing);
|
||||
if (dockSide === "top")
|
||||
return seamY;
|
||||
if (dockSide === "bottom")
|
||||
return seamY - h;
|
||||
return placement === "left" ? seamY - h : seamY;
|
||||
}
|
||||
|
||||
function _popoutFillOverlapX() {
|
||||
return (ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom") ? win._seamOverlap : 0;
|
||||
}
|
||||
|
||||
function _popoutFillOverlapY() {
|
||||
return (ConnectedModeState.popoutBarSide === "left" || ConnectedModeState.popoutBarSide === "right") ? win._seamOverlap : 0;
|
||||
}
|
||||
|
||||
function _dockFillOverlapX() {
|
||||
return (win._dockState.barSide === "top" || win._dockState.barSide === "bottom") ? win._seamOverlap : 0;
|
||||
}
|
||||
|
||||
function _dockFillOverlapY() {
|
||||
return (win._dockState.barSide === "left" || win._dockState.barSide === "right") ? win._seamOverlap : 0;
|
||||
}
|
||||
|
||||
function _popoutArcExtent() {
|
||||
return (ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom") ? _popoutBodyBlurAnchor.height : _popoutBodyBlurAnchor.width;
|
||||
}
|
||||
|
||||
function _popoutArcVisible() {
|
||||
if (!_popoutBodyBlurAnchor._active || _popoutBodyBlurAnchor.width <= 0 || _popoutBodyBlurAnchor.height <= 0)
|
||||
return false;
|
||||
return win._popoutArcExtent() >= win._ccr * (1 + win._ccr * 0.02);
|
||||
}
|
||||
|
||||
function _popoutBlurCapThickness() {
|
||||
const extent = win._popoutArcExtent();
|
||||
return Math.max(0, Math.min(win._effectivePopoutCcr, extent - win._surfaceRadius));
|
||||
}
|
||||
|
||||
function _popoutChromeX() {
|
||||
const barSide = ConnectedModeState.popoutBarSide;
|
||||
return ConnectedModeState.popoutBodyX - ((barSide === "top" || barSide === "bottom") ? win._effectivePopoutCcr : 0);
|
||||
}
|
||||
|
||||
function _popoutChromeY() {
|
||||
const barSide = ConnectedModeState.popoutBarSide;
|
||||
return ConnectedModeState.popoutBodyY - ((barSide === "left" || barSide === "right") ? win._effectivePopoutCcr : 0);
|
||||
}
|
||||
|
||||
function _popoutChromeWidth() {
|
||||
const barSide = ConnectedModeState.popoutBarSide;
|
||||
return ConnectedModeState.popoutBodyW + ((barSide === "top" || barSide === "bottom") ? win._effectivePopoutCcr * 2 : 0);
|
||||
}
|
||||
|
||||
function _popoutChromeHeight() {
|
||||
const barSide = ConnectedModeState.popoutBarSide;
|
||||
return ConnectedModeState.popoutBodyH + ((barSide === "left" || barSide === "right") ? win._effectivePopoutCcr * 2 : 0);
|
||||
}
|
||||
|
||||
function _popoutClipX() {
|
||||
return _popoutBodyBlurAnchor.x - win._popoutChromeX() - win._popoutFillOverlapX();
|
||||
}
|
||||
|
||||
function _popoutClipY() {
|
||||
return _popoutBodyBlurAnchor.y - win._popoutChromeY() - win._popoutFillOverlapY();
|
||||
}
|
||||
|
||||
function _popoutClipWidth() {
|
||||
return _popoutBodyBlurAnchor.width + win._popoutFillOverlapX() * 2;
|
||||
}
|
||||
|
||||
function _popoutClipHeight() {
|
||||
return _popoutBodyBlurAnchor.height + win._popoutFillOverlapY() * 2;
|
||||
}
|
||||
|
||||
function _popoutBodyXInClip() {
|
||||
return (ConnectedModeState.popoutBarSide === "left" ? _popoutBodyBlurAnchor._dxClamp : 0) - win._popoutFillOverlapX();
|
||||
}
|
||||
|
||||
function _popoutBodyYInClip() {
|
||||
return (ConnectedModeState.popoutBarSide === "top" ? _popoutBodyBlurAnchor._dyClamp : 0) - win._popoutFillOverlapY();
|
||||
}
|
||||
|
||||
function _popoutBodyFullWidth() {
|
||||
return ConnectedModeState.popoutBodyW + win._popoutFillOverlapX() * 2;
|
||||
}
|
||||
|
||||
function _popoutBodyFullHeight() {
|
||||
return ConnectedModeState.popoutBodyH + win._popoutFillOverlapY() * 2;
|
||||
}
|
||||
|
||||
function _dockChromeX() {
|
||||
const dockSide = win._dockState.barSide;
|
||||
return _dockBodyBlurAnchor.x - ((dockSide === "top" || dockSide === "bottom") ? win._dockConnectorRadius() : 0);
|
||||
}
|
||||
|
||||
function _dockChromeY() {
|
||||
const dockSide = win._dockState.barSide;
|
||||
return _dockBodyBlurAnchor.y - ((dockSide === "left" || dockSide === "right") ? win._dockConnectorRadius() : 0);
|
||||
}
|
||||
|
||||
function _dockChromeWidth() {
|
||||
const dockSide = win._dockState.barSide;
|
||||
return _dockBodyBlurAnchor.width + ((dockSide === "top" || dockSide === "bottom") ? win._dockConnectorRadius() * 2 : 0);
|
||||
}
|
||||
|
||||
function _dockChromeHeight() {
|
||||
const dockSide = win._dockState.barSide;
|
||||
return _dockBodyBlurAnchor.height + ((dockSide === "left" || dockSide === "right") ? win._dockConnectorRadius() * 2 : 0);
|
||||
}
|
||||
|
||||
function _dockBodyXInChrome() {
|
||||
return ((win._dockState.barSide === "top" || win._dockState.barSide === "bottom") ? win._dockConnectorRadius() : 0) - win._dockFillOverlapX();
|
||||
}
|
||||
|
||||
function _dockBodyYInChrome() {
|
||||
return ((win._dockState.barSide === "left" || win._dockState.barSide === "right") ? win._dockConnectorRadius() : 0) - win._dockFillOverlapY();
|
||||
}
|
||||
|
||||
function _connectorArcCorner(barSide, placement) {
|
||||
if (barSide === "top")
|
||||
return placement === "left" ? "bottomLeft" : "bottomRight";
|
||||
if (barSide === "bottom")
|
||||
return placement === "left" ? "topLeft" : "topRight";
|
||||
if (barSide === "left")
|
||||
return placement === "left" ? "topRight" : "bottomRight";
|
||||
return placement === "left" ? "topLeft" : "bottomLeft";
|
||||
}
|
||||
|
||||
function _connectorCutoutX(connectorX, connectorWidth, arcCorner, radius) {
|
||||
const r = radius === undefined ? win._effectivePopoutCcr : radius;
|
||||
return (arcCorner === "topLeft" || arcCorner === "bottomLeft") ? connectorX - r : connectorX + connectorWidth - r;
|
||||
}
|
||||
|
||||
function _connectorCutoutY(connectorY, connectorHeight, arcCorner, radius) {
|
||||
const r = radius === undefined ? win._effectivePopoutCcr : radius;
|
||||
return (arcCorner === "topLeft" || arcCorner === "topRight") ? connectorY - r : connectorY + connectorHeight - r;
|
||||
}
|
||||
|
||||
// ─── Blur build / teardown ────────────────────────────────────────────────
|
||||
|
||||
function _buildBlur() {
|
||||
try {
|
||||
if (!BlurService.enabled || !SettingsData.frameBlurEnabled || !win._frameActive || !win.visible) {
|
||||
win.BackgroundEffect.blurRegion = null;
|
||||
return;
|
||||
}
|
||||
win.BackgroundEffect.blurRegion = _staticBlurRegion;
|
||||
} catch (e) {
|
||||
console.warn("FrameWindow: Failed to set blur region:", e);
|
||||
}
|
||||
}
|
||||
|
||||
function _teardownBlur() {
|
||||
try {
|
||||
win.BackgroundEffect.blurRegion = null;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
function onConnectedFrameModeActiveChanged() {
|
||||
_blurRebuildTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: BlurService
|
||||
function onEnabledChanged() {
|
||||
_blurRebuildTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
_blurRebuildTimer.restart();
|
||||
} else {
|
||||
_teardownBlur();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: Qt.callLater(() => win._buildBlur())
|
||||
Component.onDestruction: win._teardownBlur()
|
||||
|
||||
// ─── Frame border ─────────────────────────────────────────────────────────
|
||||
|
||||
FrameBorder {
|
||||
anchors.fill: parent
|
||||
visible: win._frameActive && !win._connectedActive
|
||||
cutoutTopInset: win.cutoutTopInset
|
||||
cutoutBottomInset: win.cutoutBottomInset
|
||||
cutoutLeftInset: win.cutoutLeftInset
|
||||
cutoutRightInset: win.cutoutRightInset
|
||||
cutoutRadius: win.cutoutRadius
|
||||
}
|
||||
|
||||
// ─── Connected chrome fills ───────────────────────────────────────────────
|
||||
|
||||
Item {
|
||||
id: _connectedSurfaceLayer
|
||||
anchors.fill: parent
|
||||
visible: win._connectedActive
|
||||
opacity: win._surfaceOpacity
|
||||
layer.enabled: opacity < 1
|
||||
layer.smooth: false
|
||||
|
||||
FrameBorder {
|
||||
anchors.fill: parent
|
||||
borderColor: win._opaqueSurfaceColor
|
||||
cutoutTopInset: win.cutoutTopInset
|
||||
cutoutBottomInset: win.cutoutBottomInset
|
||||
cutoutLeftInset: win.cutoutLeftInset
|
||||
cutoutRightInset: win.cutoutRightInset
|
||||
cutoutRadius: win.cutoutRadius
|
||||
}
|
||||
|
||||
Item {
|
||||
id: _connectedChrome
|
||||
anchors.fill: parent
|
||||
visible: true
|
||||
|
||||
Item {
|
||||
id: _popoutChrome
|
||||
visible: ConnectedModeState.popoutVisible && ConnectedModeState.popoutScreen === win._screenName
|
||||
x: win._popoutChromeX()
|
||||
y: win._popoutChromeY()
|
||||
width: win._popoutChromeWidth()
|
||||
height: win._popoutChromeHeight()
|
||||
|
||||
Item {
|
||||
id: _popoutClip
|
||||
readonly property bool _barHoriz: ConnectedModeState.popoutBarSide === "top" || ConnectedModeState.popoutBarSide === "bottom"
|
||||
// Expand clip by ccr on bar axis to include arc columns
|
||||
x: win._popoutClipX() - (_barHoriz ? win._effectivePopoutCcr : 0)
|
||||
y: win._popoutClipY() - (_barHoriz ? 0 : win._effectivePopoutCcr)
|
||||
width: win._popoutClipWidth() + (_barHoriz ? win._effectivePopoutCcr * 2 : 0)
|
||||
height: win._popoutClipHeight() + (_barHoriz ? 0 : win._effectivePopoutCcr * 2)
|
||||
clip: true
|
||||
|
||||
ConnectedShape {
|
||||
id: _popoutShape
|
||||
visible: _popoutBodyBlurAnchor._active && _popoutBodyBlurAnchor.width > 0 && _popoutBodyBlurAnchor.height > 0
|
||||
barSide: ConnectedModeState.popoutBarSide
|
||||
bodyWidth: win._popoutClipWidth()
|
||||
bodyHeight: win._popoutClipHeight()
|
||||
connectorRadius: win._effectivePopoutCcr
|
||||
surfaceRadius: win._surfaceRadius
|
||||
fillColor: win._opaqueSurfaceColor
|
||||
x: 0
|
||||
y: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: _dockChrome
|
||||
visible: _dockBodyBlurAnchor._active
|
||||
x: win._dockChromeX()
|
||||
y: win._dockChromeY()
|
||||
width: win._dockChromeWidth()
|
||||
height: win._dockChromeHeight()
|
||||
|
||||
Rectangle {
|
||||
id: _dockFill
|
||||
x: win._dockBodyXInChrome()
|
||||
y: win._dockBodyYInChrome()
|
||||
width: _dockBodyBlurAnchor.width + win._dockFillOverlapX() * 2
|
||||
height: _dockBodyBlurAnchor.height + win._dockFillOverlapY() * 2
|
||||
color: win._opaqueSurfaceColor
|
||||
z: 1
|
||||
|
||||
readonly property string _dockSide: win._dockState.barSide
|
||||
readonly property real _dockRadius: win._dockBodyBlurRadius()
|
||||
topLeftRadius: (_dockSide === "top" || _dockSide === "left") ? 0 : _dockRadius
|
||||
topRightRadius: (_dockSide === "top" || _dockSide === "right") ? 0 : _dockRadius
|
||||
bottomLeftRadius: (_dockSide === "bottom" || _dockSide === "left") ? 0 : _dockRadius
|
||||
bottomRightRadius: (_dockSide === "bottom" || _dockSide === "right") ? 0 : _dockRadius
|
||||
}
|
||||
|
||||
ConnectedCorner {
|
||||
id: _connDockLeft
|
||||
visible: _dockBodyBlurAnchor._active
|
||||
barSide: win._dockState.barSide
|
||||
placement: "left"
|
||||
spacing: 0
|
||||
connectorRadius: win._dockConnectorRadius()
|
||||
color: win._opaqueSurfaceColor
|
||||
dpr: win._dpr
|
||||
x: Theme.snap(win._dockConnectorX(_dockBodyBlurAnchor.x, _dockBodyBlurAnchor.width, "left", 0) - _dockChrome.x, win._dpr)
|
||||
y: Theme.snap(win._dockConnectorY(_dockBodyBlurAnchor.y, _dockBodyBlurAnchor.height, "left", 0) - _dockChrome.y, win._dpr)
|
||||
}
|
||||
|
||||
ConnectedCorner {
|
||||
id: _connDockRight
|
||||
visible: _dockBodyBlurAnchor._active
|
||||
barSide: win._dockState.barSide
|
||||
placement: "right"
|
||||
spacing: 0
|
||||
connectorRadius: win._dockConnectorRadius()
|
||||
color: win._opaqueSurfaceColor
|
||||
dpr: win._dpr
|
||||
x: Theme.snap(win._dockConnectorX(_dockBodyBlurAnchor.x, _dockBodyBlurAnchor.width, "right", 0) - _dockChrome.x, win._dpr)
|
||||
y: Theme.snap(win._dockConnectorY(_dockBodyBlurAnchor.y, _dockBodyBlurAnchor.height, "right", 0) - _dockChrome.y, win._dpr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: _notifChrome
|
||||
visible: _notifBodyBlurAnchor._active
|
||||
|
||||
readonly property string _notifSide: win._notifState.barSide
|
||||
readonly property bool _isHoriz: _notifSide === "top" || _notifSide === "bottom"
|
||||
readonly property real _notifCcr: Theme.snap(Math.max(0, Math.min(win._ccr, win._surfaceRadius, (_isHoriz ? _notifBodyBlurAnchor.width : _notifBodyBlurAnchor.height) / 2)), win._dpr)
|
||||
readonly property real _sideUnderlap: _isHoriz ? 0 : win._seamOverlap
|
||||
readonly property real _bodyW: Theme.snap(_notifBodyBlurAnchor.width + _sideUnderlap, win._dpr)
|
||||
readonly property real _bodyH: Theme.snap(_notifBodyBlurAnchor.height, win._dpr)
|
||||
|
||||
z: _isHoriz ? 0 : -1
|
||||
x: Theme.snap(_notifBodyBlurAnchor.x - (_isHoriz ? _notifCcr : (_notifSide === "left" ? _sideUnderlap : 0)), win._dpr)
|
||||
y: Theme.snap(_notifBodyBlurAnchor.y - (_isHoriz ? 0 : _notifCcr), win._dpr)
|
||||
width: _isHoriz ? Theme.snap(_bodyW + _notifCcr * 2, win._dpr) : _bodyW
|
||||
height: Theme.snap(_bodyH + (_isHoriz ? 0 : _notifCcr * 2), win._dpr)
|
||||
|
||||
ConnectedShape {
|
||||
visible: _notifBodyBlurAnchor._active && _notifBodyBlurAnchor.width > 0 && _notifBodyBlurAnchor.height > 0
|
||||
barSide: _notifChrome._notifSide
|
||||
bodyWidth: _notifChrome._bodyW
|
||||
bodyHeight: _notifChrome._bodyH
|
||||
connectorRadius: _notifChrome._notifCcr
|
||||
surfaceRadius: win._surfaceRadius
|
||||
fillColor: win._opaqueSurfaceColor
|
||||
x: 0
|
||||
y: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1338,7 +1338,7 @@ Item {
|
||||
enabled: MprisController.activePlayer?.canGoPrevious ?? false
|
||||
hoverEnabled: enabled
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: MprisController.activePlayer?.previous()
|
||||
onClicked: MprisController.previousOrRewind()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,11 +34,12 @@ Rectangle {
|
||||
readonly property real actionButtonHeight: compactMode ? 20 : 24
|
||||
readonly property real collapsedContentHeight: Math.max(iconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2))
|
||||
readonly property real baseCardHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing
|
||||
readonly property bool connectedFrameMode: SettingsData.connectedFrameModeActive
|
||||
|
||||
width: parent ? parent.width : 400
|
||||
height: expanded ? (expandedContent.height + cardPadding * 2) : (baseCardHeight + collapsedContent.extraHeight)
|
||||
readonly property real targetHeight: expanded ? (expandedContent.height + cardPadding * 2) : (baseCardHeight + collapsedContent.extraHeight)
|
||||
radius: Theme.cornerRadius
|
||||
radius: connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
|
||||
scale: (cardHoverHandler.hovered ? 1.004 : 1.0) * listLevelAdjacentScaleInfluence
|
||||
readonly property bool shadowsAllowed: Theme.elevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||
readonly property var shadowElevation: Theme.elevationLevel1
|
||||
@@ -100,6 +101,8 @@ Rectangle {
|
||||
if (keyboardNavigationActive && expanded && selectedNotificationIndex >= 0) {
|
||||
return Theme.primaryHoverLight;
|
||||
}
|
||||
if (connectedFrameMode)
|
||||
return Theme.popupLayerColor(Theme.surfaceContainerHigh);
|
||||
return Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency);
|
||||
}
|
||||
border.color: {
|
||||
@@ -959,9 +962,9 @@ Rectangle {
|
||||
Behavior on height {
|
||||
enabled: root.__initialized && root.userInitiatedExpansion && root.animateExpansion
|
||||
NumberAnimation {
|
||||
duration: root.expanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration
|
||||
duration: root.connectedFrameMode ? Theme.variantDuration(Theme.popoutAnimationDuration, root.expanded) : (root.expanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasized
|
||||
easing.bezierCurve: root.connectedFrameMode ? (root.expanded ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : Theme.expressiveCurves.emphasized
|
||||
onRunningChanged: {
|
||||
if (running) {
|
||||
root.isAnimating = true;
|
||||
|
||||
@@ -39,11 +39,9 @@ DankPopout {
|
||||
}
|
||||
}
|
||||
|
||||
popupWidth: triggerScreen ? Math.min(500, Math.max(380, triggerScreen.width - 48)) : 400
|
||||
popupWidth: 400
|
||||
popupHeight: stablePopupHeight
|
||||
positioning: ""
|
||||
animationScaleCollapsed: 0.94
|
||||
animationOffset: 0
|
||||
suspendShadowWhileResizing: false
|
||||
|
||||
screen: triggerScreen
|
||||
|
||||
@@ -10,13 +10,29 @@ import qs.Widgets
|
||||
PanelWindow {
|
||||
id: win
|
||||
|
||||
readonly property bool connectedFrameMode: SettingsData.frameEnabled
|
||||
&& Theme.isConnectedEffect
|
||||
&& SettingsData.isScreenInPreferences(win.screen, SettingsData.frameScreenPreferences)
|
||||
readonly property string notifBarSide: {
|
||||
const pos = SettingsData.notificationPopupPosition;
|
||||
if (pos === -1) return "top";
|
||||
switch (pos) {
|
||||
case SettingsData.Position.Top: return "right";
|
||||
case SettingsData.Position.Left: return "left";
|
||||
case SettingsData.Position.BottomCenter: return "bottom";
|
||||
case SettingsData.Position.Right: return "right";
|
||||
case SettingsData.Position.Bottom: return "left";
|
||||
default: return "top";
|
||||
}
|
||||
}
|
||||
|
||||
WindowBlur {
|
||||
targetWindow: win
|
||||
blurX: content.x + content.cardInset + swipeTx.x + tx.x
|
||||
blurY: content.y + content.cardInset + swipeTx.y + tx.y
|
||||
blurWidth: !win._finalized ? Math.max(0, content.width - content.cardInset * 2) : 0
|
||||
blurHeight: !win._finalized ? Math.max(0, content.height - content.cardInset * 2) : 0
|
||||
blurRadius: Theme.cornerRadius
|
||||
blurWidth: !win._finalized && !win.connectedFrameMode ? Math.max(0, content.width - content.cardInset * 2) : 0
|
||||
blurHeight: !win._finalized && !win.connectedFrameMode ? Math.max(0, content.height - content.cardInset * 2) : 0
|
||||
blurRadius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
|
||||
}
|
||||
|
||||
WlrLayershell.namespace: "dms:notification-popup"
|
||||
@@ -32,6 +48,29 @@ PanelWindow {
|
||||
property real _lastReportedAlignedHeight: -1
|
||||
property real _storedTopMargin: 0
|
||||
property real _storedBottomMargin: 0
|
||||
readonly property bool directionalEffect: Theme.isDirectionalEffect
|
||||
readonly property bool depthEffect: Theme.isDepthEffect
|
||||
readonly property real entryTravel: {
|
||||
const base = Math.abs(Theme.effectAnimOffset);
|
||||
if (directionalEffect) {
|
||||
if (isCenterPosition)
|
||||
return Math.max(base, Math.round(content.height * 1.1));
|
||||
return Math.max(base, Math.round(content.width * 0.95));
|
||||
}
|
||||
if (depthEffect)
|
||||
return Math.max(base, 44);
|
||||
return base;
|
||||
}
|
||||
readonly property real exitTravel: {
|
||||
if (directionalEffect) {
|
||||
if (isCenterPosition)
|
||||
return content.height + entryTravel;
|
||||
return content.width + entryTravel;
|
||||
}
|
||||
if (depthEffect)
|
||||
return Math.round(entryTravel * 1.35);
|
||||
return Anims.slidePx;
|
||||
}
|
||||
readonly property string clearText: I18n.tr("Dismiss")
|
||||
property bool descriptionExpanded: false
|
||||
readonly property bool hasExpandableBody: (notificationData?.htmlBody || "").replace(/<[^>]*>/g, "").trim().length > 0
|
||||
@@ -61,6 +100,7 @@ PanelWindow {
|
||||
signal exitStarted
|
||||
signal exitFinished
|
||||
signal popupHeightChanged
|
||||
signal popupChromeGeometryChanged
|
||||
|
||||
function startExit() {
|
||||
if (exiting || _isDestroying) {
|
||||
@@ -68,6 +108,7 @@ PanelWindow {
|
||||
}
|
||||
exiting = true;
|
||||
exitStarted();
|
||||
popupChromeGeometryChanged();
|
||||
exitAnim.restart();
|
||||
exitWatchdog.restart();
|
||||
if (NotificationService.removeFromVisibleNotifications)
|
||||
@@ -145,9 +186,10 @@ PanelWindow {
|
||||
enabled: !exiting && !_isDestroying
|
||||
NumberAnimation {
|
||||
id: implicitHeightAnim
|
||||
duration: descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration
|
||||
duration: Theme.variantDuration(descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration, descriptionExpanded)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasized
|
||||
easing.bezierCurve: descriptionExpanded ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
onFinished: win.popupHeightChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,12 +282,24 @@ PanelWindow {
|
||||
});
|
||||
}
|
||||
|
||||
function _frameEdgeInset(side) {
|
||||
if (!screen)
|
||||
return 0;
|
||||
const edges = SettingsData.getActiveBarEdgesForScreen(screen);
|
||||
const raw = edges.includes(side) ? SettingsData.frameBarSize : SettingsData.frameThickness;
|
||||
return Math.max(0, Math.round(Theme.px(raw, dpr)));
|
||||
}
|
||||
|
||||
function getTopMargin() {
|
||||
const popupPos = SettingsData.notificationPopupPosition;
|
||||
const isTop = isTopCenter || popupPos === SettingsData.Position.Top || popupPos === SettingsData.Position.Left;
|
||||
if (!isTop)
|
||||
return 0;
|
||||
|
||||
if (connectedFrameMode) {
|
||||
const cornerClear = isCenterPosition ? 0 : (Theme.px(SettingsData.frameRounding, dpr) + Theme.px(Theme.connectedCornerRadius, dpr));
|
||||
return _frameEdgeInset("top") + cornerClear + screenY;
|
||||
}
|
||||
const barInfo = getBarInfo();
|
||||
const base = barInfo.topBar > 0 ? barInfo.topBar : Theme.popupDistance;
|
||||
return base + screenY;
|
||||
@@ -257,6 +311,10 @@ PanelWindow {
|
||||
if (!isBottom)
|
||||
return 0;
|
||||
|
||||
if (connectedFrameMode) {
|
||||
const cornerClear = isCenterPosition ? 0 : (Theme.px(SettingsData.frameRounding, dpr) + Theme.px(Theme.connectedCornerRadius, dpr));
|
||||
return _frameEdgeInset("bottom") + cornerClear + screenY;
|
||||
}
|
||||
const barInfo = getBarInfo();
|
||||
const base = barInfo.bottomBar > 0 ? barInfo.bottomBar : Theme.popupDistance;
|
||||
return base + screenY;
|
||||
@@ -271,6 +329,8 @@ PanelWindow {
|
||||
if (!isLeft)
|
||||
return 0;
|
||||
|
||||
if (connectedFrameMode)
|
||||
return _frameEdgeInset("left");
|
||||
const barInfo = getBarInfo();
|
||||
return barInfo.leftBar > 0 ? barInfo.leftBar : Theme.popupDistance;
|
||||
}
|
||||
@@ -284,6 +344,8 @@ PanelWindow {
|
||||
if (!isRight)
|
||||
return 0;
|
||||
|
||||
if (connectedFrameMode)
|
||||
return _frameEdgeInset("right");
|
||||
const barInfo = getBarInfo();
|
||||
return barInfo.rightBar > 0 ? barInfo.rightBar : Theme.popupDistance;
|
||||
}
|
||||
@@ -328,10 +390,64 @@ PanelWindow {
|
||||
return Theme.snap(getContentY() - windowShadowPad, dpr);
|
||||
}
|
||||
|
||||
function _swipeDismissTarget() {
|
||||
return (content.swipeDismissDirection < 0 ? -1 : 1) * content.width;
|
||||
}
|
||||
|
||||
function _frameEdgeSwipeDirection() {
|
||||
const popupPos = SettingsData.notificationPopupPosition;
|
||||
return (popupPos === SettingsData.Position.Left || popupPos === SettingsData.Position.Bottom) ? -1 : 1;
|
||||
}
|
||||
|
||||
function _swipeDismissesTowardFrameEdge() {
|
||||
return content.swipeDismissDirection === _frameEdgeSwipeDirection();
|
||||
}
|
||||
|
||||
function popupChromeMotionActive() {
|
||||
return exiting || content.swipeActive || content.swipeDismissing || Math.abs(content.swipeOffset) > 0.5;
|
||||
}
|
||||
|
||||
function popupLayoutReservesSlot() {
|
||||
return !content.swipeDismissing;
|
||||
}
|
||||
|
||||
function popupChromeReservesSlot() {
|
||||
return !content.swipeDismissing;
|
||||
}
|
||||
|
||||
function popupChromeReleaseProgress() {
|
||||
if (content.swipeDismissing)
|
||||
return Math.max(0, Math.min(1, Math.abs(content.swipeOffset) / Math.max(1, content.swipeTravelDistance)));
|
||||
if (!exiting)
|
||||
return 0;
|
||||
const exitOffset = isCenterPosition ? tx.y : tx.x;
|
||||
return Math.max(0, Math.min(1, Math.abs(exitOffset) / Math.max(1, exitTravel)));
|
||||
}
|
||||
|
||||
function popupChromeMotionX() {
|
||||
if (!popupChromeMotionActive() || isCenterPosition)
|
||||
return 0;
|
||||
const motion = content.swipeOffset + tx.x;
|
||||
if (content.swipeDismissing && !_swipeDismissesTowardFrameEdge())
|
||||
return exiting ? Theme.snap(tx.x, dpr) : 0;
|
||||
if (content.swipeActive && motion * _frameEdgeSwipeDirection() < 0)
|
||||
return 0;
|
||||
return Theme.snap(motion, dpr);
|
||||
}
|
||||
|
||||
function popupChromeMotionY() {
|
||||
return popupChromeMotionActive() ? Theme.snap(tx.y, dpr) : 0;
|
||||
}
|
||||
|
||||
readonly property bool screenValid: win.screen && !_isDestroying
|
||||
readonly property real dpr: screenValid ? CompositorService.getScreenScale(win.screen) : 1
|
||||
readonly property real alignedWidth: Theme.px(Math.max(0, implicitWidth - (windowShadowPad * 2)), dpr)
|
||||
readonly property real alignedHeight: Theme.px(Math.max(0, implicitHeight - (windowShadowPad * 2)), dpr)
|
||||
onScreenYChanged: popupChromeGeometryChanged()
|
||||
onScreenChanged: popupChromeGeometryChanged()
|
||||
onConnectedFrameModeChanged: popupChromeGeometryChanged()
|
||||
onAlignedWidthChanged: popupChromeGeometryChanged()
|
||||
onAlignedHeightChanged: popupChromeGeometryChanged()
|
||||
|
||||
Item {
|
||||
id: content
|
||||
@@ -340,7 +456,7 @@ PanelWindow {
|
||||
y: Theme.snap(windowShadowPad, dpr)
|
||||
width: alignedWidth
|
||||
height: alignedHeight
|
||||
visible: !win._finalized
|
||||
visible: !win._finalized && !chromeOnlyExit
|
||||
scale: cardHoverHandler.hovered ? 1.01 : 1.0
|
||||
transformOrigin: Item.Center
|
||||
|
||||
@@ -352,13 +468,25 @@ PanelWindow {
|
||||
}
|
||||
|
||||
property real swipeOffset: 0
|
||||
readonly property real dismissThreshold: isCenterPosition ? height * 0.4 : width * 0.35
|
||||
property real swipeDismissDirection: 1
|
||||
property bool chromeOnlyExit: false
|
||||
readonly property real dismissThreshold: width * 0.35
|
||||
readonly property real swipeFadeStartRatio: 0.75
|
||||
readonly property real swipeTravelDistance: isCenterPosition ? height : width
|
||||
readonly property real swipeTravelDistance: width
|
||||
readonly property real swipeFadeStartOffset: swipeTravelDistance * swipeFadeStartRatio
|
||||
readonly property real swipeFadeDistance: Math.max(1, swipeTravelDistance - swipeFadeStartOffset)
|
||||
readonly property bool swipeActive: swipeDragHandler.active
|
||||
property bool swipeDismissing: false
|
||||
onSwipeDismissingChanged: {
|
||||
if (!win.connectedFrameMode)
|
||||
return;
|
||||
win.popupHeightChanged();
|
||||
win.popupChromeGeometryChanged();
|
||||
}
|
||||
onSwipeOffsetChanged: {
|
||||
if (win.connectedFrameMode)
|
||||
win.popupChromeGeometryChanged();
|
||||
}
|
||||
|
||||
readonly property bool shadowsAllowed: Theme.elevationEnabled && SettingsData.notificationPopupShadowEnabled
|
||||
readonly property var elevLevel: cardHoverHandler.hovered ? Theme.elevationLevel4 : Theme.elevationLevel3
|
||||
@@ -399,7 +527,7 @@ PanelWindow {
|
||||
shadowOffsetX: content.shadowOffsetX
|
||||
shadowOffsetY: content.shadowOffsetY
|
||||
shadowColor: content.shadowsAllowed && content.elevLevel ? Theme.elevationShadowColor(content.elevLevel) : "transparent"
|
||||
shadowEnabled: !win._isDestroying && win.screenValid && content.shadowsAllowed
|
||||
shadowEnabled: !win._isDestroying && win.screenValid && content.shadowsAllowed && !win.connectedFrameMode
|
||||
layer.textureSize: Qt.size(Math.round(width * win.dpr), Math.round(height * win.dpr))
|
||||
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
||||
|
||||
@@ -408,8 +536,12 @@ PanelWindow {
|
||||
sourceRect.y: content.shadowRenderPadding + content.cardInset
|
||||
sourceRect.width: Math.max(0, content.width - (content.cardInset * 2))
|
||||
sourceRect.height: Math.max(0, content.height - (content.cardInset * 2))
|
||||
sourceRect.radius: Theme.cornerRadius
|
||||
sourceRect.color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
sourceRect.radius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
|
||||
sourceRect.color: win.connectedFrameMode ? Theme.popupLayerColor(Theme.surfaceContainer) : Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
sourceRect.antialiasing: true
|
||||
sourceRect.layer.enabled: win.connectedFrameMode
|
||||
sourceRect.layer.smooth: true
|
||||
sourceRect.layer.textureSize: win.connectedFrameMode && win.dpr > 1 ? Qt.size(Math.ceil(sourceRect.width * win.dpr), Math.ceil(sourceRect.height * win.dpr)) : Qt.size(0, 0)
|
||||
sourceRect.border.color: notificationData && notificationData.urgency === NotificationUrgency.Critical ? Theme.withAlpha(Theme.primary, 0.3) : Theme.withAlpha(Theme.outline, 0.08)
|
||||
sourceRect.border.width: notificationData && notificationData.urgency === NotificationUrgency.Critical ? 2 : 0
|
||||
|
||||
@@ -447,10 +579,10 @@ PanelWindow {
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: content.cardInset
|
||||
radius: Theme.cornerRadius
|
||||
radius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
|
||||
color: "transparent"
|
||||
border.color: BlurService.borderColor
|
||||
border.width: BlurService.borderWidth
|
||||
border.color: win.connectedFrameMode ? "transparent" : BlurService.borderColor
|
||||
border.width: win.connectedFrameMode ? 0 : BlurService.borderWidth
|
||||
z: 100
|
||||
}
|
||||
|
||||
@@ -850,14 +982,15 @@ PanelWindow {
|
||||
DragHandler {
|
||||
id: swipeDragHandler
|
||||
target: null
|
||||
xAxis.enabled: !isCenterPosition
|
||||
yAxis.enabled: isCenterPosition
|
||||
xAxis.enabled: true
|
||||
yAxis.enabled: false
|
||||
|
||||
onActiveChanged: {
|
||||
if (active || win.exiting || content.swipeDismissing)
|
||||
return;
|
||||
|
||||
if (Math.abs(content.swipeOffset) > content.dismissThreshold) {
|
||||
content.swipeDismissDirection = content.swipeOffset < 0 ? -1 : 1;
|
||||
content.swipeDismissing = true;
|
||||
swipeDismissAnim.start();
|
||||
} else {
|
||||
@@ -869,15 +1002,7 @@ PanelWindow {
|
||||
if (win.exiting)
|
||||
return;
|
||||
|
||||
const raw = isCenterPosition ? translation.y : translation.x;
|
||||
if (isTopCenter) {
|
||||
content.swipeOffset = Math.min(0, raw);
|
||||
} else if (isBottomCenter) {
|
||||
content.swipeOffset = Math.max(0, raw);
|
||||
} else {
|
||||
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
|
||||
content.swipeOffset = isLeft ? Math.min(0, raw) : Math.max(0, raw);
|
||||
}
|
||||
content.swipeOffset = translation.x;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -908,20 +1033,28 @@ PanelWindow {
|
||||
id: swipeDismissAnim
|
||||
target: content
|
||||
property: "swipeOffset"
|
||||
to: isTopCenter ? -content.height : isBottomCenter ? content.height : (SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom ? -content.width : content.width)
|
||||
to: win._swipeDismissTarget()
|
||||
duration: Theme.notificationExitDuration
|
||||
easing.type: Easing.OutCubic
|
||||
onStopped: {
|
||||
NotificationService.dismissNotification(notificationData);
|
||||
win.forceExit();
|
||||
const inwardConnectedExit = win.connectedFrameMode && !win.isCenterPosition && !win._swipeDismissesTowardFrameEdge();
|
||||
if (inwardConnectedExit)
|
||||
content.chromeOnlyExit = true;
|
||||
if (win.connectedFrameMode && (win.isCenterPosition || inwardConnectedExit)) {
|
||||
win.startExit();
|
||||
NotificationService.dismissNotification(notificationData);
|
||||
} else {
|
||||
NotificationService.dismissNotification(notificationData);
|
||||
win.forceExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transform: [
|
||||
Translate {
|
||||
id: swipeTx
|
||||
x: isCenterPosition ? 0 : content.swipeOffset
|
||||
y: isCenterPosition ? content.swipeOffset : 0
|
||||
x: content.swipeOffset
|
||||
y: 0
|
||||
},
|
||||
Translate {
|
||||
id: tx
|
||||
@@ -929,9 +1062,17 @@ PanelWindow {
|
||||
if (isCenterPosition)
|
||||
return 0;
|
||||
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
|
||||
return isLeft ? -Anims.slidePx : Anims.slidePx;
|
||||
return isLeft ? -entryTravel : entryTravel;
|
||||
}
|
||||
y: isTopCenter ? -entryTravel : isBottomCenter ? entryTravel : 0
|
||||
onXChanged: {
|
||||
if (win.connectedFrameMode)
|
||||
win.popupChromeGeometryChanged();
|
||||
}
|
||||
onYChanged: {
|
||||
if (win.connectedFrameMode)
|
||||
win.popupChromeGeometryChanged();
|
||||
}
|
||||
y: isTopCenter ? -Anims.slidePx : isBottomCenter ? Anims.slidePx : 0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -943,16 +1084,16 @@ PanelWindow {
|
||||
property: isCenterPosition ? "y" : "x"
|
||||
from: {
|
||||
if (isTopCenter)
|
||||
return -Anims.slidePx;
|
||||
return -entryTravel;
|
||||
if (isBottomCenter)
|
||||
return Anims.slidePx;
|
||||
return entryTravel;
|
||||
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
|
||||
return isLeft ? -Anims.slidePx : Anims.slidePx;
|
||||
return isLeft ? -entryTravel : entryTravel;
|
||||
}
|
||||
to: 0
|
||||
duration: Theme.notificationEnterDuration
|
||||
duration: Theme.variantDuration(Theme.notificationEnterDuration, true)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: isCenterPosition ? Theme.expressiveCurves.standardDecel : Theme.expressiveCurves.emphasizedDecel
|
||||
easing.bezierCurve: Theme.variantPopoutEnterCurve
|
||||
onStopped: {
|
||||
if (!win.exiting && !win._isDestroying) {
|
||||
if (isCenterPosition) {
|
||||
@@ -977,35 +1118,35 @@ PanelWindow {
|
||||
from: 0
|
||||
to: {
|
||||
if (isTopCenter)
|
||||
return -Anims.slidePx;
|
||||
return -exitTravel;
|
||||
if (isBottomCenter)
|
||||
return Anims.slidePx;
|
||||
return exitTravel;
|
||||
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
|
||||
return isLeft ? -Anims.slidePx : Anims.slidePx;
|
||||
return isLeft ? -exitTravel : exitTravel;
|
||||
}
|
||||
duration: Theme.notificationExitDuration
|
||||
duration: Theme.variantDuration(Theme.notificationExitDuration, false)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
|
||||
easing.bezierCurve: Theme.variantPopoutExitCurve
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: content
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
duration: Theme.notificationExitDuration
|
||||
to: Theme.isDirectionalEffect ? 1 : 0
|
||||
duration: Theme.variantDuration(Theme.notificationExitDuration, false)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.standardAccel
|
||||
easing.bezierCurve: Theme.variantPopoutExitCurve
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: content
|
||||
property: "scale"
|
||||
from: 1
|
||||
to: 0.98
|
||||
duration: Theme.notificationExitDuration
|
||||
to: Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed
|
||||
duration: Theme.variantDuration(Theme.notificationExitDuration, false)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
|
||||
easing.bezierCurve: Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,23 +8,40 @@ QtObject {
|
||||
property var modelData
|
||||
property int topMargin: 0
|
||||
readonly property bool compactMode: SettingsData.notificationCompactMode
|
||||
readonly property bool notificationConnectedMode: SettingsData.frameEnabled
|
||||
&& Theme.isConnectedEffect
|
||||
&& SettingsData.isScreenInPreferences(manager.modelData, SettingsData.frameScreenPreferences)
|
||||
readonly property string notifBarSide: {
|
||||
const pos = SettingsData.notificationPopupPosition;
|
||||
if (pos === -1) return "top";
|
||||
switch (pos) {
|
||||
case SettingsData.Position.Top: return "right";
|
||||
case SettingsData.Position.Left: return "left";
|
||||
case SettingsData.Position.BottomCenter: return "bottom";
|
||||
case SettingsData.Position.Right: return "right";
|
||||
case SettingsData.Position.Bottom: return "left";
|
||||
default: return "top";
|
||||
}
|
||||
}
|
||||
readonly property real cardPadding: compactMode ? Theme.notificationCardPaddingCompact : Theme.notificationCardPadding
|
||||
readonly property real popupIconSize: compactMode ? Theme.notificationIconSizeCompact : Theme.notificationIconSizeNormal
|
||||
readonly property real actionButtonHeight: compactMode ? 20 : 24
|
||||
readonly property real contentSpacing: compactMode ? Theme.spacingXS : Theme.spacingS
|
||||
readonly property real popupSpacing: compactMode ? 0 : Theme.spacingXS
|
||||
readonly property real popupSpacing: notificationConnectedMode ? 0 : (compactMode ? 0 : Theme.spacingXS)
|
||||
readonly property real collapsedContentHeight: Math.max(popupIconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2))
|
||||
readonly property int baseNotificationHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing + popupSpacing
|
||||
property var popupWindows: []
|
||||
property var destroyingWindows: new Set()
|
||||
property var pendingDestroys: []
|
||||
property int destroyDelayMs: 100
|
||||
property bool _chromeSyncPending: false
|
||||
property Component popupComponent
|
||||
|
||||
popupComponent: Component {
|
||||
NotificationPopup {
|
||||
onExitFinished: manager._onPopupExitFinished(this)
|
||||
onPopupHeightChanged: manager._onPopupHeightChanged(this)
|
||||
onPopupChromeGeometryChanged: manager._onPopupChromeGeometryChanged(this)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +125,14 @@ QtObject {
|
||||
return p && p.status !== Component.Null && !p._isDestroying && p.hasValidData;
|
||||
}
|
||||
|
||||
function _layoutWindows() {
|
||||
return popupWindows.filter(p => _isValidWindow(p) && p.notificationData?.popup && !p.exiting && (!p.popupLayoutReservesSlot || p.popupLayoutReservesSlot()));
|
||||
}
|
||||
|
||||
function _chromeWindows() {
|
||||
return popupWindows.filter(p => p && p.status !== Component.Null && p.visible && !p._finalized && p.hasValidData && (p.notificationData?.popup || p.exiting));
|
||||
}
|
||||
|
||||
function _isFocusedScreen() {
|
||||
if (!SettingsData.notificationFocusedMonitor)
|
||||
return true;
|
||||
@@ -116,18 +141,24 @@ QtObject {
|
||||
}
|
||||
|
||||
function _sync(newWrappers) {
|
||||
let needsReposition = false;
|
||||
for (const p of popupWindows.slice()) {
|
||||
if (!_isValidWindow(p) || p.exiting)
|
||||
continue;
|
||||
if (p.notificationData && newWrappers.indexOf(p.notificationData) === -1) {
|
||||
p.notificationData.removedByLimit = true;
|
||||
p.notificationData.popup = false;
|
||||
needsReposition = true;
|
||||
}
|
||||
}
|
||||
for (const w of newWrappers) {
|
||||
if (w && !_hasWindowFor(w) && _isFocusedScreen())
|
||||
if (w && !_hasWindowFor(w) && _isFocusedScreen()) {
|
||||
_insertAtTop(w);
|
||||
needsReposition = false;
|
||||
}
|
||||
}
|
||||
if (needsReposition)
|
||||
_repositionAll();
|
||||
}
|
||||
|
||||
function _popupHeight(p) {
|
||||
@@ -157,7 +188,7 @@ QtObject {
|
||||
}
|
||||
|
||||
function _repositionAll() {
|
||||
const active = popupWindows.filter(p => _isValidWindow(p) && p.notificationData?.popup && !p.exiting);
|
||||
const active = _layoutWindows();
|
||||
|
||||
const pinnedSlots = [];
|
||||
for (const p of active) {
|
||||
@@ -181,6 +212,168 @@ QtObject {
|
||||
win.screenY = currentY;
|
||||
currentY += _popupHeight(win);
|
||||
}
|
||||
_scheduleNotificationChromeSync();
|
||||
}
|
||||
|
||||
function _scheduleNotificationChromeSync() {
|
||||
if (_chromeSyncPending)
|
||||
return;
|
||||
_chromeSyncPending = true;
|
||||
Qt.callLater(() => {
|
||||
_chromeSyncPending = false;
|
||||
_syncNotificationChromeState();
|
||||
});
|
||||
}
|
||||
|
||||
function _popupChromeRect(p, useMotionOffset) {
|
||||
if (!p || !p.screen)
|
||||
return null;
|
||||
const motionX = useMotionOffset && p.popupChromeMotionX ? p.popupChromeMotionX() : 0;
|
||||
const motionY = useMotionOffset && p.popupChromeMotionY ? p.popupChromeMotionY() : 0;
|
||||
const x = (p.getContentX ? p.getContentX() : 0) + motionX;
|
||||
const y = (p.getContentY ? p.getContentY() : 0) + motionY;
|
||||
const w = p.alignedWidth || 0;
|
||||
const h = Math.max(p.alignedHeight || 0, baseNotificationHeight);
|
||||
if (w <= 0 || h <= 0)
|
||||
return null;
|
||||
return {
|
||||
x: x,
|
||||
y: y,
|
||||
right: x + w,
|
||||
bottom: y + h
|
||||
};
|
||||
}
|
||||
|
||||
function _popupChromeBoundsRect(p, trailing, useMotionOffset) {
|
||||
const rect = _popupChromeRect(p, useMotionOffset);
|
||||
if (!rect || p !== trailing || !p.popupChromeReleaseProgress)
|
||||
return rect;
|
||||
|
||||
const progress = Math.max(0, Math.min(1, p.popupChromeReleaseProgress()));
|
||||
if (progress <= 0)
|
||||
return rect;
|
||||
|
||||
const anchorsTop = _stackAnchorsTop();
|
||||
const h = Math.max(0, rect.bottom - rect.y);
|
||||
const shrink = h * progress;
|
||||
if (anchorsTop)
|
||||
rect.bottom = Math.max(rect.y, rect.bottom - shrink);
|
||||
else
|
||||
rect.y = Math.min(rect.bottom, rect.y + shrink);
|
||||
return rect;
|
||||
}
|
||||
|
||||
function _stackAnchorsTop() {
|
||||
const pos = SettingsData.notificationPopupPosition;
|
||||
return pos === -1 || pos === SettingsData.Position.Top || pos === SettingsData.Position.Left;
|
||||
}
|
||||
|
||||
function _trailingChromeWindow(candidates) {
|
||||
const anchorsTop = _stackAnchorsTop();
|
||||
let trailing = null;
|
||||
let edge = anchorsTop ? -Infinity : Infinity;
|
||||
for (const p of candidates) {
|
||||
const rect = _popupChromeRect(p, false);
|
||||
if (!rect)
|
||||
continue;
|
||||
const candidateEdge = anchorsTop ? rect.bottom : rect.y;
|
||||
if ((anchorsTop && candidateEdge > edge) || (!anchorsTop && candidateEdge < edge)) {
|
||||
edge = candidateEdge;
|
||||
trailing = p;
|
||||
}
|
||||
}
|
||||
return trailing;
|
||||
}
|
||||
|
||||
function _chromeWindowReservesSlot(p, trailing) {
|
||||
if (p === trailing)
|
||||
return true;
|
||||
return !p.popupChromeReservesSlot || p.popupChromeReservesSlot();
|
||||
}
|
||||
|
||||
function _stackAnchoredChromeEdge(candidates) {
|
||||
const anchorsTop = _stackAnchorsTop();
|
||||
let edge = anchorsTop ? Infinity : -Infinity;
|
||||
for (const p of candidates) {
|
||||
const rect = _popupChromeRect(p, false);
|
||||
if (!rect)
|
||||
continue;
|
||||
if (anchorsTop && rect.y < edge)
|
||||
edge = rect.y;
|
||||
if (!anchorsTop && rect.bottom > edge)
|
||||
edge = rect.bottom;
|
||||
}
|
||||
if (edge === Infinity || edge === -Infinity)
|
||||
return null;
|
||||
return {
|
||||
anchorsTop: anchorsTop,
|
||||
edge: edge
|
||||
};
|
||||
}
|
||||
|
||||
function _syncNotificationChromeState() {
|
||||
const screenName = manager.modelData?.name || "";
|
||||
if (!screenName)
|
||||
return;
|
||||
if (!notificationConnectedMode) {
|
||||
ConnectedModeState.clearNotificationState(screenName);
|
||||
return;
|
||||
}
|
||||
const chromeCandidates = _chromeWindows();
|
||||
if (chromeCandidates.length === 0) {
|
||||
ConnectedModeState.clearNotificationState(screenName);
|
||||
return;
|
||||
}
|
||||
const trailing = chromeCandidates.length > 1 ? _trailingChromeWindow(chromeCandidates) : null;
|
||||
let active = chromeCandidates;
|
||||
if (chromeCandidates.length > 1) {
|
||||
const reserving = chromeCandidates.filter(p => _chromeWindowReservesSlot(p, trailing));
|
||||
if (reserving.length > 0)
|
||||
active = reserving;
|
||||
}
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxXEnd = -Infinity;
|
||||
let maxYEnd = -Infinity;
|
||||
const useMotionOffset = active.length === 1 && active[0].popupChromeMotionActive && active[0].popupChromeMotionActive();
|
||||
for (const p of active) {
|
||||
const rect = _popupChromeBoundsRect(p, trailing, useMotionOffset);
|
||||
if (!rect)
|
||||
continue;
|
||||
if (rect.x < minX)
|
||||
minX = rect.x;
|
||||
if (rect.y < minY)
|
||||
minY = rect.y;
|
||||
if (rect.right > maxXEnd)
|
||||
maxXEnd = rect.right;
|
||||
if (rect.bottom > maxYEnd)
|
||||
maxYEnd = rect.bottom;
|
||||
}
|
||||
const stackEdge = _stackAnchoredChromeEdge(chromeCandidates);
|
||||
if (stackEdge !== null) {
|
||||
if (stackEdge.anchorsTop && stackEdge.edge < minY)
|
||||
minY = stackEdge.edge;
|
||||
if (!stackEdge.anchorsTop && stackEdge.edge > maxYEnd)
|
||||
maxYEnd = stackEdge.edge;
|
||||
}
|
||||
if (minX === Infinity || minY === Infinity || maxXEnd <= minX || maxYEnd <= minY) {
|
||||
ConnectedModeState.clearNotificationState(screenName);
|
||||
return;
|
||||
}
|
||||
ConnectedModeState.setNotificationState(screenName, {
|
||||
visible: true,
|
||||
barSide: notifBarSide,
|
||||
bodyX: minX,
|
||||
bodyY: minY,
|
||||
bodyW: maxXEnd - minX,
|
||||
bodyH: maxYEnd - minY
|
||||
});
|
||||
}
|
||||
|
||||
function _onPopupChromeGeometryChanged(p) {
|
||||
if (!p || popupWindows.indexOf(p) === -1)
|
||||
return;
|
||||
_scheduleNotificationChromeSync();
|
||||
}
|
||||
|
||||
function _onPopupHeightChanged(p) {
|
||||
@@ -227,8 +420,15 @@ QtObject {
|
||||
}
|
||||
popupWindows = [];
|
||||
destroyingWindows.clear();
|
||||
_chromeSyncPending = false;
|
||||
_syncNotificationChromeState();
|
||||
}
|
||||
|
||||
onNotificationConnectedModeChanged: _scheduleNotificationChromeSync()
|
||||
onNotifBarSideChanged: _scheduleNotificationChromeSync()
|
||||
onModelDataChanged: _scheduleNotificationChromeSync()
|
||||
onTopMarginChanged: _repositionAll()
|
||||
|
||||
onPopupWindowsChanged: {
|
||||
if (popupWindows.length > 0 && !sweeper.running) {
|
||||
sweeper.start();
|
||||
|
||||
@@ -27,6 +27,7 @@ Item {
|
||||
const pos = selectedBarConfig?.position ?? SettingsData.Position.Top;
|
||||
return pos === SettingsData.Position.Left || pos === SettingsData.Position.Right;
|
||||
}
|
||||
readonly property bool connectedFrameModeActive: SettingsData.connectedFrameModeActive
|
||||
|
||||
Timer {
|
||||
id: horizontalBarChangeDebounce
|
||||
@@ -693,6 +694,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 +801,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 +1037,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 +1080,64 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: dankBarTab.connectedFrameModeActive
|
||||
width: parent.width
|
||||
implicitHeight: connectedFrameStyleNote.implicitHeight + Theme.spacingS * 2
|
||||
|
||||
Row {
|
||||
id: connectedFrameStyleNote
|
||||
x: Theme.spacingM
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "frame_source"
|
||||
size: Theme.fontSizeMedium
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Connected Frame mode keeps bar shadow override, border, and corner overrides off while active")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width - Theme.fontSizeMedium - Theme.spacingS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
@@ -1054,6 +1148,8 @@ Item {
|
||||
collapsible: true
|
||||
expanded: true
|
||||
visible: selectedBarConfig?.enabled
|
||||
enabled: !dankBarTab.connectedFrameModeActive
|
||||
opacity: dankBarTab.connectedFrameModeActive ? 0.5 : 1.0
|
||||
|
||||
readonly property bool shadowActive: (selectedBarConfig?.shadowIntensity ?? 0) > 0
|
||||
readonly property bool isCustomColor: (selectedBarConfig?.shadowColorMode ?? "default") === "custom"
|
||||
@@ -1287,6 +1383,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 +1432,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
|
||||
@@ -1383,6 +1483,8 @@ Item {
|
||||
iconName: "border_style"
|
||||
title: I18n.tr("Border")
|
||||
visible: selectedBarConfig?.enabled
|
||||
enabled: !dankBarTab.connectedFrameModeActive
|
||||
opacity: dankBarTab.connectedFrameModeActive ? 0.5 : 1.0
|
||||
checked: selectedBarConfig?.borderEnabled ?? false
|
||||
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
||||
borderEnabled: checked
|
||||
|
||||
@@ -7,6 +7,9 @@ import qs.Modules.Settings.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
readonly property bool connectedFrameModeActive: SettingsData.frameEnabled
|
||||
&& SettingsData.motionEffect === 1
|
||||
&& SettingsData.directionalAnimationMode === 3
|
||||
|
||||
FileBrowserModal {
|
||||
id: dockLogoFileBrowser
|
||||
@@ -544,6 +547,8 @@ Item {
|
||||
|
||||
SettingsSliderRow {
|
||||
text: I18n.tr("Exclusive Zone Offset")
|
||||
enabled: !root.connectedFrameModeActive
|
||||
opacity: root.connectedFrameModeActive ? 0.5 : 1.0
|
||||
value: SettingsData.dockBottomGap
|
||||
minimum: -100
|
||||
maximum: 100
|
||||
@@ -553,6 +558,8 @@ Item {
|
||||
|
||||
SettingsSliderRow {
|
||||
text: I18n.tr("Margin")
|
||||
enabled: !root.connectedFrameModeActive
|
||||
opacity: root.connectedFrameModeActive ? 0.5 : 1.0
|
||||
value: SettingsData.dockMargin
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
@@ -561,11 +568,42 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: root.connectedFrameModeActive
|
||||
width: parent.width
|
||||
implicitHeight: dockConnectedNote.implicitHeight + Theme.spacingS * 2
|
||||
|
||||
Row {
|
||||
id: dockConnectedNote
|
||||
x: Theme.spacingM
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "frame_source"
|
||||
size: Theme.fontSizeMedium
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Connected Frame mode manages dock edge offset, transparency, blur, and border styling")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width - Theme.fontSizeMedium - Theme.spacingS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "opacity"
|
||||
title: I18n.tr("Transparency")
|
||||
settingKey: "dockTransparency"
|
||||
enabled: !root.connectedFrameModeActive
|
||||
opacity: root.connectedFrameModeActive ? 0.5 : 1.0
|
||||
|
||||
SettingsSliderRow {
|
||||
text: I18n.tr("Dock Transparency")
|
||||
@@ -585,6 +623,8 @@ Item {
|
||||
settingKey: "dockBorder"
|
||||
collapsible: true
|
||||
expanded: false
|
||||
enabled: !root.connectedFrameModeActive
|
||||
opacity: root.connectedFrameModeActive ? 0.5 : 1.0
|
||||
|
||||
SettingsToggleRow {
|
||||
text: I18n.tr("Border")
|
||||
|
||||
324
quickshell/Modules/Settings/FrameTab.qml
Normal file
324
quickshell/Modules/Settings/FrameTab.qml
Normal file
@@ -0,0 +1,324 @@
|
||||
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")
|
||||
description: SettingsData.connectedFrameModeActive
|
||||
? I18n.tr("Controls the radius of the frame and all connected popout, dock, and modal surfaces while Connected Mode is active")
|
||||
: I18n.tr("Controls the frame border radius. This also becomes the connected surface radius whenever Connected Mode is active")
|
||||
unit: "px"
|
||||
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", "surface", "popup", "opacity", "transparency"]
|
||||
text: I18n.tr("Surface Opacity")
|
||||
description: I18n.tr("Frame border opacity. Controls all surface opacity globally when Connected Mode is active")
|
||||
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: true
|
||||
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)
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
visible: SettingsData.frameEnabled
|
||||
settingKey: "directionalAnimationMode"
|
||||
tags: ["frame", "connected", "popout", "corner", "animation"]
|
||||
text: I18n.tr("Connected Mode")
|
||||
description: I18n.tr("Popouts emerge flush from the bar edge as one continuous piece (based on Slide)")
|
||||
checked: SettingsData.connectedFrameModeActive
|
||||
onToggled: checked => {
|
||||
if (checked) {
|
||||
if (SettingsData.directionalAnimationMode !== 3)
|
||||
SettingsData.set("previousDirectionalMode", SettingsData.directionalAnimationMode);
|
||||
SettingsData.set("motionEffect", 1);
|
||||
SettingsData.set("directionalAnimationMode", 3);
|
||||
} else {
|
||||
SettingsData.set("directionalAnimationMode", SettingsData.previousDirectionalMode);
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onDirectionalAnimationModeChanged() {}
|
||||
function onMotionEffectChanged() {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Display Assignment ────────────────────────────────────────────
|
||||
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,6 +46,13 @@ 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,6 +91,16 @@ 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"]
|
||||
|
||||
@@ -11,6 +11,7 @@ import qs.Modules.Settings.Widgets
|
||||
Item {
|
||||
id: themeColorsTab
|
||||
|
||||
readonly property bool connectedFrameModeActive: SettingsData.connectedFrameModeActive
|
||||
property var cachedIconThemes: SettingsData.availableIconThemes
|
||||
property var cachedCursorThemes: SettingsData.availableCursorThemes
|
||||
property var cachedMatugenSchemes: Theme.availableMatugenSchemes.map(option => option.label)
|
||||
@@ -1615,10 +1616,14 @@ Item {
|
||||
|
||||
SettingsSliderRow {
|
||||
tab: "theme"
|
||||
tags: ["popup", "transparency", "opacity", "modal"]
|
||||
tags: ["surface", "popup", "transparency", "opacity", "modal"]
|
||||
settingKey: "popupTransparency"
|
||||
text: I18n.tr("Popup Transparency")
|
||||
description: I18n.tr("Controls opacity of all popouts, modals, and their content layers")
|
||||
text: I18n.tr("Surface Opacity")
|
||||
description: themeColorsTab.connectedFrameModeActive
|
||||
? I18n.tr("Connected Frame mode follows Surface Opacity from the Frame tab for connected popouts, docks, and modal surfaces")
|
||||
: I18n.tr("Controls opacity of all popouts, modals, and their content layers")
|
||||
enabled: !themeColorsTab.connectedFrameModeActive
|
||||
opacity: themeColorsTab.connectedFrameModeActive ? 0.5 : 1.0
|
||||
value: Math.round(SettingsData.popupTransparency * 100)
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
@@ -1632,7 +1637,9 @@ Item {
|
||||
tags: ["corner", "radius", "rounded", "square"]
|
||||
settingKey: "cornerRadius"
|
||||
text: I18n.tr("Corner Radius")
|
||||
description: I18n.tr("0 = square corners")
|
||||
description: themeColorsTab.connectedFrameModeActive
|
||||
? I18n.tr("Controls general UI rounding. Connected frame popouts, docks, and modal surfaces follow Border Radius in the Frame tab while Connected Frame mode is active")
|
||||
: I18n.tr("0 = square corners")
|
||||
value: SettingsData.cornerRadius
|
||||
minimum: 0
|
||||
maximum: 32
|
||||
@@ -1837,7 +1844,11 @@ Item {
|
||||
tags: ["blur", "background", "transparency", "glass", "frosted"]
|
||||
settingKey: "blurEnabled"
|
||||
text: I18n.tr("Background Blur")
|
||||
description: BlurService.available ? I18n.tr("Blur the background behind bars, popouts, modals, and notifications. Requires compositor support and configuration.") : I18n.tr("Requires a newer version of Quickshell")
|
||||
description: !BlurService.available
|
||||
? I18n.tr("Requires a newer version of Quickshell")
|
||||
: (themeColorsTab.connectedFrameModeActive
|
||||
? I18n.tr("Connected Frame mode follows Frame Blur for connected surfaces while this remains the master blur availability toggle")
|
||||
: I18n.tr("Blur the background behind bars, popouts, modals, and notifications. Requires compositor support and configuration."))
|
||||
checked: SettingsData.blurEnabled ?? false
|
||||
enabled: BlurService.available
|
||||
onToggled: checked => SettingsData.set("blurEnabled", checked)
|
||||
|
||||
@@ -55,6 +55,192 @@ Item {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
SettingsCard {
|
||||
tab: "typography"
|
||||
tags: ["animation", "variant", "style", "slide", "fluent", "dynamic", "motion"]
|
||||
title: I18n.tr("Animation Style")
|
||||
settingKey: "animationVariant"
|
||||
iconName: "auto_awesome_motion"
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: animVariantGroup.implicitHeight
|
||||
clip: true
|
||||
|
||||
DankButtonGroup {
|
||||
id: animVariantGroup
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
buttonPadding: parent.width < 480 ? Theme.spacingS : Theme.spacingL
|
||||
minButtonWidth: parent.width < 480 ? 64 : 96
|
||||
textSize: parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium
|
||||
model: [I18n.tr("Material"), I18n.tr("Fluent"), I18n.tr("Dynamic")]
|
||||
selectionMode: "single"
|
||||
currentIndex: SettingsData.animationVariant
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (!selected)
|
||||
return;
|
||||
SettingsData.set("animationVariant", index);
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onAnimationVariantChanged() {
|
||||
animVariantGroup.currentIndex = SettingsData.animationVariant;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
opacity: 0.15
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: variantDescription.implicitHeight + Theme.spacingS * 2
|
||||
|
||||
StyledText {
|
||||
id: variantDescription
|
||||
x: Theme.spacingM
|
||||
y: Theme.spacingS
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
text: {
|
||||
switch (SettingsData.animationVariant) {
|
||||
case 1:
|
||||
return I18n.tr("Fluent: Smooth cubic deceleration in, quick snap out — clean, elegant curves.");
|
||||
case 2:
|
||||
return I18n.tr("Dynamic: Spring bezier with overshoot — entry briefly exceeds its target then settles. Expressive and alive.");
|
||||
default:
|
||||
return I18n.tr("Material: Material Design 3 Expressive bezier curves. The DMS default feel.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
tab: "typography"
|
||||
tags: ["animation", "motion", "effect", "slide", "directional", "depth", "spring", "physics"]
|
||||
title: I18n.tr("Motion Effects")
|
||||
settingKey: "motionEffect"
|
||||
iconName: "motion_photos_on"
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: motionEffectGroup.implicitHeight
|
||||
clip: true
|
||||
|
||||
DankButtonGroup {
|
||||
id: motionEffectGroup
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
buttonPadding: parent.width < 480 ? Theme.spacingS : Theme.spacingL
|
||||
minButtonWidth: parent.width < 480 ? 64 : 96
|
||||
textSize: parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium
|
||||
model: [I18n.tr("Standard"), I18n.tr("Directional"), I18n.tr("Depth")]
|
||||
selectionMode: "single"
|
||||
currentIndex: SettingsData.motionEffect
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (!selected)
|
||||
return;
|
||||
SettingsData.set("motionEffect", index);
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onMotionEffectChanged() {
|
||||
motionEffectGroup.currentIndex = SettingsData.motionEffect;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
opacity: 0.15
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: motionEffectDescription.implicitHeight + Theme.spacingS * 2
|
||||
|
||||
StyledText {
|
||||
id: motionEffectDescription
|
||||
x: Theme.spacingM
|
||||
y: Theme.spacingS
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
text: {
|
||||
switch (SettingsData.motionEffect) {
|
||||
case 1:
|
||||
return I18n.tr("Directional: Panels glide in from a larger distance at full size — no scale change, pure clean motion.");
|
||||
case 2:
|
||||
return I18n.tr("Depth: Panels scale up from small as they slide in — a dramatic pop-forward depth effect.");
|
||||
default:
|
||||
return I18n.tr("Standard: Classic Material Design 3 — panels rise from below with a subtle scale. The DMS default.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
opacity: 0.15
|
||||
visible: SettingsData.motionEffect === 1
|
||||
}
|
||||
|
||||
SettingsDropdownRow {
|
||||
visible: SettingsData.motionEffect === 1
|
||||
tab: "typography"
|
||||
tags: ["animation", "directional", "behavior", "overlap", "sticky", "roll", "connected"]
|
||||
settingKey: "directionalAnimationMode"
|
||||
text: I18n.tr("Directional Behavior")
|
||||
description: {
|
||||
if (SettingsData.connectedFrameModeActive)
|
||||
return I18n.tr("Popouts emerge flush from the bar edge as a single continuous piece, with corner connectors bridging the junction");
|
||||
return I18n.tr("How the popout emerges from the DankBar");
|
||||
}
|
||||
options: SettingsData.frameEnabled
|
||||
? [I18n.tr("Overlap"), I18n.tr("Slide"), I18n.tr("Roll"), I18n.tr("Connected")]
|
||||
: [I18n.tr("Overlap"), I18n.tr("Slide"), I18n.tr("Roll")]
|
||||
currentValue: {
|
||||
switch (SettingsData.directionalAnimationMode) {
|
||||
case 1:
|
||||
return I18n.tr("Slide");
|
||||
case 2:
|
||||
return I18n.tr("Roll");
|
||||
case 3:
|
||||
return SettingsData.frameEnabled ? I18n.tr("Connected") : I18n.tr("Slide");
|
||||
default:
|
||||
return I18n.tr("Overlap");
|
||||
}
|
||||
}
|
||||
onValueChanged: value => {
|
||||
if (value === I18n.tr("Slide"))
|
||||
SettingsData.set("directionalAnimationMode", 1);
|
||||
else if (value === I18n.tr("Roll"))
|
||||
SettingsData.set("directionalAnimationMode", 2);
|
||||
else if (value === I18n.tr("Connected") && SettingsData.frameEnabled) {
|
||||
if (SettingsData.directionalAnimationMode !== 3)
|
||||
SettingsData.set("previousDirectionalMode", SettingsData.directionalAnimationMode);
|
||||
SettingsData.set("directionalAnimationMode", 3);
|
||||
} else
|
||||
SettingsData.set("directionalAnimationMode", 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
tab: "typography"
|
||||
tags: ["font", "family", "text", "typography"]
|
||||
|
||||
@@ -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"];
|
||||
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion"];
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if (widget[keys[i]] !== undefined)
|
||||
result[keys[i]] = widget[keys[i]];
|
||||
@@ -712,6 +712,8 @@ 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"];
|
||||
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion"];
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
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"
|
||||
visible: modelData.id === "clock" || modelData.id === "focusedWindow" || modelData.id === "keyboard_layout_name" || modelData.id === "appsDock" || modelData.id === "systemTray"
|
||||
|
||||
DankActionButton {
|
||||
id: compactModeButton
|
||||
@@ -543,6 +543,39 @@ Column {
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: trayMenuButton
|
||||
buttonSize: 32
|
||||
visible: modelData.id === "systemTray"
|
||||
iconName: "more_vert"
|
||||
iconSize: 18
|
||||
iconColor: Theme.outline
|
||||
onClicked: {
|
||||
trayContextMenu.widgetData = modelData;
|
||||
trayContextMenu.sectionId = root.sectionId;
|
||||
trayContextMenu.widgetIndex = index;
|
||||
|
||||
var buttonPos = trayMenuButton.mapToItem(root, 0, 0);
|
||||
var popupWidth = trayContextMenu.width;
|
||||
var popupHeight = trayContextMenu.height;
|
||||
|
||||
var xPos = buttonPos.x - popupWidth - Theme.spacingS;
|
||||
if (xPos < 0)
|
||||
xPos = buttonPos.x + trayMenuButton.width + Theme.spacingS;
|
||||
|
||||
var yPos = buttonPos.y - popupHeight / 2 + trayMenuButton.height / 2;
|
||||
if (yPos < 0) {
|
||||
yPos = Theme.spacingS;
|
||||
} else if (yPos + popupHeight > root.height) {
|
||||
yPos = root.height - popupHeight - Theme.spacingS;
|
||||
}
|
||||
|
||||
trayContextMenu.x = xPos;
|
||||
trayContextMenu.y = yPos;
|
||||
trayContextMenu.open();
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: compactModeTooltip
|
||||
width: tooltipText.contentWidth + Theme.spacingM * 2
|
||||
@@ -931,6 +964,88 @@ Column {
|
||||
}
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: trayContextMenu
|
||||
|
||||
property var widgetData: null
|
||||
property string sectionId: ""
|
||||
property int widgetIndex: -1
|
||||
readonly property var currentWidgetData: (widgetIndex >= 0 && widgetIndex < root.items.length) ? root.items[widgetIndex] : widgetData
|
||||
|
||||
width: 220
|
||||
height: contentColumn.implicitHeight + Theme.spacingS * 2
|
||||
padding: 0
|
||||
modal: true
|
||||
focus: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
background: Rectangle {
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 0
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
Column {
|
||||
id: contentColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: 2
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: trayOverflowArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "arrow_selector_tool"
|
||||
size: 16
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Use Inline Expansion")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
id: trayOverflowToggle
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 40
|
||||
height: 20
|
||||
checked: trayContextMenu.currentWidgetData?.trayUseInlineExpansion ?? false
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: trayOverflowArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
const newValue = !(trayContextMenu.currentWidgetData?.trayUseInlineExpansion ?? false);
|
||||
root.overflowSettingChanged(trayContextMenu.sectionId, trayContextMenu.widgetIndex, "trayUseInlineExpansion", newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: diskUsageContextMenu
|
||||
|
||||
|
||||
@@ -121,9 +121,9 @@ Scope {
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,45 +154,69 @@ Scope {
|
||||
id: scaleTransform
|
||||
origin.x: contentContainer.width / 2
|
||||
origin.y: contentContainer.height / 2
|
||||
xScale: overviewScope.overviewOpen ? 1 : 0.96
|
||||
yScale: overviewScope.overviewOpen ? 1 : 0.96
|
||||
xScale: overviewScope.overviewOpen ? 1 : Theme.effectScaleCollapsed
|
||||
yScale: overviewScope.overviewOpen ? 1 : Theme.effectScaleCollapsed
|
||||
|
||||
Behavior on xScale {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on yScale {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Translate {
|
||||
id: motionTransform
|
||||
x: 0
|
||||
y: overviewScope.overviewOpen ? 0 : Theme.spacingL
|
||||
x: {
|
||||
if (overviewScope.overviewOpen)
|
||||
return 0;
|
||||
if (Theme.isDirectionalEffect)
|
||||
return 0;
|
||||
if (Theme.isDepthEffect)
|
||||
return Theme.effectAnimOffset * 0.25;
|
||||
return 0;
|
||||
}
|
||||
y: {
|
||||
if (overviewScope.overviewOpen)
|
||||
return 0;
|
||||
if (Theme.isDirectionalEffect)
|
||||
return -Math.max(contentContainer.height * 0.8, Theme.effectAnimOffset * 1.1);
|
||||
if (Theme.isDepthEffect)
|
||||
return Math.max(Theme.effectAnimOffset * 0.85, 28);
|
||||
return Theme.effectAnimOffset;
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -202,8 +202,18 @@ Scope {
|
||||
|
||||
Item {
|
||||
id: spotlightContainer
|
||||
x: Theme.snap((parent.width - width) / 2, overlayWindow.dpr)
|
||||
y: Theme.snap((parent.height - height) / 2, overlayWindow.dpr)
|
||||
readonly property bool directionalEffect: Theme.isDirectionalEffect
|
||||
readonly property bool depthEffect: Theme.isDepthEffect
|
||||
readonly property real collapsedMotionX: depthEffect ? Theme.effectAnimOffset * 0.25 : 0
|
||||
readonly property real collapsedMotionY: {
|
||||
if (directionalEffect)
|
||||
return Math.max(height * 0.85, Theme.effectAnimOffset * 1.1);
|
||||
if (depthEffect)
|
||||
return Math.max(Theme.effectAnimOffset * 0.8, 30);
|
||||
return 0;
|
||||
}
|
||||
x: Theme.snap((parent.width - width) / 2 + (overlayWindow.shouldShowSpotlight ? 0 : collapsedMotionX), overlayWindow.dpr)
|
||||
y: Theme.snap((parent.height - height) / 2 + (overlayWindow.shouldShowSpotlight ? 0 : collapsedMotionY), overlayWindow.dpr)
|
||||
|
||||
readonly property int baseWidth: {
|
||||
switch (SettingsData.dankLauncherV2Size) {
|
||||
@@ -234,8 +244,8 @@ Scope {
|
||||
|
||||
readonly property bool animatingOut: niriOverviewScope.isClosing && overlayWindow.isSpotlightScreen
|
||||
|
||||
scale: overlayWindow.shouldShowSpotlight ? 1.0 : 0.96
|
||||
opacity: overlayWindow.shouldShowSpotlight ? 1 : 0
|
||||
scale: Theme.isDirectionalEffect ? 1 : (overlayWindow.shouldShowSpotlight ? 1.0 : Theme.effectScaleCollapsed)
|
||||
opacity: Theme.isDirectionalEffect ? 1 : (overlayWindow.shouldShowSpotlight ? 1 : 0)
|
||||
visible: overlayWindow.shouldShowSpotlight || animatingOut
|
||||
enabled: overlayWindow.shouldShowSpotlight
|
||||
|
||||
@@ -245,10 +255,11 @@ Scope {
|
||||
|
||||
Behavior on scale {
|
||||
id: scaleAnimation
|
||||
enabled: !Theme.isDirectionalEffect
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.fast
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
|
||||
easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
onRunningChanged: {
|
||||
if (running || !spotlightContainer.animatingOut)
|
||||
return;
|
||||
@@ -258,10 +269,27 @@ Scope {
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: !Theme.isDirectionalEffect
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.fast
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
|
||||
easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,30 +62,30 @@ Item {
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||
easing.bezierCurve: Theme.variantModalEnterCurve
|
||||
}
|
||||
}
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||
easing.bezierCurve: Theme.variantModalEnterCurve
|
||||
}
|
||||
}
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||
easing.bezierCurve: Theme.variantModalEnterCurve
|
||||
}
|
||||
}
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||
easing.bezierCurve: Theme.variantModalEnterCurve
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,16 +124,16 @@ Item {
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||
easing.bezierCurve: Theme.variantModalEnterCurve
|
||||
}
|
||||
}
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||
easing.bezierCurve: Theme.variantModalEnterCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ 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
|
||||
|
||||
@@ -67,6 +68,16 @@ 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));
|
||||
@@ -395,7 +406,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; do
|
||||
for event_key in audio-volume-change power-plug power-unplug message message-new-instant desktop-login; do
|
||||
found=0
|
||||
|
||||
case "$event_key" in
|
||||
@@ -457,7 +468,8 @@ 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"
|
||||
"message-new-instant": "../assets/sounds/freedesktop/message-new-instant.wav",
|
||||
"desktop-login": "../assets/sounds/freedesktop/desktop-login.wav"
|
||||
};
|
||||
|
||||
const specialConditions = {
|
||||
@@ -551,6 +563,10 @@ EOFCONFIG
|
||||
criticalNotificationSound.destroy();
|
||||
criticalNotificationSound = null;
|
||||
}
|
||||
if (loginSound) {
|
||||
loginSound.destroy();
|
||||
loginSound = null;
|
||||
}
|
||||
}
|
||||
|
||||
function createSoundPlayers() {
|
||||
@@ -622,6 +638,19 @@ 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);
|
||||
}
|
||||
@@ -661,6 +690,31 @@ 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,6 +255,12 @@ Singleton {
|
||||
return pinnedEntries.some(pinnedEntry => pinnedEntry.hash === entryHash);
|
||||
}
|
||||
|
||||
onClipboardAvailableChanged: {
|
||||
if (!clipboardAvailable || refCount <= 0)
|
||||
return;
|
||||
refresh();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DMSService
|
||||
enabled: root.refCount > 0
|
||||
|
||||
@@ -4,10 +4,75 @@ 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: availablePlayers.find(p => p.isPlaying) ?? availablePlayers.find(p => p.canControl && p.canPlay) ?? null
|
||||
property MprisPlayer activePlayer: null
|
||||
|
||||
onAvailablePlayersChanged: _resolveActivePlayer()
|
||||
Component.onCompleted: _resolveActivePlayer()
|
||||
|
||||
Instantiator {
|
||||
model: root.availablePlayers
|
||||
delegate: Connections {
|
||||
required property MprisPlayer modelData
|
||||
target: modelData
|
||||
function onIsPlayingChanged() {
|
||||
if (modelData.isPlaying)
|
||||
root._resolveActivePlayer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _resolveActivePlayer(): void {
|
||||
const playing = availablePlayers.find(p => p.isPlaying);
|
||||
if (playing) {
|
||||
activePlayer = playing;
|
||||
_persistIdentity(playing.identity);
|
||||
return;
|
||||
}
|
||||
if (activePlayer && availablePlayers.indexOf(activePlayer) >= 0)
|
||||
return;
|
||||
const savedId = SessionData.lastPlayerIdentity;
|
||||
if (savedId) {
|
||||
const match = availablePlayers.find(p => p.identity === savedId);
|
||||
if (match) {
|
||||
activePlayer = match;
|
||||
return;
|
||||
}
|
||||
}
|
||||
activePlayer = availablePlayers.find(p => p.canControl && p.canPlay) ?? null;
|
||||
if (activePlayer)
|
||||
_persistIdentity(activePlayer.identity);
|
||||
}
|
||||
|
||||
function setActivePlayer(player: MprisPlayer): void {
|
||||
activePlayer = player;
|
||||
if (player)
|
||||
_persistIdentity(player.identity);
|
||||
}
|
||||
|
||||
function _persistIdentity(identity: string): void {
|
||||
if (identity && SessionData.lastPlayerIdentity !== identity)
|
||||
SessionData.set("lastPlayerIdentity", identity);
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 1000
|
||||
running: root.activePlayer?.playbackState === MprisPlaybackState.Playing
|
||||
repeat: true
|
||||
onTriggered: root.activePlayer?.positionChanged()
|
||||
}
|
||||
|
||||
function previousOrRewind(): void {
|
||||
if (!activePlayer)
|
||||
return;
|
||||
if (activePlayer.position > 8 && activePlayer.canSeek)
|
||||
activePlayer.position = 0;
|
||||
else if (activePlayer.canGoPrevious)
|
||||
activePlayer.previous();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -58,6 +59,10 @@ Singleton {
|
||||
}
|
||||
|
||||
readonly property bool screensharingActive: {
|
||||
if (CompositorService.isNiri && NiriService.hasActiveCast) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!Pipewire.ready || !Pipewire.nodes?.values) {
|
||||
return false
|
||||
}
|
||||
@@ -74,6 +79,12 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
if (node.properties && node.properties["media.class"] === "Stream/Output/Video") {
|
||||
if (looksLikeScreencast(node)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (node.properties && node.properties["media.class"] === "Stream/Input/Audio") {
|
||||
const mediaName = (node.properties["media.name"] || "").toLowerCase()
|
||||
const appName = (node.properties["application.name"] || "").toLowerCase()
|
||||
@@ -110,8 +121,9 @@ Singleton {
|
||||
}
|
||||
const appName = (node.properties && node.properties["application.name"] || "").toLowerCase()
|
||||
const nodeName = (node.name || "").toLowerCase()
|
||||
const combined = appName + " " + nodeName
|
||||
return /xdg-desktop-portal|xdpw|screencast|screen|gnome shell|kwin|obs/.test(combined)
|
||||
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)
|
||||
}
|
||||
|
||||
function getMicrophoneStatus() {
|
||||
|
||||
@@ -231,7 +231,10 @@ Singleton {
|
||||
return;
|
||||
isChecking = true;
|
||||
hasError = false;
|
||||
if (updChecker.length > 0) {
|
||||
if (pkgManager === "paru" || pkgManager === "yay") {
|
||||
const repoCmd = updChecker.length > 0 ? updChecker : `${pkgManager} -Qu`;
|
||||
updateChecker.command = ["sh", "-c", `(${repoCmd} 2>/dev/null; ${pkgManager} -Qua 2>/dev/null) || true`];
|
||||
} else if (updChecker.length > 0) {
|
||||
updateChecker.command = [updChecker].concat(updateCheckerParams[updChecker].listUpdatesSettings.params);
|
||||
} else {
|
||||
updateChecker.command = [pkgManager].concat(packageManagerParams[pkgManager].listUpdatesSettings.params);
|
||||
|
||||
159
quickshell/Widgets/ConnectedCorner.qml
Normal file
159
quickshell/Widgets/ConnectedCorner.qml
Normal file
@@ -0,0 +1,159 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.Common
|
||||
|
||||
// Concave arc connector filling the gap between a bar corner and an adjacent surface.
|
||||
//
|
||||
// NOTE: FrameWindow now uses ConnectedShape.qml for frame-owned connected chrome
|
||||
// (unified single-path rendering). This component is still used by DankPopout's
|
||||
// own shadow source for non-frame-owned chrome (popouts on non-frame screens).
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string barSide: "top"
|
||||
property string placement: "left"
|
||||
property real spacing: 4
|
||||
property real connectorRadius: 12
|
||||
property color color: "transparent"
|
||||
property real edgeStrokeWidth: 0
|
||||
property color edgeStrokeColor: color
|
||||
property real dpr: 1
|
||||
|
||||
readonly property bool isHorizontalBar: barSide === "top" || barSide === "bottom"
|
||||
readonly property bool isPlacementLeft: placement === "left"
|
||||
readonly property real _edgeStrokeWidth: Math.max(0, edgeStrokeWidth)
|
||||
readonly property string arcCorner: {
|
||||
if (barSide === "top")
|
||||
return isPlacementLeft ? "bottomLeft" : "bottomRight";
|
||||
if (barSide === "bottom")
|
||||
return isPlacementLeft ? "topLeft" : "topRight";
|
||||
if (barSide === "left")
|
||||
return isPlacementLeft ? "topRight" : "bottomRight";
|
||||
return isPlacementLeft ? "topLeft" : "bottomLeft";
|
||||
}
|
||||
readonly property real pathStartX: {
|
||||
switch (arcCorner) {
|
||||
case "topLeft":
|
||||
return width;
|
||||
case "topRight":
|
||||
case "bottomLeft":
|
||||
return 0;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
readonly property real pathStartY: {
|
||||
switch (arcCorner) {
|
||||
case "bottomRight":
|
||||
return height;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
readonly property real firstLineX: {
|
||||
switch (arcCorner) {
|
||||
case "topLeft":
|
||||
case "bottomLeft":
|
||||
return width;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
readonly property real firstLineY: {
|
||||
switch (arcCorner) {
|
||||
case "topLeft":
|
||||
case "topRight":
|
||||
return height;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
readonly property real secondLineX: {
|
||||
switch (arcCorner) {
|
||||
case "topRight":
|
||||
case "bottomLeft":
|
||||
case "bottomRight":
|
||||
return width;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
readonly property real secondLineY: {
|
||||
switch (arcCorner) {
|
||||
case "topLeft":
|
||||
case "topRight":
|
||||
case "bottomLeft":
|
||||
return height;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
readonly property real arcCenterX: arcCorner === "topRight" || arcCorner === "bottomRight" ? width : 0
|
||||
readonly property real arcCenterY: arcCorner === "bottomLeft" || arcCorner === "bottomRight" ? height : 0
|
||||
readonly property real arcStartAngle: {
|
||||
switch (arcCorner) {
|
||||
case "topLeft":
|
||||
case "topRight":
|
||||
return 90;
|
||||
case "bottomLeft":
|
||||
return 0;
|
||||
default:
|
||||
return -90;
|
||||
}
|
||||
}
|
||||
readonly property real arcSweepAngle: {
|
||||
switch (arcCorner) {
|
||||
case "topRight":
|
||||
return 90;
|
||||
default:
|
||||
return -90;
|
||||
}
|
||||
}
|
||||
|
||||
width: isHorizontalBar ? connectorRadius : (spacing + connectorRadius)
|
||||
height: isHorizontalBar ? (spacing + connectorRadius) : connectorRadius
|
||||
|
||||
Shape {
|
||||
x: -root._edgeStrokeWidth
|
||||
y: -root._edgeStrokeWidth
|
||||
width: root.width + root._edgeStrokeWidth * 2
|
||||
height: root.height + root._edgeStrokeWidth * 2
|
||||
asynchronous: false
|
||||
antialiasing: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0)
|
||||
|
||||
ShapePath {
|
||||
fillColor: root.color
|
||||
strokeColor: root._edgeStrokeWidth > 0 ? root.edgeStrokeColor : "transparent"
|
||||
strokeWidth: root._edgeStrokeWidth * 2
|
||||
joinStyle: ShapePath.RoundJoin
|
||||
capStyle: ShapePath.RoundCap
|
||||
fillRule: ShapePath.WindingFill
|
||||
startX: root.pathStartX + root._edgeStrokeWidth
|
||||
startY: root.pathStartY + root._edgeStrokeWidth
|
||||
|
||||
PathLine {
|
||||
x: root.firstLineX + root._edgeStrokeWidth
|
||||
y: root.firstLineY + root._edgeStrokeWidth
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: root.secondLineX + root._edgeStrokeWidth
|
||||
y: root.secondLineY + root._edgeStrokeWidth
|
||||
}
|
||||
|
||||
PathAngleArc {
|
||||
centerX: root.arcCenterX + root._edgeStrokeWidth
|
||||
centerY: root.arcCenterY + root._edgeStrokeWidth
|
||||
radiusX: root.connectorRadius
|
||||
radiusY: root.connectorRadius
|
||||
startAngle: root.arcStartAngle
|
||||
sweepAngle: root.arcSweepAngle
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
286
quickshell/Widgets/ConnectedShape.qml
Normal file
286
quickshell/Widgets/ConnectedShape.qml
Normal file
@@ -0,0 +1,286 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.Common
|
||||
|
||||
// Unified connected silhouette: body + concave arcs as one ShapePath.
|
||||
// PathArc pattern — 4 arcs + 4 lines, no sibling alignment.
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string barSide: "top"
|
||||
|
||||
property real bodyWidth: 0
|
||||
property real bodyHeight: 0
|
||||
|
||||
property real connectorRadius: 12
|
||||
|
||||
property real surfaceRadius: 12
|
||||
|
||||
property color fillColor: "transparent"
|
||||
|
||||
// ── Derived layout ──
|
||||
readonly property bool _horiz: barSide === "top" || barSide === "bottom"
|
||||
readonly property real _cr: Math.max(0, connectorRadius)
|
||||
readonly property real _sr: Math.max(0, Math.min(surfaceRadius, (_horiz ? bodyWidth : bodyHeight) / 2, (_horiz ? bodyHeight : bodyWidth) / 2))
|
||||
|
||||
// Root-level aliases — PathArc/PathLine elements can't use `parent`.
|
||||
readonly property real _bw: bodyWidth
|
||||
readonly property real _bh: bodyHeight
|
||||
readonly property real _totalW: _horiz ? _bw + _cr * 2 : _bw
|
||||
readonly property real _totalH: _horiz ? _bh : _bh + _cr * 2
|
||||
|
||||
width: _totalW
|
||||
height: _totalH
|
||||
|
||||
readonly property real bodyX: _horiz ? _cr : 0
|
||||
readonly property real bodyY: _horiz ? 0 : _cr
|
||||
|
||||
Shape {
|
||||
anchors.fill: parent
|
||||
asynchronous: false
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
fillColor: root.fillColor
|
||||
strokeWidth: -1
|
||||
fillRule: ShapePath.WindingFill
|
||||
|
||||
// CW path: bar edge → concave arc → body → convex arc → far edge → convex arc → body → concave arc
|
||||
|
||||
startX: root.barSide === "right" ? root._totalW : 0
|
||||
startY: {
|
||||
switch (root.barSide) {
|
||||
case "bottom":
|
||||
return root._totalH;
|
||||
case "left":
|
||||
return root._totalH;
|
||||
case "right":
|
||||
return 0;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Bar edge
|
||||
PathLine {
|
||||
x: {
|
||||
switch (root.barSide) {
|
||||
case "left":
|
||||
return 0;
|
||||
case "right":
|
||||
return root._totalW;
|
||||
default:
|
||||
return root._totalW;
|
||||
}
|
||||
}
|
||||
y: {
|
||||
switch (root.barSide) {
|
||||
case "bottom":
|
||||
return root._totalH;
|
||||
case "left":
|
||||
return 0;
|
||||
case "right":
|
||||
return root._totalH;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Concave arc 1
|
||||
PathArc {
|
||||
relativeX: {
|
||||
switch (root.barSide) {
|
||||
case "left":
|
||||
return root._cr;
|
||||
case "right":
|
||||
return -root._cr;
|
||||
default:
|
||||
return -root._cr;
|
||||
}
|
||||
}
|
||||
relativeY: {
|
||||
switch (root.barSide) {
|
||||
case "bottom":
|
||||
return -root._cr;
|
||||
case "left":
|
||||
return root._cr;
|
||||
case "right":
|
||||
return -root._cr;
|
||||
default:
|
||||
return root._cr;
|
||||
}
|
||||
}
|
||||
radiusX: root._cr
|
||||
radiusY: root._cr
|
||||
direction: root.barSide === "bottom" ? PathArc.Clockwise : PathArc.Counterclockwise
|
||||
}
|
||||
|
||||
// Body edge to first convex corner
|
||||
PathLine {
|
||||
x: {
|
||||
switch (root.barSide) {
|
||||
case "left":
|
||||
return root._bw - root._sr;
|
||||
case "right":
|
||||
return root._sr;
|
||||
default:
|
||||
return root._totalW - root._cr;
|
||||
}
|
||||
}
|
||||
y: {
|
||||
switch (root.barSide) {
|
||||
case "bottom":
|
||||
return root._sr;
|
||||
case "left":
|
||||
return root._cr;
|
||||
case "right":
|
||||
return root._cr + root._bh;
|
||||
default:
|
||||
return root._totalH - root._sr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convex arc 1
|
||||
PathArc {
|
||||
relativeX: {
|
||||
switch (root.barSide) {
|
||||
case "left":
|
||||
return root._sr;
|
||||
case "right":
|
||||
return -root._sr;
|
||||
default:
|
||||
return -root._sr;
|
||||
}
|
||||
}
|
||||
relativeY: {
|
||||
switch (root.barSide) {
|
||||
case "bottom":
|
||||
return -root._sr;
|
||||
case "left":
|
||||
return root._sr;
|
||||
case "right":
|
||||
return -root._sr;
|
||||
default:
|
||||
return root._sr;
|
||||
}
|
||||
}
|
||||
radiusX: root._sr
|
||||
radiusY: root._sr
|
||||
direction: root.barSide === "bottom" ? PathArc.Counterclockwise : PathArc.Clockwise
|
||||
}
|
||||
|
||||
// Far edge
|
||||
PathLine {
|
||||
x: {
|
||||
switch (root.barSide) {
|
||||
case "left":
|
||||
return root._bw;
|
||||
case "right":
|
||||
return 0;
|
||||
default:
|
||||
return root._cr + root._sr;
|
||||
}
|
||||
}
|
||||
y: {
|
||||
switch (root.barSide) {
|
||||
case "bottom":
|
||||
return 0;
|
||||
case "left":
|
||||
return root._cr + root._bh - root._sr;
|
||||
case "right":
|
||||
return root._cr + root._sr;
|
||||
default:
|
||||
return root._totalH;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convex arc 2
|
||||
PathArc {
|
||||
relativeX: {
|
||||
switch (root.barSide) {
|
||||
case "left":
|
||||
return -root._sr;
|
||||
case "right":
|
||||
return root._sr;
|
||||
default:
|
||||
return -root._sr;
|
||||
}
|
||||
}
|
||||
relativeY: {
|
||||
switch (root.barSide) {
|
||||
case "bottom":
|
||||
return root._sr;
|
||||
case "left":
|
||||
return root._sr;
|
||||
case "right":
|
||||
return -root._sr;
|
||||
default:
|
||||
return -root._sr;
|
||||
}
|
||||
}
|
||||
radiusX: root._sr
|
||||
radiusY: root._sr
|
||||
direction: root.barSide === "bottom" ? PathArc.Counterclockwise : PathArc.Clockwise
|
||||
}
|
||||
|
||||
// Body edge to second concave arc
|
||||
PathLine {
|
||||
x: {
|
||||
switch (root.barSide) {
|
||||
case "left":
|
||||
return root._cr;
|
||||
case "right":
|
||||
return root._bw - root._cr;
|
||||
default:
|
||||
return root._cr;
|
||||
}
|
||||
}
|
||||
y: {
|
||||
switch (root.barSide) {
|
||||
case "bottom":
|
||||
return root._totalH - root._cr;
|
||||
case "left":
|
||||
return root._cr + root._bh;
|
||||
case "right":
|
||||
return root._cr;
|
||||
default:
|
||||
return root._cr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Concave arc 2
|
||||
PathArc {
|
||||
relativeX: {
|
||||
switch (root.barSide) {
|
||||
case "left":
|
||||
return -root._cr;
|
||||
case "right":
|
||||
return root._cr;
|
||||
default:
|
||||
return -root._cr;
|
||||
}
|
||||
}
|
||||
relativeY: {
|
||||
switch (root.barSide) {
|
||||
case "bottom":
|
||||
return root._cr;
|
||||
case "left":
|
||||
return root._cr;
|
||||
case "right":
|
||||
return -root._cr;
|
||||
default:
|
||||
return -root._cr;
|
||||
}
|
||||
}
|
||||
radiusX: root._cr
|
||||
radiusY: root._cr
|
||||
direction: root.barSide === "bottom" ? PathArc.Clockwise : PathArc.Counterclockwise
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,10 @@ Item {
|
||||
property string triggerSection: ""
|
||||
property string positioning: "center"
|
||||
property int animationDuration: Theme.popoutAnimationDuration
|
||||
property real animationScaleCollapsed: 0.96
|
||||
property real animationOffset: Theme.spacingL
|
||||
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
|
||||
property real animationScaleCollapsed: Theme.effectScaleCollapsed
|
||||
property real animationOffset: Theme.effectAnimOffset
|
||||
property list<real> animationEnterCurve: Theme.variantPopoutEnterCurve
|
||||
property list<real> animationExitCurve: Theme.variantPopoutExitCurve
|
||||
property bool suspendShadowWhileResizing: false
|
||||
property bool shouldBeVisible: false
|
||||
property var customKeyboardFocus: null
|
||||
@@ -34,6 +34,8 @@ Item {
|
||||
property bool _resizeActive: false
|
||||
property real _surfaceMarginLeft: 0
|
||||
property real _surfaceW: 0
|
||||
property string _chromeClaimId: ""
|
||||
property int _connectedChromeSerial: 0
|
||||
|
||||
property real storedBarThickness: Theme.barHeight - 4
|
||||
property real storedBarSpacing: 4
|
||||
@@ -47,6 +49,8 @@ Item {
|
||||
property var screen: null
|
||||
|
||||
readonly property real effectiveBarThickness: {
|
||||
if (Theme.isConnectedEffect)
|
||||
return Math.max(0, storedBarThickness);
|
||||
const padding = storedBarConfig ? (storedBarConfig.innerPadding !== undefined ? storedBarConfig.innerPadding : 4) : 4;
|
||||
return Math.max(26 + padding * 0.6, Theme.barHeight - 4 - (8 - padding)) + storedBarSpacing;
|
||||
}
|
||||
@@ -68,12 +72,14 @@ Item {
|
||||
readonly property real barWidth: barBounds.width
|
||||
readonly property real barHeight: barBounds.height
|
||||
readonly property real barWingSize: barBounds.wingSize
|
||||
readonly property bool effectiveSurfaceBlurEnabled: Theme.connectedSurfaceBlurEnabled
|
||||
|
||||
signal opened
|
||||
signal popoutClosed
|
||||
signal backgroundClicked
|
||||
|
||||
property var _lastOpenedScreen: null
|
||||
property bool isClosing: false
|
||||
|
||||
property int effectiveBarPosition: 0
|
||||
property real effectiveBarBottomGap: 0
|
||||
@@ -147,7 +153,102 @@ Item {
|
||||
setBarContext(pos, bottomGap);
|
||||
}
|
||||
|
||||
function _nextChromeClaimId() {
|
||||
_connectedChromeSerial += 1;
|
||||
return layerNamespace + ":" + _connectedChromeSerial + ":" + (new Date()).getTime();
|
||||
}
|
||||
|
||||
function _connectedChromeState(visibleOverride) {
|
||||
const visible = visibleOverride !== undefined ? !!visibleOverride : contentWindow.visible;
|
||||
return {
|
||||
"visible": visible,
|
||||
"barSide": contentContainer.connectedBarSide,
|
||||
"bodyX": root.alignedX,
|
||||
"bodyY": root.alignedY,
|
||||
"bodyW": root.alignedWidth,
|
||||
"bodyH": root.alignedHeight,
|
||||
"animX": contentContainer.animX,
|
||||
"animY": contentContainer.animY,
|
||||
"screen": root.screen ? root.screen.name : ""
|
||||
};
|
||||
}
|
||||
|
||||
function _publishConnectedChromeState(forceClaim, visibleOverride) {
|
||||
if (!root.frameOwnsConnectedChrome || !root.screen || !_chromeClaimId)
|
||||
return;
|
||||
|
||||
const state = _connectedChromeState(visibleOverride);
|
||||
if (forceClaim || !ConnectedModeState.hasPopoutOwner(_chromeClaimId)) {
|
||||
ConnectedModeState.claimPopout(_chromeClaimId, state);
|
||||
} else {
|
||||
ConnectedModeState.updatePopout(_chromeClaimId, state);
|
||||
}
|
||||
}
|
||||
|
||||
function _releaseConnectedChromeState() {
|
||||
if (_chromeClaimId)
|
||||
ConnectedModeState.releasePopout(_chromeClaimId);
|
||||
_chromeClaimId = "";
|
||||
}
|
||||
|
||||
// ─── Exposed animation state for ConnectedModeState ────────────────────
|
||||
readonly property real contentAnimX: contentContainer.animX
|
||||
readonly property real contentAnimY: contentContainer.animY
|
||||
|
||||
// ─── ConnectedModeState sync ────────────────────────────────────────────
|
||||
function _syncPopoutChromeState() {
|
||||
if (!root.frameOwnsConnectedChrome) {
|
||||
_releaseConnectedChromeState();
|
||||
return;
|
||||
}
|
||||
if (!root.screen) {
|
||||
_releaseConnectedChromeState();
|
||||
return;
|
||||
}
|
||||
if (!contentWindow.visible && !shouldBeVisible)
|
||||
return;
|
||||
if (!_chromeClaimId)
|
||||
_chromeClaimId = _nextChromeClaimId();
|
||||
_publishConnectedChromeState(contentWindow.visible && !ConnectedModeState.hasPopoutOwner(_chromeClaimId));
|
||||
}
|
||||
|
||||
onAlignedXChanged: _syncPopoutChromeState()
|
||||
onAlignedYChanged: _syncPopoutChromeState()
|
||||
onAlignedWidthChanged: _syncPopoutChromeState()
|
||||
onContentAnimXChanged: _syncPopoutChromeState()
|
||||
onContentAnimYChanged: _syncPopoutChromeState()
|
||||
onScreenChanged: _syncPopoutChromeState()
|
||||
onEffectiveBarPositionChanged: _syncPopoutChromeState()
|
||||
|
||||
Connections {
|
||||
target: contentWindow
|
||||
function onVisibleChanged() {
|
||||
if (contentWindow.visible)
|
||||
root._publishConnectedChromeState(true);
|
||||
else
|
||||
root._releaseConnectedChromeState();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onConnectedFrameModeActiveChanged() {
|
||||
if (root.frameOwnsConnectedChrome) {
|
||||
if (contentWindow.visible || root.shouldBeVisible) {
|
||||
if (!root._chromeClaimId)
|
||||
root._chromeClaimId = root._nextChromeClaimId();
|
||||
root._publishConnectedChromeState(true);
|
||||
}
|
||||
} else {
|
||||
root._releaseConnectedChromeState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property bool useBackgroundWindow: !CompositorService.isHyprland || CompositorService.useHyprlandFocusGrab
|
||||
readonly property bool frameOwnsConnectedChrome: SettingsData.connectedFrameModeActive
|
||||
&& !!root.screen
|
||||
&& SettingsData.isScreenInPreferences(root.screen, SettingsData.frameScreenPreferences)
|
||||
|
||||
function updateSurfacePosition() {
|
||||
if (useBackgroundWindow && shouldBeVisible) {
|
||||
@@ -156,10 +257,14 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
property bool animationsEnabled: true
|
||||
|
||||
function open() {
|
||||
if (!screen)
|
||||
return;
|
||||
closeTimer.stop();
|
||||
isClosing = false;
|
||||
animationsEnabled = false;
|
||||
|
||||
// Snapshot mask geometry
|
||||
_frozenMaskX = maskX;
|
||||
@@ -174,12 +279,29 @@ Item {
|
||||
}
|
||||
_lastOpenedScreen = screen;
|
||||
|
||||
shouldBeVisible = true;
|
||||
if (contentContainer) {
|
||||
contentContainer.animX = Theme.snap(contentContainer.offsetX, root.dpr);
|
||||
contentContainer.animY = Theme.snap(contentContainer.offsetY, root.dpr);
|
||||
contentContainer.scaleValue = root.animationScaleCollapsed;
|
||||
}
|
||||
|
||||
if (root.frameOwnsConnectedChrome) {
|
||||
_chromeClaimId = _nextChromeClaimId();
|
||||
_publishConnectedChromeState(true, true);
|
||||
} else {
|
||||
_chromeClaimId = "";
|
||||
}
|
||||
|
||||
if (useBackgroundWindow) {
|
||||
_surfaceMarginLeft = alignedX - shadowBuffer;
|
||||
_surfaceW = alignedWidth + shadowBuffer * 2;
|
||||
backgroundWindow.visible = true;
|
||||
}
|
||||
contentWindow.visible = true;
|
||||
|
||||
Qt.callLater(() => {
|
||||
animationsEnabled = true;
|
||||
shouldBeVisible = true;
|
||||
if (shouldBeVisible && screen) {
|
||||
if (useBackgroundWindow)
|
||||
backgroundWindow.visible = true;
|
||||
@@ -191,6 +313,7 @@ Item {
|
||||
}
|
||||
|
||||
function close() {
|
||||
isClosing = true;
|
||||
shouldBeVisible = false;
|
||||
_primeContent = false;
|
||||
PopoutManager.popoutChanged();
|
||||
@@ -222,9 +345,10 @@ Item {
|
||||
|
||||
Timer {
|
||||
id: closeTimer
|
||||
interval: animationDuration
|
||||
interval: Theme.variantCloseInterval(animationDuration)
|
||||
onTriggered: {
|
||||
if (!shouldBeVisible) {
|
||||
isClosing = false;
|
||||
contentWindow.visible = false;
|
||||
if (useBackgroundWindow)
|
||||
backgroundWindow.visible = false;
|
||||
@@ -234,19 +358,80 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: _releaseConnectedChromeState()
|
||||
|
||||
readonly property real screenWidth: screen ? screen.width : 0
|
||||
readonly property real screenHeight: screen ? screen.height : 0
|
||||
readonly property real dpr: screen ? screen.devicePixelRatio : 1
|
||||
readonly property real frameInset: {
|
||||
if (!SettingsData.frameEnabled)
|
||||
return 0;
|
||||
const ft = SettingsData.frameThickness;
|
||||
const fr = SettingsData.frameRounding;
|
||||
const ccr = Theme.connectedCornerRadius;
|
||||
if (Theme.isConnectedEffect)
|
||||
return Math.max(ft * 4, ft + ccr * 2);
|
||||
const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true;
|
||||
const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 6;
|
||||
const gap = useAutoGaps ? Math.max(6, storedBarSpacing) : manualGapValue;
|
||||
return Math.max(ft + gap, fr);
|
||||
}
|
||||
|
||||
readonly property var shadowLevel: Theme.elevationLevel3
|
||||
readonly property real shadowFallbackOffset: 6
|
||||
readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.popoutElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, effectiveShadowDirection, shadowFallbackOffset, 8, 16) : 0
|
||||
readonly property real shadowMotionPadding: Math.max(0, animationOffset)
|
||||
readonly property real shadowMotionPadding: {
|
||||
if (Theme.isConnectedEffect)
|
||||
return Math.max(storedBarSpacing + Theme.connectedCornerRadius + 4, 40);
|
||||
if (Theme.isDirectionalEffect) {
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode !== 0)
|
||||
return 16; // Slide Behind and Roll Out do not add animationOffset, enabling strict Wayland clipping.
|
||||
return Math.max(0, animationOffset) + 16;
|
||||
}
|
||||
if (Theme.isDepthEffect)
|
||||
return Math.max(0, animationOffset) + 8;
|
||||
return Math.max(0, animationOffset);
|
||||
}
|
||||
readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
|
||||
readonly property real alignedWidth: Theme.px(popupWidth, dpr)
|
||||
readonly property real alignedHeight: Theme.px(popupHeight, dpr)
|
||||
readonly property real connectedAnchorX: {
|
||||
if (!Theme.isConnectedEffect)
|
||||
return triggerX;
|
||||
switch (effectiveBarPosition) {
|
||||
case SettingsData.Position.Left:
|
||||
return barX + barWidth;
|
||||
case SettingsData.Position.Right:
|
||||
return barX;
|
||||
default:
|
||||
return triggerX;
|
||||
}
|
||||
}
|
||||
readonly property real connectedAnchorY: {
|
||||
if (!Theme.isConnectedEffect)
|
||||
return triggerY;
|
||||
switch (effectiveBarPosition) {
|
||||
case SettingsData.Position.Top:
|
||||
return barY + barHeight;
|
||||
case SettingsData.Position.Bottom:
|
||||
return barY;
|
||||
default:
|
||||
return triggerY;
|
||||
}
|
||||
}
|
||||
|
||||
function adjacentBarClearance(exclusion) {
|
||||
if (exclusion <= 0)
|
||||
return 0;
|
||||
if (!Theme.isConnectedEffect)
|
||||
return exclusion;
|
||||
// In a shared frame corner, the adjacent connected bar already occupies
|
||||
// one rounded-corner radius before the popout's own connector begins.
|
||||
return exclusion + Theme.connectedCornerRadius * 2;
|
||||
}
|
||||
|
||||
onAlignedHeightChanged: {
|
||||
_syncPopoutChromeState();
|
||||
if (!suspendShadowWhileResizing || !shouldBeVisible)
|
||||
return;
|
||||
_resizeActive = true;
|
||||
@@ -269,17 +454,22 @@ Item {
|
||||
readonly property real alignedX: Theme.snap((() => {
|
||||
const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true;
|
||||
const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 4;
|
||||
const popupGap = useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue;
|
||||
const rawPopupGap = useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue;
|
||||
const popupGap = Theme.isConnectedEffect ? 0 : rawPopupGap;
|
||||
const edgeGap = Math.max(popupGap, frameInset);
|
||||
const anchorX = Theme.isConnectedEffect ? connectedAnchorX : triggerX;
|
||||
|
||||
switch (effectiveBarPosition) {
|
||||
case SettingsData.Position.Left:
|
||||
return Math.max(popupGap, Math.min(screenWidth - popupWidth - popupGap, triggerX));
|
||||
// bar on left: left side is bar-adjacent (popupGap), right side is frame-perpendicular (edgeGap)
|
||||
return Math.max(popupGap, Math.min(screenWidth - popupWidth - edgeGap, anchorX));
|
||||
case SettingsData.Position.Right:
|
||||
return Math.max(popupGap, Math.min(screenWidth - popupWidth - popupGap, triggerX - popupWidth));
|
||||
// bar on right: right side is bar-adjacent (popupGap), left side is frame-perpendicular (edgeGap)
|
||||
return Math.max(edgeGap, Math.min(screenWidth - popupWidth - popupGap, anchorX - popupWidth));
|
||||
default:
|
||||
const rawX = triggerX + (triggerWidth / 2) - (popupWidth / 2);
|
||||
const minX = adjacentBarInfo.leftBar > 0 ? adjacentBarInfo.leftBar : popupGap;
|
||||
const maxX = screenWidth - popupWidth - (adjacentBarInfo.rightBar > 0 ? adjacentBarInfo.rightBar : popupGap);
|
||||
const minX = Math.max(edgeGap, adjacentBarClearance(adjacentBarInfo.leftBar));
|
||||
const maxX = screenWidth - popupWidth - Math.max(edgeGap, adjacentBarClearance(adjacentBarInfo.rightBar));
|
||||
return Math.max(minX, Math.min(maxX, rawX));
|
||||
}
|
||||
})(), dpr)
|
||||
@@ -287,17 +477,22 @@ Item {
|
||||
readonly property real alignedY: Theme.snap((() => {
|
||||
const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true;
|
||||
const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 4;
|
||||
const popupGap = useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue;
|
||||
const rawPopupGap = useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue;
|
||||
const popupGap = Theme.isConnectedEffect ? 0 : rawPopupGap;
|
||||
const edgeGap = Math.max(popupGap, frameInset);
|
||||
const anchorY = Theme.isConnectedEffect ? connectedAnchorY : triggerY;
|
||||
|
||||
switch (effectiveBarPosition) {
|
||||
case SettingsData.Position.Bottom:
|
||||
return Math.max(popupGap, Math.min(screenHeight - popupHeight - popupGap, triggerY - popupHeight));
|
||||
// bar on bottom: bottom side is bar-adjacent (popupGap), top side is frame-perpendicular (edgeGap)
|
||||
return Math.max(edgeGap, Math.min(screenHeight - popupHeight - popupGap, anchorY - popupHeight));
|
||||
case SettingsData.Position.Top:
|
||||
return Math.max(popupGap, Math.min(screenHeight - popupHeight - popupGap, triggerY));
|
||||
// bar on top: top side is bar-adjacent (popupGap), bottom side is frame-perpendicular (edgeGap)
|
||||
return Math.max(popupGap, Math.min(screenHeight - popupHeight - edgeGap, anchorY));
|
||||
default:
|
||||
const rawY = triggerY - (popupHeight / 2);
|
||||
const minY = adjacentBarInfo.topBar > 0 ? adjacentBarInfo.topBar : popupGap;
|
||||
const maxY = screenHeight - popupHeight - (adjacentBarInfo.bottomBar > 0 ? adjacentBarInfo.bottomBar : popupGap);
|
||||
const minY = Math.max(edgeGap, adjacentBarClearance(adjacentBarInfo.topBar));
|
||||
const maxY = screenHeight - popupHeight - Math.max(edgeGap, adjacentBarClearance(adjacentBarInfo.bottomBar));
|
||||
return Math.max(minY, Math.min(maxY, rawY));
|
||||
}
|
||||
})(), dpr)
|
||||
@@ -353,6 +548,10 @@ Item {
|
||||
|
||||
mask: Region {
|
||||
item: maskRect
|
||||
Region {
|
||||
item: contentExclusionRect
|
||||
intersection: Intersection.Subtract
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@@ -361,26 +560,70 @@ Item {
|
||||
color: "transparent"
|
||||
x: root._frozenMaskX
|
||||
y: root._frozenMaskY
|
||||
width: (shouldBeVisible && backgroundInteractive) ? root._frozenMaskWidth : 0
|
||||
height: (shouldBeVisible && backgroundInteractive) ? root._frozenMaskHeight : 0
|
||||
width: (backgroundWindow.visible && backgroundInteractive) ? root._frozenMaskWidth : 0
|
||||
height: (backgroundWindow.visible && backgroundInteractive) ? root._frozenMaskHeight : 0
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
Item {
|
||||
id: contentExclusionRect
|
||||
visible: false
|
||||
x: root.alignedX
|
||||
y: root.alignedY
|
||||
width: root.alignedWidth
|
||||
height: root.alignedHeight
|
||||
}
|
||||
|
||||
Item {
|
||||
id: outsideClickCatcher
|
||||
x: root._frozenMaskX
|
||||
y: root._frozenMaskY
|
||||
width: root._frozenMaskWidth
|
||||
height: root._frozenMaskHeight
|
||||
hoverEnabled: false
|
||||
enabled: shouldBeVisible && backgroundInteractive
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onClicked: mouse => {
|
||||
const clickX = mouse.x + root._frozenMaskX;
|
||||
const clickY = mouse.y + root._frozenMaskY;
|
||||
const outsideContent = clickX < root.alignedX || clickX > root.alignedX + root.alignedWidth || clickY < root.alignedY || clickY > root.alignedY + root.alignedHeight;
|
||||
enabled: root.shouldBeVisible && root.backgroundInteractive
|
||||
|
||||
if (!outsideContent)
|
||||
return;
|
||||
backgroundClicked();
|
||||
readonly property real contentLeft: Math.max(0, root.alignedX - x)
|
||||
readonly property real contentTop: Math.max(0, root.alignedY - y)
|
||||
readonly property real contentRight: Math.min(width, contentLeft + root.alignedWidth)
|
||||
readonly property real contentBottom: Math.min(height, contentTop + root.alignedHeight)
|
||||
|
||||
MouseArea {
|
||||
x: 0
|
||||
y: 0
|
||||
width: outsideClickCatcher.width
|
||||
height: Math.max(0, outsideClickCatcher.contentTop)
|
||||
enabled: parent.enabled
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onClicked: root.backgroundClicked()
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
x: 0
|
||||
y: outsideClickCatcher.contentBottom
|
||||
width: outsideClickCatcher.width
|
||||
height: Math.max(0, outsideClickCatcher.height - outsideClickCatcher.contentBottom)
|
||||
enabled: parent.enabled
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onClicked: root.backgroundClicked()
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
x: 0
|
||||
y: outsideClickCatcher.contentTop
|
||||
width: Math.max(0, outsideClickCatcher.contentLeft)
|
||||
height: Math.max(0, outsideClickCatcher.contentBottom - outsideClickCatcher.contentTop)
|
||||
enabled: parent.enabled
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onClicked: root.backgroundClicked()
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
x: outsideClickCatcher.contentRight
|
||||
y: outsideClickCatcher.contentTop
|
||||
width: Math.max(0, outsideClickCatcher.width - outsideClickCatcher.contentRight)
|
||||
height: Math.max(0, outsideClickCatcher.contentBottom - outsideClickCatcher.contentTop)
|
||||
enabled: parent.enabled
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onClicked: root.backgroundClicked()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,12 +644,21 @@ Item {
|
||||
WindowBlur {
|
||||
id: popoutBlur
|
||||
targetWindow: contentWindow
|
||||
blurEnabled: root.effectiveSurfaceBlurEnabled && !root.frameOwnsConnectedChrome
|
||||
|
||||
readonly property real s: Math.min(1, contentContainer.scaleValue)
|
||||
blurX: contentContainer.x + contentContainer.width * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr)
|
||||
blurY: contentContainer.y + contentContainer.height * (1 - s) * 0.5 + Theme.snap(contentContainer.animY, root.dpr)
|
||||
blurWidth: (shouldBeVisible && contentWrapper.opacity > 0) ? contentContainer.width * s : 0
|
||||
blurHeight: (shouldBeVisible && contentWrapper.opacity > 0) ? contentContainer.height * s : 0
|
||||
blurRadius: Theme.cornerRadius
|
||||
readonly property bool trackBlurFromBarEdge: Theme.isConnectedEffect || (typeof SettingsData !== "undefined" && Theme.isDirectionalEffect && SettingsData.directionalAnimationMode !== 2)
|
||||
|
||||
// Directional popouts clip to the bar edge, so the blur needs to grow from
|
||||
// that same edge instead of translating through the bar before settling.
|
||||
readonly property real _dyClamp: (contentContainer.barTop || contentContainer.barBottom) ? Math.max(-contentContainer.height, Math.min(contentContainer.animY, contentContainer.height)) : 0
|
||||
readonly property real _dxClamp: (contentContainer.barLeft || contentContainer.barRight) ? Math.max(-contentContainer.width, Math.min(contentContainer.animX, contentContainer.width)) : 0
|
||||
|
||||
blurX: trackBlurFromBarEdge ? contentContainer.x + (contentContainer.barRight ? _dxClamp : 0) : contentContainer.x + contentContainer.width * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr) - contentContainer.horizontalConnectorExtent * s
|
||||
blurY: trackBlurFromBarEdge ? contentContainer.y + (contentContainer.barBottom ? _dyClamp : 0) : contentContainer.y + contentContainer.height * (1 - s) * 0.5 + Theme.snap(contentContainer.animY, root.dpr) - contentContainer.verticalConnectorExtent * s
|
||||
blurWidth: (shouldBeVisible && contentWrapper.opacity > 0) ? (trackBlurFromBarEdge ? Math.max(0, contentContainer.width - Math.abs(_dxClamp)) : (contentContainer.width + contentContainer.horizontalConnectorExtent * 2) * s) : 0
|
||||
blurHeight: (shouldBeVisible && contentWrapper.opacity > 0) ? (trackBlurFromBarEdge ? Math.max(0, contentContainer.height - Math.abs(_dyClamp)) : (contentContainer.height + contentContainer.verticalConnectorExtent * 2) * s) : 0
|
||||
blurRadius: Theme.isConnectedEffect ? Theme.connectedCornerRadius : Theme.connectedSurfaceRadius
|
||||
}
|
||||
|
||||
WlrLayershell.namespace: root.layerNamespace
|
||||
@@ -436,7 +688,6 @@ Item {
|
||||
}
|
||||
|
||||
readonly property bool _fullHeight: useBackgroundWindow && root.fullHeightSurface
|
||||
|
||||
anchors {
|
||||
left: true
|
||||
top: true
|
||||
@@ -462,10 +713,10 @@ Item {
|
||||
Item {
|
||||
id: contentMaskRect
|
||||
visible: false
|
||||
x: contentContainer.x
|
||||
y: contentContainer.y
|
||||
width: shouldBeVisible ? root.alignedWidth : 0
|
||||
height: shouldBeVisible ? root.alignedHeight : 0
|
||||
x: contentContainer.x - contentContainer.horizontalConnectorExtent
|
||||
y: contentContainer.y - contentContainer.verticalConnectorExtent
|
||||
width: root.alignedWidth + contentContainer.horizontalConnectorExtent * 2
|
||||
height: root.alignedHeight + contentContainer.verticalConnectorExtent * 2
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
@@ -494,12 +745,122 @@ Item {
|
||||
readonly property bool barBottom: effectiveBarPosition === SettingsData.Position.Bottom
|
||||
readonly property bool barLeft: effectiveBarPosition === SettingsData.Position.Left
|
||||
readonly property bool barRight: effectiveBarPosition === SettingsData.Position.Right
|
||||
readonly property real offsetX: barLeft ? root.animationOffset : (barRight ? -root.animationOffset : 0)
|
||||
readonly property real offsetY: barBottom ? -root.animationOffset : (barTop ? root.animationOffset : 0)
|
||||
readonly property string connectedBarSide: barTop ? "top" : (barBottom ? "bottom" : (barLeft ? "left" : "right"))
|
||||
readonly property real surfaceRadius: Theme.connectedSurfaceRadius
|
||||
readonly property color surfaceColor: Theme.popupLayerColor(Theme.surfaceContainer)
|
||||
readonly property color surfaceBorderColor: Theme.isConnectedEffect ? "transparent" : (BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium)
|
||||
readonly property real surfaceBorderWidth: Theme.isConnectedEffect ? 0 : BlurService.borderWidth
|
||||
readonly property real surfaceTopLeftRadius: Theme.isConnectedEffect && (barTop || barLeft) ? 0 : surfaceRadius
|
||||
readonly property real surfaceTopRightRadius: Theme.isConnectedEffect && (barTop || barRight) ? 0 : surfaceRadius
|
||||
readonly property real surfaceBottomLeftRadius: Theme.isConnectedEffect && (barBottom || barLeft) ? 0 : surfaceRadius
|
||||
readonly property real surfaceBottomRightRadius: Theme.isConnectedEffect && (barBottom || barRight) ? 0 : surfaceRadius
|
||||
readonly property bool directionalEffect: Theme.isDirectionalEffect
|
||||
readonly property bool depthEffect: Theme.isDepthEffect
|
||||
readonly property real directionalTravelX: Math.max(root.animationOffset, root.alignedWidth + Theme.spacingL)
|
||||
readonly property real directionalTravelY: Math.max(root.animationOffset, root.alignedHeight + Theme.spacingL)
|
||||
readonly property real depthTravel: Math.max(root.animationOffset * 0.7, 28)
|
||||
readonly property real sectionTilt: (triggerSection === "left" ? -1 : (triggerSection === "right" ? 1 : 0))
|
||||
readonly property real horizontalConnectorExtent: Theme.isConnectedEffect && (barTop || barBottom) ? Theme.connectedCornerRadius : 0
|
||||
readonly property real verticalConnectorExtent: Theme.isConnectedEffect && (barLeft || barRight) ? Theme.connectedCornerRadius : 0
|
||||
|
||||
function connectorWidth(spacing) {
|
||||
return (barTop || barBottom) ? Theme.connectedCornerRadius : (spacing + Theme.connectedCornerRadius);
|
||||
}
|
||||
|
||||
function connectorHeight(spacing) {
|
||||
return (barTop || barBottom) ? (spacing + Theme.connectedCornerRadius) : Theme.connectedCornerRadius;
|
||||
}
|
||||
|
||||
function connectorSeamX(baseX, bodyWidth, placement) {
|
||||
if (barTop || barBottom)
|
||||
return placement === "left" ? baseX : baseX + bodyWidth;
|
||||
return barLeft ? baseX : baseX + bodyWidth;
|
||||
}
|
||||
|
||||
function connectorSeamY(baseY, bodyHeight, placement) {
|
||||
if (barTop)
|
||||
return baseY;
|
||||
if (barBottom)
|
||||
return baseY + bodyHeight;
|
||||
return placement === "left" ? baseY : baseY + bodyHeight;
|
||||
}
|
||||
|
||||
function connectorX(baseX, bodyWidth, placement, spacing) {
|
||||
const seamX = connectorSeamX(baseX, bodyWidth, placement);
|
||||
const width = connectorWidth(spacing);
|
||||
if (barTop || barBottom)
|
||||
return placement === "left" ? seamX - width : seamX;
|
||||
return barLeft ? seamX : seamX - width;
|
||||
}
|
||||
|
||||
function connectorY(baseY, bodyHeight, placement, spacing) {
|
||||
const seamY = connectorSeamY(baseY, bodyHeight, placement);
|
||||
const height = connectorHeight(spacing);
|
||||
if (barTop)
|
||||
return seamY;
|
||||
if (barBottom)
|
||||
return seamY - height;
|
||||
return placement === "left" ? seamY - height : seamY;
|
||||
}
|
||||
|
||||
readonly property real offsetX: {
|
||||
if (directionalEffect) {
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2)
|
||||
return 0;
|
||||
if (barLeft)
|
||||
return -directionalTravelX;
|
||||
if (barRight)
|
||||
return directionalTravelX;
|
||||
if (barTop || barBottom)
|
||||
return 0;
|
||||
return sectionTilt * directionalTravelX * 0.2;
|
||||
}
|
||||
if (depthEffect) {
|
||||
if (barLeft)
|
||||
return -depthTravel;
|
||||
if (barRight)
|
||||
return depthTravel;
|
||||
if (barTop || barBottom)
|
||||
return 0;
|
||||
return sectionTilt * depthTravel * 0.2;
|
||||
}
|
||||
return barLeft ? root.animationOffset : (barRight ? -root.animationOffset : 0);
|
||||
}
|
||||
readonly property real offsetY: {
|
||||
if (directionalEffect) {
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2)
|
||||
return 0;
|
||||
if (barBottom)
|
||||
return directionalTravelY;
|
||||
if (barTop)
|
||||
return -directionalTravelY;
|
||||
if (barLeft || barRight)
|
||||
return 0;
|
||||
return directionalTravelY;
|
||||
}
|
||||
if (depthEffect) {
|
||||
if (barBottom)
|
||||
return depthTravel;
|
||||
if (barTop)
|
||||
return -depthTravel;
|
||||
if (barLeft || barRight)
|
||||
return 0;
|
||||
return depthTravel;
|
||||
}
|
||||
return barBottom ? -root.animationOffset : (barTop ? root.animationOffset : 0);
|
||||
}
|
||||
|
||||
property real animX: 0
|
||||
property real animY: 0
|
||||
property real scaleValue: root.animationScaleCollapsed
|
||||
|
||||
readonly property real computedScaleCollapsed: (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) ? 0.0 : root.animationScaleCollapsed
|
||||
property real scaleValue: computedScaleCollapsed
|
||||
|
||||
Component.onCompleted: {
|
||||
animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr);
|
||||
animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr);
|
||||
scaleValue = root.shouldBeVisible ? 1.0 : computedScaleCollapsed;
|
||||
}
|
||||
|
||||
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
|
||||
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
|
||||
@@ -509,96 +870,216 @@ Item {
|
||||
function onShouldBeVisibleChanged() {
|
||||
contentContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetX, root.dpr);
|
||||
contentContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetY, root.dpr);
|
||||
contentContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed;
|
||||
contentContainer.scaleValue = root.shouldBeVisible ? 1.0 : contentContainer.computedScaleCollapsed;
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on animX {
|
||||
enabled: root.animationsEnabled
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on animY {
|
||||
enabled: root.animationsEnabled
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scaleValue {
|
||||
enabled: root.animationsEnabled
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
ElevationShadow {
|
||||
id: shadowSource
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
opacity: contentWrapper.opacity
|
||||
scale: contentWrapper.scale
|
||||
x: contentWrapper.x
|
||||
y: contentWrapper.y
|
||||
level: root.shadowLevel
|
||||
direction: root.effectiveShadowDirection
|
||||
fallbackOffset: root.shadowFallbackOffset
|
||||
targetRadius: Theme.cornerRadius
|
||||
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !(root.suspendShadowWhileResizing && root._resizeActive)
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentWrapper
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
opacity: shouldBeVisible ? 1 : 0
|
||||
visible: opacity > 0
|
||||
scale: contentContainer.scaleValue
|
||||
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
||||
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
||||
id: directionalClipMask
|
||||
|
||||
layer.enabled: contentWrapper.opacity < 1
|
||||
layer.smooth: false
|
||||
layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0)
|
||||
readonly property bool shouldClip: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0
|
||||
readonly property real clipOversize: 1000
|
||||
readonly property real connectedClipAllowance: Theme.isConnectedEffect ? Math.ceil(root.shadowRenderPadding + BlurService.borderWidth + 2) : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: animationDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
clip: shouldClip
|
||||
|
||||
// Bound the clipping strictly to the bar side, allowing massive overflow on the other 3 sides for shadows
|
||||
x: shouldClip ? (contentContainer.barLeft ? -connectedClipAllowance : -clipOversize) : 0
|
||||
y: shouldClip ? (contentContainer.barTop ? -connectedClipAllowance : -clipOversize) : 0
|
||||
|
||||
width: {
|
||||
if (!shouldClip)
|
||||
return parent.width;
|
||||
if (contentContainer.barLeft)
|
||||
return parent.width + connectedClipAllowance + clipOversize;
|
||||
if (contentContainer.barRight)
|
||||
return parent.width + clipOversize + connectedClipAllowance;
|
||||
return parent.width + clipOversize * 2;
|
||||
}
|
||||
height: {
|
||||
if (!shouldClip)
|
||||
return parent.height;
|
||||
if (contentContainer.barTop)
|
||||
return parent.height + connectedClipAllowance + clipOversize;
|
||||
if (contentContainer.barBottom)
|
||||
return parent.height + clipOversize + connectedClipAllowance;
|
||||
return parent.height + clipOversize * 2;
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.fill: parent
|
||||
active: root._primeContent || shouldBeVisible || contentWindow.visible
|
||||
asynchronous: false
|
||||
}
|
||||
}
|
||||
Item {
|
||||
id: aligner
|
||||
readonly property real baseWidth: contentContainer.width
|
||||
readonly property real baseHeight: contentContainer.height
|
||||
readonly property bool isRollOut: typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
x: contentWrapper.x
|
||||
y: contentWrapper.y
|
||||
opacity: contentWrapper.opacity
|
||||
scale: contentWrapper.scale
|
||||
visible: contentWrapper.visible
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
border.color: BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium
|
||||
border.width: BlurService.borderWidth
|
||||
z: 100
|
||||
}
|
||||
}
|
||||
x: (directionalClipMask.x !== 0 ? -directionalClipMask.x : 0) + (isRollOut && contentContainer.barRight ? baseWidth * (1 - contentContainer.scaleValue) : 0)
|
||||
y: (directionalClipMask.y !== 0 ? -directionalClipMask.y : 0) + (isRollOut && contentContainer.barBottom ? baseHeight * (1 - contentContainer.scaleValue) : 0)
|
||||
width: isRollOut && (contentContainer.barLeft || contentContainer.barRight) ? Math.max(0, baseWidth * contentContainer.scaleValue) : baseWidth
|
||||
height: isRollOut && (contentContainer.barTop || contentContainer.barBottom) ? Math.max(0, baseHeight * contentContainer.scaleValue) : baseHeight
|
||||
|
||||
clip: isRollOut
|
||||
|
||||
Item {
|
||||
id: unrollCounteract
|
||||
x: aligner.isRollOut && contentContainer.barRight ? -(aligner.baseWidth * (1 - contentContainer.scaleValue)) : 0
|
||||
y: aligner.isRollOut && contentContainer.barBottom ? -(aligner.baseHeight * (1 - contentContainer.scaleValue)) : 0
|
||||
width: aligner.baseWidth
|
||||
height: aligner.baseHeight
|
||||
|
||||
ElevationShadow {
|
||||
id: shadowSource
|
||||
readonly property real connectorExtent: Theme.isConnectedEffect ? Theme.connectedCornerRadius : 0
|
||||
readonly property real extraLeft: Theme.isConnectedEffect && (contentContainer.barTop || contentContainer.barBottom) ? connectorExtent : 0
|
||||
readonly property real extraRight: Theme.isConnectedEffect && (contentContainer.barTop || contentContainer.barBottom) ? connectorExtent : 0
|
||||
readonly property real extraTop: Theme.isConnectedEffect && (contentContainer.barLeft || contentContainer.barRight) ? connectorExtent : 0
|
||||
readonly property real extraBottom: Theme.isConnectedEffect && (contentContainer.barLeft || contentContainer.barRight) ? connectorExtent : 0
|
||||
readonly property real bodyX: extraLeft
|
||||
readonly property real bodyY: extraTop
|
||||
readonly property real bodyWidth: parent.width
|
||||
readonly property real bodyHeight: parent.height
|
||||
|
||||
width: parent.width + extraLeft + extraRight
|
||||
height: parent.height + extraTop + extraBottom
|
||||
opacity: contentWrapper.opacity
|
||||
scale: contentWrapper.scale
|
||||
x: contentWrapper.x - extraLeft
|
||||
y: contentWrapper.y - extraTop
|
||||
level: root.shadowLevel
|
||||
direction: root.effectiveShadowDirection
|
||||
fallbackOffset: root.shadowFallbackOffset
|
||||
targetRadius: contentContainer.surfaceRadius
|
||||
topLeftRadius: contentContainer.surfaceTopLeftRadius
|
||||
topRightRadius: contentContainer.surfaceTopRightRadius
|
||||
bottomLeftRadius: contentContainer.surfaceBottomLeftRadius
|
||||
bottomRightRadius: contentContainer.surfaceBottomRightRadius
|
||||
targetColor: contentContainer.surfaceColor
|
||||
borderColor: contentContainer.surfaceBorderColor
|
||||
borderWidth: contentContainer.surfaceBorderWidth
|
||||
useCustomSource: Theme.isConnectedEffect
|
||||
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !(root.suspendShadowWhileResizing && root._resizeActive)
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: Theme.isConnectedEffect && !root.frameOwnsConnectedChrome
|
||||
clip: false
|
||||
|
||||
Rectangle {
|
||||
x: shadowSource.bodyX
|
||||
y: shadowSource.bodyY
|
||||
width: shadowSource.bodyWidth
|
||||
height: shadowSource.bodyHeight
|
||||
topLeftRadius: contentContainer.surfaceTopLeftRadius
|
||||
topRightRadius: contentContainer.surfaceTopRightRadius
|
||||
bottomLeftRadius: contentContainer.surfaceBottomLeftRadius
|
||||
bottomRightRadius: contentContainer.surfaceBottomRightRadius
|
||||
color: contentContainer.surfaceColor
|
||||
}
|
||||
|
||||
ConnectedCorner {
|
||||
visible: Theme.isConnectedEffect
|
||||
barSide: contentContainer.connectedBarSide
|
||||
placement: "left"
|
||||
spacing: 0
|
||||
connectorRadius: Theme.connectedCornerRadius
|
||||
color: contentContainer.surfaceColor
|
||||
dpr: root.dpr
|
||||
x: Theme.snap(contentContainer.connectorX(shadowSource.bodyX, shadowSource.bodyWidth, placement, spacing), root.dpr)
|
||||
y: Theme.snap(contentContainer.connectorY(shadowSource.bodyY, shadowSource.bodyHeight, placement, spacing), root.dpr)
|
||||
}
|
||||
|
||||
ConnectedCorner {
|
||||
visible: Theme.isConnectedEffect
|
||||
barSide: contentContainer.connectedBarSide
|
||||
placement: "right"
|
||||
spacing: 0
|
||||
connectorRadius: Theme.connectedCornerRadius
|
||||
color: contentContainer.surfaceColor
|
||||
dpr: root.dpr
|
||||
x: Theme.snap(contentContainer.connectorX(shadowSource.bodyX, shadowSource.bodyWidth, placement, spacing), root.dpr)
|
||||
y: Theme.snap(contentContainer.connectorY(shadowSource.bodyY, shadowSource.bodyHeight, placement, spacing), root.dpr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentWrapper
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
opacity: Theme.isDirectionalEffect ? 1 : (shouldBeVisible ? 1 : 0)
|
||||
visible: opacity > 0
|
||||
|
||||
scale: aligner.isRollOut ? 1.0 : contentContainer.scaleValue
|
||||
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - scale) * 0.5, root.dpr)
|
||||
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - scale) * 0.5, root.dpr)
|
||||
|
||||
layer.enabled: contentWrapper.opacity < 1
|
||||
layer.smooth: false
|
||||
layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0)
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: !Theme.isDirectionalEffect
|
||||
NumberAnimation {
|
||||
duration: Math.round(Theme.variantDuration(animationDuration, shouldBeVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
clip: false
|
||||
visible: !Theme.isConnectedEffect
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
topLeftRadius: contentContainer.surfaceTopLeftRadius
|
||||
topRightRadius: contentContainer.surfaceTopRightRadius
|
||||
bottomLeftRadius: contentContainer.surfaceBottomLeftRadius
|
||||
bottomRightRadius: contentContainer.surfaceBottomRightRadius
|
||||
color: contentContainer.surfaceColor
|
||||
border.color: contentContainer.surfaceBorderColor
|
||||
border.width: contentContainer.surfaceBorderWidth
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.fill: parent
|
||||
active: root._primeContent || shouldBeVisible || contentWindow.visible
|
||||
asynchronous: false
|
||||
}
|
||||
} // closes contentWrapper
|
||||
} // closes unrollCounteract
|
||||
} // closes aligner
|
||||
} // closes directionalClipMask
|
||||
} // closes contentContainer
|
||||
|
||||
Item {
|
||||
id: focusHelper
|
||||
|
||||
@@ -8,13 +8,122 @@ Item {
|
||||
id: root
|
||||
|
||||
property MprisPlayer activePlayer
|
||||
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 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: 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
|
||||
|
||||
@@ -29,58 +138,35 @@ 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: waveSeekDebounceTimer
|
||||
interval: 150
|
||||
id: waveHoldIndicatorTimer
|
||||
interval: root.holdIndicatorDelay
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
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
|
||||
}
|
||||
if (parent.pressed && root.isSeeking)
|
||||
root.isDraggingSeek = true;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,6 +179,7 @@ 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 {
|
||||
@@ -110,7 +197,22 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: parent.fillColor
|
||||
radius: height / 2
|
||||
Behavior on width { NumberAnimation { duration: 80 } }
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: 80
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: root.isDraggingSeek
|
||||
width: 2
|
||||
height: Math.max(parent.lineWidth + 4, 10)
|
||||
radius: width / 2
|
||||
color: parent.actualProgressColor
|
||||
x: Math.max(0, Math.min(parent.width, parent.width * root.playerValue)) - width / 2
|
||||
y: parent.midY - height / 2
|
||||
z: 2
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@@ -122,59 +224,37 @@ 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: flatSeekDebounceTimer
|
||||
interval: 150
|
||||
id: flatHoldIndicatorTimer
|
||||
interval: root.holdIndicatorDelay
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
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
|
||||
}
|
||||
if (parent.pressed && root.isSeeking)
|
||||
root.isDraggingSeek = true;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ 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
|
||||
@@ -15,6 +17,7 @@ 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) {
|
||||
@@ -22,7 +25,12 @@ 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 {
|
||||
@@ -65,7 +73,9 @@ Item {
|
||||
|
||||
readonly property real startX: snap(root.lineWidth / 2)
|
||||
readonly property real aaBias: (0.25 / root.dpr)
|
||||
readonly property real endX: Math.max(startX, Math.min(root.playX - startX - aaBias, width))
|
||||
readonly property real endX: root.previewAhead ? Math.max(startX, Math.min(root.actualX - aaBias, width)) : Math.max(startX, Math.min(root.playX - startX - aaBias, width))
|
||||
readonly property real gapStartX: root.previewAhead ? Math.max(startX, Math.min(root.actualX + aaBias, width)) : Math.max(startX, Math.min(root.playX + playhead.width / 2, width))
|
||||
readonly property real gapEndX: root.previewAhead ? Math.max(gapStartX, Math.min(root.playX - playhead.width / 2 - aaBias, width)) : Math.max(gapStartX, Math.min(root.actualX - aaBias, width))
|
||||
|
||||
Rectangle {
|
||||
id: mask
|
||||
@@ -100,6 +110,37 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: actualMask
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
x: waveClip.gapStartX
|
||||
width: Math.max(0, waveClip.gapEndX - waveClip.gapStartX)
|
||||
color: "transparent"
|
||||
clip: true
|
||||
visible: (root.previewBehind || root.previewAhead) && width > 0
|
||||
|
||||
Shape {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: root.width + 4 * root.wavelength
|
||||
antialiasing: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
x: waveOffsetX
|
||||
|
||||
ShapePath {
|
||||
strokeColor: root.actualProgressColor
|
||||
strokeWidth: snap(root.lineWidth)
|
||||
capStyle: ShapePath.RoundCap
|
||||
joinStyle: ShapePath.RoundJoin
|
||||
fillColor: "transparent"
|
||||
PathSvg {
|
||||
path: waveSvg.path
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: startCap
|
||||
width: snap(root.lineWidth)
|
||||
@@ -107,7 +148,7 @@ Item {
|
||||
radius: width / 2
|
||||
color: root.fillColor
|
||||
x: waveClip.startX - width / 2
|
||||
y: root.midY - height / 2 + root.currentAmp * Math.sin((waveClip.startX / root.wavelength) * 2 * Math.PI + root.phase)
|
||||
y: waveY(waveClip.startX) - height / 2
|
||||
visible: waveClip.endX > waveClip.startX
|
||||
z: 2
|
||||
}
|
||||
@@ -119,10 +160,34 @@ Item {
|
||||
radius: width / 2
|
||||
color: root.fillColor
|
||||
x: waveClip.endX - width / 2
|
||||
y: root.midY - height / 2 + root.currentAmp * Math.sin((waveClip.endX / root.wavelength) * 2 * Math.PI + root.phase)
|
||||
y: waveY(waveClip.endX) - height / 2
|
||||
visible: waveClip.endX > waveClip.startX
|
||||
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 {
|
||||
@@ -141,6 +206,10 @@ 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 {
|
||||
@@ -148,8 +217,9 @@ Item {
|
||||
onTriggered: {
|
||||
if (root.isPlaying)
|
||||
root.phase += 0.03 * frameTime * 60;
|
||||
startCap.y = root.midY - startCap.height / 2 + root.currentAmp * Math.sin((waveClip.startX / root.wavelength) * 2 * Math.PI + root.phase);
|
||||
endCap.y = root.midY - endCap.height / 2 + root.currentAmp * Math.sin((waveClip.endX / root.wavelength) * 2 * Math.PI + root.phase);
|
||||
startCap.y = waveY(waveClip.startX) - startCap.height / 2;
|
||||
endCap.y = waveY(waveClip.endX) - endCap.height / 2;
|
||||
actualEndCap.y = waveY(waveClip.gapEndX) - actualEndCap.height / 2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
@@ -8,6 +9,7 @@ Item {
|
||||
|
||||
required property var targetWindow
|
||||
property var blurItem: null
|
||||
property bool blurEnabled: Theme.connectedSurfaceBlurEnabled
|
||||
property real blurX: 0
|
||||
property real blurY: 0
|
||||
property real blurWidth: 0
|
||||
@@ -17,7 +19,7 @@ Item {
|
||||
property var _region: null
|
||||
|
||||
function _apply() {
|
||||
if (!BlurService.enabled || !targetWindow) {
|
||||
if (!blurEnabled || !BlurService.enabled || !targetWindow) {
|
||||
_cleanup();
|
||||
return;
|
||||
}
|
||||
@@ -43,6 +45,8 @@ Item {
|
||||
_region = null;
|
||||
}
|
||||
|
||||
onBlurEnabledChanged: _apply()
|
||||
|
||||
Connections {
|
||||
target: BlurService
|
||||
function onEnabledChanged() {
|
||||
|
||||
BIN
quickshell/assets/sounds/freedesktop/desktop-login.oga
Normal file
BIN
quickshell/assets/sounds/freedesktop/desktop-login.oga
Normal file
Binary file not shown.
BIN
quickshell/assets/sounds/freedesktop/desktop-login.wav
Normal file
BIN
quickshell/assets/sounds/freedesktop/desktop-login.wav
Normal file
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 []
|
||||
context_str = " | ".join(contexts) if contexts else term
|
||||
comment = " | ".join(contexts) if contexts else ""
|
||||
|
||||
entry = {
|
||||
"term": term,
|
||||
"context": context_str,
|
||||
"context": term,
|
||||
"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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user