1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-27 21:45:19 -04:00

Compare commits

..

29 Commits

Author SHA1 Message Date
purian23 aed731efb0 fix(clipboard): restore Save button targets in editor 2026-05-25 23:19:42 -04:00
purian23 cf0632c077 feat(Clipboard): Revive ClipboardEditor PR
- Original PR #1916 by @nabaco
2026-05-24 23:28:21 -04:00
Nachum Barcohen e92da4a15f Show full clipboard text in editor 2026-05-24 22:34:24 -04:00
Nachum Barcohen 8abdff3220 Add clipboard editor shortcuts and hints 2026-05-24 22:34:24 -04:00
Nachum Barcohen 584d57a8de Add split save menu for clipboard editor 2026-05-24 22:34:05 -04:00
Nachum Barcohen afb5e59c29 feat(clipboard): Add editing capability to clipboard entries 2026-05-24 22:34:05 -04:00
purian23 d9525908f1 refactor(Notifications): further support for duplicate notification logic
- New setting to stack or suppress identical alerts (on by default)
Closes #2334
2026-05-24 22:22:34 -04:00
Lucas 6093c37b41 settings: add descriptions for DankBar menu (#2490) 2026-05-24 18:56:42 -04:00
purian23 bb05cbb6c5 feat(sessions): implement local user session switching functionality
- Core user is logged in tty1 while user two is in tty3, you can now seamlessly switch bewteen them
New IPC options:
- `dms ipc call sessions list`
- `dms switch-user [target]`
- New Powermenu switch users option
2026-05-24 18:33:38 -04:00
purian23 4d4af8f549 feat(Users): add user management UI in DMS Settings 2026-05-24 18:15:41 -04:00
Feng Yu 0b55fbcb15 fix(DankBar): Resolve tray freeze and wallpaper loss after DPMS resume (#2457)
Fixes #2354

Root cause (tray freeze): In clickThrough mode, the PanelWindow mask uses
sectionRect() with mapToItem() to compute input regions. After DPMS resume,
the PanelWindow is recreated with width=0, and mapToItem() returns wrong
positions. The right section's implicitWidth doesn't change after creation
(fixed-size tray icons), so the mask binding is never re-evaluated when the
compositor sets the actual screen width. Adding barWindow.width as a binding
dependency ensures the mask recalculates on resize.

Root cause (wallpaper loss): Wallpaper PanelWindows are recreated by Variants
during screen reconnection before the compositor finishes output initialization.
The wallpaper Image renders at 0x0 dimensions, resulting in a black screen.

Changes:
- DankBarWindow: add barWindow.width dependency to clickThrough mask bindings
- DMSShell: add surface recovery mechanism (screen reconnect + session resume)
  with progressive 2-pass timer (800ms + 2800ms) that recreates bar, Frame,
  wallpaper, and dock surfaces after the compositor is ready
- WlrOutputService: re-request output state on session resume
2026-05-22 09:05:41 -04:00
Domen Kožar 7476a220b5 feat: Blink WiFi/Bluetooth icons while connecting (#2448)
Pulses the WiFi and Bluetooth status icons while a connection is in
progress (lock screen, DankBar control center button, control center
compound pill). The pulse is implemented as a reusable Widgets/DankBlink
component, and the wifi-connecting condition is centralized as
NetworkService.isWifiConnecting.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:03:25 -04:00
Huỳnh Thiện Lộc aaff1ab61e feat: implement interactive microphone volume OSD and IPC controls (#2406)
* feat: implement interactive microphone volume OSD and persistence

Addresses #2388

* refactor: reduce scope to interactive microphone OSD and IPC controls only
2026-05-22 09:00:12 -04:00
Cloud 39622eb62a fix(lock): avoid U2F PAM polling in OR mode (#2459) 2026-05-22 08:59:11 -04:00
Lucas eea039f575 feat(Launcher/Spotlight): improve context keyboard navigation and mode persistence (#2467)
* feat(Spotlight): fix submenu keyboard navigation

* feat(Launcher/Spotlight): disable persisting last mode when changed by triggers

* feat(Launcher/Spotlight): add option to disable last mode being persisted

* fix(Launcher/Spotlight): fix context menu keys navigation

* fix(NiriOverviewOverlay): fix context menu keys navigation and position
2026-05-22 08:53:45 -04:00
purian23 ef5de19f6b distros(dms-greeter): Add sysusers.d immutable distro support
- Closes #1975
2026-05-21 23:21:44 -04:00
bbedward f0c31bd7b3 launcher: add /d /f file search prefixes. Fix prefix not always
triggering
2026-05-21 21:10:30 -04:00
Domen Kožar 7ddd0ca90d fix(Network): Bucket WiFi signal for stable list order (#2449)
Sort networks by signal bucket of 25 with SSID as tiebreaker so minor
RSSI fluctuations between scans no longer reshuffle the list while the
user is selecting a network.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 13:04:31 -04:00
bbedward b84e5abc4a core: remove niri parse test 2026-05-21 10:33:12 -04:00
purian23 fb9ec8e721 feat: (Launcher/Spotlight): Updated w/New Settings & QOL features
- New Spotlight toggle to show/hide chips, off by default
- Updated blur effects on all launcher inputs and footers
- Fixed previous queries resurfacing
- Upated Spotlight keyboard navigation
- Added functionality to show and shortcut to keybinds from the Launcher tab
2026-05-21 01:05:56 -04:00
purian23 078c9b4890 refactor(niri): Normalize key bindings to be case insensitive within DMS 2026-05-21 00:38:31 -04:00
purian23 37c98220a9 refactor(Spotlight): Use Spotlight alongside OG Launcher
- Update to add DMS Action keys in Keyboard Shortcuts
- Defaulted in niri/hyprland includes file as `Alt+Space`
- New (IPC): `dms ipc call spotlight-bar toggle`
- Slight UI update to follow user radius
2026-05-20 17:21:03 -04:00
Klesh Wong fc07611b3b fix(osd): ensure OSD appears on all monitors after resume (#2453)
Some monitors, especially cheaper models, are slow to power on after sleep and may not be present in Quickshell.screens
when onScreensChanged is triggered. This change adds a 3-second delay before updating currentOSDsByScreen to ensure all
screens are detected, mitigating the issue of OSDs not appearing on certain monitors after resume.

Co-authored-by: Klesh Wong <kleshwong@gmail.com>
2026-05-20 11:58:13 -04:00
Graeme Foster a923308c09 networkd: classify links by Type instead of name prefix (#2447)
* networkd: classify links by Type instead of name prefix

The systemd-networkd backend decided wifi-vs-ethernet by checking
whether the interface name started with "wlan" or "wlp". Anything
else (that was not on a small virtual-prefix denylist) was treated
as wired ethernet. That misclassified two common cases:

* Nebula tunnels (kernel name like "nebula.homelab", Type=none,
  Kind=tun) showed up as a wired ethernet device — DMS rendered an
  "Ethernet connected" indicator whenever the overlay was up, even
  with no physical NIC plugged in.
* Renamed wifi interfaces (e.g. systemd link files that rename
  wlan0 to a friendlier name like "wifi") were also miscategorised
  as ethernet, because they no longer matched wlan*/wlp*.

networkd already publishes the real link kind in the JSON returned
by the per-link Describe method ("ether", "wlan", "loopback",
"none"). Fetch it during enumerateLinks, cache it on linkInfo, and
classify against that. The old prefix logic is kept as a fallback
for the case where Describe ever fails to populate Type.

The package-level looksVirtual() helper replaces the unexported
isVirtualInterface method so the new classification helpers and
their tests can use it without needing a live backend.

Tests cover both the Type-based and fallback paths, including the
Nebula-shaped Type=none/tun case that motivated this change.

* networkd: cache linkType across signal ticks and unit-test Describe fallback

enumerateLinks runs on every PropertiesChanged signal under
/org/freedesktop/network1, which fires on carrier flap, DHCP renew, and
each address change. The previous version rebuilt every linkInfo from
scratch on each tick, including a synchronous Describe D-Bus round-trip
per link, despite the link Type being fixed at netlink creation. Preserve
existing entries when the D-Bus path matches, refreshing only ifindex,
and only call fetchLinkType on a genuinely new entry. A link torn down
and re-created at a different path still triggers a refetch.

Extract parseDescribeType from fetchLinkType so the JSON failure path —
malformed payload, missing Type field, wrong type for Type — can be
exercised without a live D-Bus connection. The classifier's fallback to
name-prefix heuristics already had coverage; this locks in that the seam
between Describe and the classifier surfaces an empty string on every
failure mode rather than misclassifying a link.
2026-05-20 11:43:50 -04:00
Lichie 0990b43a43 feat(FocusedWindow): Improve content width calculation and add size options (#2444)
* use RowLayout in focusedapp widget for better width calculation

* Add context menu with additional size options for focused app widget
2026-05-20 11:43:14 -04:00
purian23 548c2305fb refactor(Settings): Rename fullscreen properties to overlay layer for consistency
- Updated property names from `showOverFullscreen` to `useOverlayLayer` across various components.
- Fixed Darken Modal Overlay in Standalone mode
2026-05-19 22:03:33 -04:00
purian23 4634763840 refactor(fullscreen): Refine fullscreen layering and frame overlay behavior
- Replaced fullscreen hide/reveal toggles with Show Over Fullscreen layer toggles
- Added Launcher opt to Show Over Fullscreen setting
- Kept fullscreen stacking compositor-owned via top/overlay layer choices
- Fixed Hyrland Special Workspaces
- Updated DMS Advanced Configuration docs
2026-05-19 18:42:45 -04:00
bbedward cdc1102092 popout: fix opening popouts across monitors
cc/brightness: fix delegate bindings and pinning
2026-05-19 11:23:03 -04:00
purian23 4845299cc2 fix(Spotlight): Update the new clipboard/settings merge w/cache & debouced refresh 2026-05-19 01:39:16 -04:00
112 changed files with 5879 additions and 1156 deletions
+1
View File
@@ -541,5 +541,6 @@ func getCommonCommands() []*cobra.Command {
blurCmd, blurCmd,
trashCmd, trashCmd,
systemCmd, systemCmd,
switchUserCmd,
} }
} }
+187
View File
@@ -0,0 +1,187 @@
package main
import (
"bufio"
"fmt"
"os"
"os/exec"
"sort"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/spf13/cobra"
)
var switchUserCmd = &cobra.Command{
Use: "switch-user [target]",
Short: "Switch to another active session on this seat",
Long: `Switch the active VT to another running session.
With no target, prints the list of switchable sessions. Pass a username or a
numeric session ID to switch directly. Requires the target to already be a
running session on the same seat (use the greeter for a fresh login).`,
Args: cobra.MaximumNArgs(1),
Run: runSwitchUser,
}
type sessionInfo struct {
ID string
Name string
Seat string
TTY string
Type string
Class string
Active bool
State string
Current bool
}
func runSwitchUser(cmd *cobra.Command, args []string) {
currentID := os.Getenv("XDG_SESSION_ID")
sessions, err := listSessions(currentID)
if err != nil {
log.Fatalf("%v", err)
}
switchable := make([]sessionInfo, 0, len(sessions))
for _, s := range sessions {
if s.Class != "user" || s.State == "closing" || s.Current {
continue
}
switchable = append(switchable, s)
}
if len(args) == 0 {
if len(switchable) == 0 {
fmt.Println("No other active sessions on this seat.")
return
}
printSessions(switchable)
return
}
target := args[0]
picked, err := pickSession(switchable, target)
if err != nil {
fmt.Fprintln(os.Stderr, err)
if len(switchable) == 0 {
fmt.Fprintln(os.Stderr, "No other active sessions on this seat. Only already-running sessions can be switched to.")
} else {
fmt.Fprintln(os.Stderr, "\nSwitchable sessions:")
printSessions(switchable)
}
os.Exit(1)
}
if err := activateSession(picked.ID); err != nil {
log.Fatalf("loginctl activate %s: %v", picked.ID, err)
}
}
func listSessions(currentID string) ([]sessionInfo, error) {
listOut, err := exec.Command("loginctl", "list-sessions", "--no-legend").Output()
if err != nil {
return nil, fmt.Errorf("loginctl list-sessions: %w", err)
}
var ids []string
scanner := bufio.NewScanner(strings.NewReader(string(listOut)))
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) == 0 {
continue
}
ids = append(ids, fields[0])
}
out := make([]sessionInfo, 0, len(ids))
for _, id := range ids {
s, err := showSession(id)
if err != nil {
continue
}
s.Current = currentID != "" && s.ID == currentID
out = append(out, s)
}
sort.SliceStable(out, func(i, j int) bool {
if out[i].Name != out[j].Name {
return out[i].Name < out[j].Name
}
return out[i].ID < out[j].ID
})
return out, nil
}
func showSession(id string) (sessionInfo, error) {
out, err := exec.Command("loginctl", "show-session", id,
"-p", "Id", "-p", "Name", "-p", "Seat", "-p", "TTY",
"-p", "Type", "-p", "Class", "-p", "Active", "-p", "State").Output()
if err != nil {
return sessionInfo{}, err
}
fields := map[string]string{}
for _, line := range strings.Split(string(out), "\n") {
idx := strings.IndexByte(line, '=')
if idx <= 0 {
continue
}
fields[line[:idx]] = line[idx+1:]
}
if fields["Id"] == "" {
return sessionInfo{}, fmt.Errorf("session %s: no Id", id)
}
return sessionInfo{
ID: fields["Id"],
Name: fields["Name"],
Seat: fields["Seat"],
TTY: fields["TTY"],
Type: fields["Type"],
Class: fields["Class"],
Active: fields["Active"] == "yes",
State: fields["State"],
}, nil
}
func pickSession(sessions []sessionInfo, target string) (sessionInfo, error) {
for _, s := range sessions {
if s.ID == target {
return s, nil
}
}
matches := make([]sessionInfo, 0, 2)
for _, s := range sessions {
if s.Name == target {
matches = append(matches, s)
}
}
if len(matches) == 1 {
return matches[0], nil
}
if len(matches) > 1 {
ids := make([]string, len(matches))
for i, m := range matches {
ids[i] = m.ID
}
return sessionInfo{}, fmt.Errorf("%s has multiple active sessions (%s); pass a session ID instead", target, strings.Join(ids, ", "))
}
return sessionInfo{}, fmt.Errorf("no switchable session matches %q", target)
}
func activateSession(id string) error {
return exec.Command("loginctl", "activate", id).Run()
}
func printSessions(sessions []sessionInfo) {
fmt.Printf("%-6s %-12s %-8s %-8s %-8s\n", "ID", "USER", "TYPE", "SEAT", "TTY")
for _, s := range sessions {
tty := s.TTY
if tty == "" {
tty = "-"
}
seat := s.Seat
if seat == "" {
seat = "-"
}
fmt.Printf("%-6s %-12s %-8s %-8s %-8s\n", s.ID, s.Name, s.Type, seat, tty)
}
}
@@ -3,6 +3,7 @@
-- === Application Launchers === -- === Application Launchers ===
hl.bind("SUPER + T", hl.dsp.exec_cmd("{{TERMINAL_COMMAND}}")) hl.bind("SUPER + T", hl.dsp.exec_cmd("{{TERMINAL_COMMAND}}"))
hl.bind("SUPER + space", hl.dsp.exec_cmd("dms ipc call spotlight toggle")) hl.bind("SUPER + space", hl.dsp.exec_cmd("dms ipc call spotlight toggle"))
hl.bind("ALT + space", hl.dsp.exec_cmd("dms ipc call spotlight-bar toggle"))
hl.bind("SUPER + V", hl.dsp.exec_cmd("dms ipc call clipboard toggle")) hl.bind("SUPER + V", hl.dsp.exec_cmd("dms ipc call clipboard toggle"))
hl.bind("SUPER + M", hl.dsp.exec_cmd("dms ipc call processlist focusOrToggle")) hl.bind("SUPER + M", hl.dsp.exec_cmd("dms ipc call processlist focusOrToggle"))
hl.bind("SUPER + comma", hl.dsp.exec_cmd("dms ipc call settings focusOrToggle")) hl.bind("SUPER + comma", hl.dsp.exec_cmd("dms ipc call settings focusOrToggle"))
@@ -9,6 +9,9 @@ binds {
Mod+Space hotkey-overlay-title="Application Launcher" { Mod+Space hotkey-overlay-title="Application Launcher" {
spawn "dms" "ipc" "call" "spotlight" "toggle"; spawn "dms" "ipc" "call" "spotlight" "toggle";
} }
Alt+Space hotkey-overlay-title="Spotlight Bar" {
spawn "dms" "ipc" "call" "spotlight-bar" "toggle";
}
Mod+V hotkey-overlay-title="Clipboard Manager" { Mod+V hotkey-overlay-title="Clipboard Manager" {
spawn "dms" "ipc" "call" "clipboard" "toggle"; spawn "dms" "ipc" "call" "clipboard" "toggle";
} }
+4 -4
View File
@@ -166,7 +166,7 @@ func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string, co
} }
if source == "dms-default" && conflicts != nil { if source == "dms-default" && conflicts != nil {
if conflictKb, ok := conflicts[keyStr]; ok { if conflictKb, ok := conflicts[normalizeNiriBindKey(keyStr)]; ok {
bind.Conflict = &keybinds.Keybind{ bind.Conflict = &keybinds.Keybind{
Key: keyStr, Key: keyStr,
Description: conflictKb.Description, Description: conflictKb.Description,
@@ -249,7 +249,7 @@ func (n *NiriProvider) SetBind(key, action, description string, options map[stri
existingBinds = make(map[string]*overrideBind) existingBinds = make(map[string]*overrideBind)
} }
existingBinds[key] = &overrideBind{ existingBinds[normalizeNiriBindKey(key)] = &overrideBind{
Key: key, Key: key,
Action: action, Action: action,
Description: description, Description: description,
@@ -265,7 +265,7 @@ func (n *NiriProvider) RemoveBind(key string) error {
return nil return nil
} }
delete(existingBinds, key) delete(existingBinds, normalizeNiriBindKey(key))
return n.writeOverrideBinds(existingBinds) return n.writeOverrideBinds(existingBinds)
} }
@@ -316,7 +316,7 @@ func (n *NiriProvider) loadOverrideBinds() (map[string]*overrideBind, error) {
action = n.formatRawAction(kb.Action, kb.Args) action = n.formatRawAction(kb.Action, kb.Args)
} }
binds[keyStr] = &overrideBind{ binds[normalizeNiriBindKey(keyStr)] = &overrideBind{
Key: keyStr, Key: keyStr,
Action: action, Action: action,
Description: kb.Description, Description: kb.Description,
@@ -162,6 +162,14 @@ func NewNiriParser(configDir string) *NiriParser {
} }
} }
func normalizeNiriBindKey(key string) string {
parts := strings.Split(key, "+")
for i := range parts {
parts[i] = strings.ToLower(strings.TrimSpace(parts[i]))
}
return strings.Join(parts, "+")
}
func (p *NiriParser) Parse() (*NiriSection, error) { func (p *NiriParser) Parse() (*NiriSection, error) {
dmsBindsPath := filepath.Join(p.configDir, "dms", "binds.kdl") dmsBindsPath := filepath.Join(p.configDir, "dms", "binds.kdl")
if _, err := os.Stat(dmsBindsPath); err == nil { if _, err := os.Stat(dmsBindsPath); err == nil {
@@ -213,24 +221,25 @@ func (p *NiriParser) finalizeBinds() []NiriKeyBinding {
func (p *NiriParser) addBind(kb *NiriKeyBinding) { func (p *NiriParser) addBind(kb *NiriKeyBinding) {
key := p.formatBindKey(kb) key := p.formatBindKey(kb)
normalizedKey := normalizeNiriBindKey(key)
isDMSBind := strings.Contains(kb.Source, "dms/binds.kdl") isDMSBind := strings.Contains(kb.Source, "dms/binds.kdl")
if isDMSBind { if isDMSBind {
p.dmsBindKeys[key] = true p.dmsBindKeys[normalizedKey] = true
p.dmsBindMap[key] = kb p.dmsBindMap[normalizedKey] = kb
} else if p.dmsBindKeys[key] { } else if p.dmsBindKeys[normalizedKey] {
p.bindsAfterDMS++ p.bindsAfterDMS++
p.conflictingConfigs[key] = kb p.conflictingConfigs[normalizedKey] = kb
p.configBindKeys[key] = true p.configBindKeys[normalizedKey] = true
return return
} else { } else {
p.configBindKeys[key] = true p.configBindKeys[normalizedKey] = true
} }
if _, exists := p.bindMap[key]; !exists { if _, exists := p.bindMap[normalizedKey]; !exists {
p.bindOrder = append(p.bindOrder, key) p.bindOrder = append(p.bindOrder, normalizedKey)
} }
p.bindMap[key] = kb p.bindMap[normalizedKey] = kb
} }
func (p *NiriParser) formatBindKey(kb *NiriKeyBinding) string { func (p *NiriParser) formatBindKey(kb *NiriKeyBinding) string {
@@ -526,6 +526,50 @@ binds {
} }
} }
func TestNiriKeyIdentityIsCaseInsensitive(t *testing.T) {
tmpDir := t.TempDir()
dmsDir := filepath.Join(tmpDir, "dms")
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
t.Fatalf("Failed to create dms dir: %v", err)
}
config := `binds {
Alt+Space hotkey-overlay-title="Spotlight Bar" { spawn "dms" "ipc" "call" "spotlight-bar" "toggle"; }
}
include "dms/binds.kdl"
`
if err := os.WriteFile(filepath.Join(tmpDir, "config.kdl"), []byte(config), 0o644); err != nil {
t.Fatalf("Failed to write config: %v", err)
}
include := `binds {
Alt+space hotkey-overlay-title="Default Launcher" { spawn "dms" "ipc" "call" "spotlight" "toggle"; }
}
`
if err := os.WriteFile(filepath.Join(dmsDir, "binds.kdl"), []byte(include), 0o644); err != nil {
t.Fatalf("Failed to write binds include: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed: %v", err)
}
var altSpaceBinds []NiriKeyBinding
parser := NewNiriParser("")
for _, kb := range result.Section.Keybinds {
if normalizeNiriBindKey(parser.formatBindKey(&kb)) == "alt+space" {
altSpaceBinds = append(altSpaceBinds, kb)
}
}
if len(altSpaceBinds) != 1 {
t.Fatalf("Expected one Alt+Space identity, got %d", len(altSpaceBinds))
}
if got := altSpaceBinds[0].Args; len(got) < 5 || got[3] != "spotlight" || got[4] != "toggle" {
t.Fatalf("Expected later DMS include to win with spotlight toggle, got action=%s args=%v", altSpaceBinds[0].Action, got)
}
}
func TestNiriParseMultipleArgs(t *testing.T) { func TestNiriParseMultipleArgs(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl") configFile := filepath.Join(tmpDir, "config.kdl")
@@ -367,7 +367,7 @@ func TestNiriEmptyArgsPreservation(t *testing.T) {
} }
for key, expected := range binds { for key, expected := range binds {
loaded, ok := loadedBinds[key] loaded, ok := loadedBinds[normalizeNiriBindKey(key)]
if !ok { if !ok {
t.Errorf("Missing bind for key %s", key) t.Errorf("Missing bind for key %s", key)
continue continue
@@ -1,6 +1,7 @@
package network package network
import ( import (
"encoding/json"
"fmt" "fmt"
"net" "net"
"strings" "strings"
@@ -18,10 +19,41 @@ const (
) )
type linkInfo struct { type linkInfo struct {
ifindex int32 ifindex int32
name string name string
path dbus.ObjectPath path dbus.ObjectPath
opState string opState string
linkType string
}
func (l *linkInfo) isWired() bool {
if l.linkType != "" {
return l.linkType == "ether"
}
if looksVirtual(l.name) || strings.HasPrefix(l.name, "wlan") || strings.HasPrefix(l.name, "wlp") {
return false
}
return true
}
func (l *linkInfo) isWireless() bool {
if l.linkType != "" {
return l.linkType == "wlan"
}
return strings.HasPrefix(l.name, "wlan") || strings.HasPrefix(l.name, "wlp")
}
func looksVirtual(name string) bool {
virtualPrefixes := []string{
"lo", "docker", "veth", "virbr", "br-", "vnet", "tun", "tap",
"vboxnet", "vmnet", "kube", "cni", "flannel", "cali",
}
for _, prefix := range virtualPrefixes {
if strings.HasPrefix(name, prefix) {
return true
}
}
return false
} }
type SystemdNetworkdBackend struct { type SystemdNetworkdBackend struct {
@@ -95,17 +127,50 @@ func (b *SystemdNetworkdBackend) enumerateLinks() error {
defer b.linksMutex.Unlock() defer b.linksMutex.Unlock()
for _, l := range links { for _, l := range links {
b.links[l.Name] = &linkInfo{ if existing, ok := b.links[l.Name]; ok && existing.path == l.Path {
ifindex: l.Ifindex, existing.ifindex = l.Ifindex
name: l.Name, continue
path: l.Path,
} }
log.Debugf("networkd: enumerated link %s (ifindex=%d, path=%s)", l.Name, l.Ifindex, l.Path) info := &linkInfo{
ifindex: l.Ifindex,
name: l.Name,
path: l.Path,
linkType: b.fetchLinkType(l.Path),
}
b.links[l.Name] = info
log.Debugf("networkd: enumerated link %s (ifindex=%d, path=%s, type=%q)", l.Name, l.Ifindex, l.Path, info.linkType)
} }
return nil return nil
} }
// fetchLinkType queries networkd's Describe method and extracts the link Type
// (e.g. "ether", "wlan", "loopback", "none"). Returns empty on failure; callers
// fall back to name-prefix heuristics in that case. The Type is fixed at link
// creation by the kernel, so callers cache the result for the lifetime of the
// linkInfo and only refetch when a link is re-created at a new D-Bus path.
func (b *SystemdNetworkdBackend) fetchLinkType(path dbus.ObjectPath) string {
linkObj := b.conn.Object(networkdBusName, path)
var describeJSON string
if err := linkObj.Call(networkdLinkIface+".Describe", 0).Store(&describeJSON); err != nil {
return ""
}
return parseDescribeType(describeJSON)
}
// parseDescribeType extracts the top-level "Type" field from a networkd
// Describe payload. Returns empty when the JSON is malformed or the field is
// absent, signalling callers to fall back to name-prefix heuristics.
func parseDescribeType(describeJSON string) string {
var parsed struct {
Type string `json:"Type"`
}
if err := json.Unmarshal([]byte(describeJSON), &parsed); err != nil {
return ""
}
return parsed.Type
}
func (b *SystemdNetworkdBackend) updateState() error { func (b *SystemdNetworkdBackend) updateState() error {
b.linksMutex.RLock() b.linksMutex.RLock()
defer b.linksMutex.RUnlock() defer b.linksMutex.RUnlock()
@@ -113,8 +178,8 @@ func (b *SystemdNetworkdBackend) updateState() error {
var wiredIface *linkInfo var wiredIface *linkInfo
var wifiIface *linkInfo var wifiIface *linkInfo
for name, link := range b.links { for _, link := range b.links {
if b.isVirtualInterface(name) { if !link.isWired() && !link.isWireless() {
continue continue
} }
@@ -126,11 +191,11 @@ func (b *SystemdNetworkdBackend) updateState() error {
} }
} }
if strings.HasPrefix(name, "wlan") || strings.HasPrefix(name, "wlp") { if link.isWireless() {
if wifiIface == nil || link.opState == "routable" || link.opState == "carrier" { if wifiIface == nil || link.opState == "routable" || link.opState == "carrier" {
wifiIface = link wifiIface = link
} }
} else if !b.isVirtualInterface(name) { } else if link.isWired() {
if wiredIface == nil || link.opState == "routable" || link.opState == "carrier" { if wiredIface == nil || link.opState == "routable" || link.opState == "carrier" {
wiredIface = link wiredIface = link
} }
@@ -140,7 +205,7 @@ func (b *SystemdNetworkdBackend) updateState() error {
var wiredConns []WiredConnection var wiredConns []WiredConnection
var ethernetDevices []EthernetDevice var ethernetDevices []EthernetDevice
for name, link := range b.links { for name, link := range b.links {
if b.isVirtualInterface(name) || strings.HasPrefix(name, "wlan") || strings.HasPrefix(name, "wlp") { if !link.isWired() {
continue continue
} }
@@ -229,19 +294,6 @@ func (b *SystemdNetworkdBackend) updateState() error {
return nil return nil
} }
func (b *SystemdNetworkdBackend) isVirtualInterface(name string) bool {
virtualPrefixes := []string{
"lo", "docker", "veth", "virbr", "br-", "vnet", "tun", "tap",
"vboxnet", "vmnet", "kube", "cni", "flannel", "cali",
}
for _, prefix := range virtualPrefixes {
if strings.HasPrefix(name, prefix) {
return true
}
}
return false
}
func (b *SystemdNetworkdBackend) getAddresses(ifname string) []string { func (b *SystemdNetworkdBackend) getAddresses(ifname string) []string {
iface, err := net.InterfaceByName(ifname) iface, err := net.InterfaceByName(ifname)
if err != nil { if err != nil {
@@ -12,7 +12,7 @@ func (b *SystemdNetworkdBackend) GetWiredConnections() ([]WiredConnection, error
var conns []WiredConnection var conns []WiredConnection
for name, link := range b.links { for name, link := range b.links {
if b.isVirtualInterface(name) || strings.HasPrefix(name, "wlan") || strings.HasPrefix(name, "wlp") { if !link.isWired() {
continue continue
} }
@@ -73,8 +73,8 @@ func (b *SystemdNetworkdBackend) GetWiredNetworkDetails(id string) (*WiredNetwor
func (b *SystemdNetworkdBackend) ConnectEthernet() error { func (b *SystemdNetworkdBackend) ConnectEthernet() error {
b.linksMutex.RLock() b.linksMutex.RLock()
var primaryWired *linkInfo var primaryWired *linkInfo
for name, l := range b.links { for _, l := range b.links {
if strings.HasPrefix(name, "lo") || strings.HasPrefix(name, "wlan") || strings.HasPrefix(name, "wlp") { if !l.isWired() {
continue continue
} }
primaryWired = l primaryWired = l
@@ -145,3 +145,73 @@ func TestSystemdNetworkdBackend_DisconnectEthernetDevice(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.Error(), "not supported") assert.Contains(t, err.Error(), "not supported")
} }
func TestLinkInfo_Classify(t *testing.T) {
// When networkd reports a Type via Describe, classification is exact.
cases := []struct {
name string
ifname string
linkType string
wantWired bool
wantWifi bool
}{
{"ether type", "dock", "ether", true, false},
{"wlan type", "wifi", "wlan", false, true},
{"loopback type", "lo", "loopback", false, false},
{"none type (tun overlay)", "nebula.homelab", "none", false, false},
{"none type (wireguard)", "wg0", "none", false, false},
// Fallback path: linkType unavailable, name-prefix heuristic applies.
{"fallback enp wired", "enp141s0", "", true, false},
{"fallback wlan wireless", "wlan0", "", false, true},
{"fallback wlp wireless", "wlp3s0", "", false, true},
{"fallback lo skipped", "lo", "", false, false},
{"fallback docker skipped", "docker0", "", false, false},
{"fallback tun skipped", "tun0", "", false, false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
l := &linkInfo{name: tc.ifname, linkType: tc.linkType}
assert.Equal(t, tc.wantWired, l.isWired(), "isWired")
assert.Equal(t, tc.wantWifi, l.isWireless(), "isWireless")
})
}
}
func TestParseDescribeType(t *testing.T) {
// parseDescribeType is the seam between networkd's Describe RPC and the
// classifier. On any failure path it must return "" so callers fall back
// to name-prefix heuristics rather than misclassifying the link.
cases := []struct {
name string
in string
want string
}{
{"ether", `{"Type":"ether","Name":"enp141s0"}`, "ether"},
{"wlan", `{"Type":"wlan","Name":"wlan0"}`, "wlan"},
{"loopback", `{"Type":"loopback","Name":"lo"}`, "loopback"},
{"none with kind", `{"Type":"none","Kind":"tun","Name":"nebula.homelab"}`, "none"},
{"empty payload", ``, ""},
{"empty object", `{}`, ""},
{"missing Type field", `{"Name":"wlan0","Kind":""}`, ""},
{"explicit empty Type", `{"Type":"","Name":"wlan0"}`, ""},
{"malformed json", `{"Type":"ether"`, ""},
{"non-string Type", `{"Type":42}`, ""},
{"unrelated payload", `"just a string"`, ""},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, parseDescribeType(tc.in))
})
}
}
func TestLooksVirtual(t *testing.T) {
virtual := []string{"lo", "docker0", "veth123", "virbr0", "br-abc", "vnet0", "tun0", "tap0", "vboxnet0", "vmnet1", "kube-ipvs0", "cni0", "flannel.1", "cali-abc"}
for _, n := range virtual {
assert.True(t, looksVirtual(n), "%s should look virtual", n)
}
real := []string{"enp141s0", "eno1", "wlan0", "wlp3s0", "wifi", "dock", "nebula.homelab", "wg0"}
for _, n := range real {
assert.False(t, looksVirtual(n), "%s should not look virtual", n)
}
}
+3 -1
View File
@@ -29,7 +29,9 @@ override_dh_auto_install:
install -Dm644 $$SOURCE_DIR/LICENSE \ install -Dm644 $$SOURCE_DIR/LICENSE \
debian/dms-greeter/usr/share/doc/dms-greeter/LICENSE && \ debian/dms-greeter/usr/share/doc/dms-greeter/LICENSE && \
install -Dpm0644 $$SOURCE_DIR/systemd/tmpfiles-dms-greeter.conf \ install -Dpm0644 $$SOURCE_DIR/systemd/tmpfiles-dms-greeter.conf \
debian/dms-greeter/usr/lib/tmpfiles.d/dms-greeter.conf; \ debian/dms-greeter/usr/lib/tmpfiles.d/dms-greeter.conf && \
install -Dm644 $$SOURCE_DIR/systemd/sysusers-dms-greeter.conf \
debian/dms-greeter/usr/lib/sysusers.d/dms-greeter.conf; \
else \ else \
echo "ERROR: No upstream source (dms-qml or Modules/Greetd/assets/dms-greeter)!" && \ echo "ERROR: No upstream source (dms-qml or Modules/Greetd/assets/dms-greeter)!" && \
echo "Contents of current directory:" && ls -la && exit 1; \ echo "Contents of current directory:" && ls -la && exit 1; \
+3
View File
@@ -53,6 +53,8 @@ install -Dm644 %{_builddir}/dms-qml/Modules/Greetd/README.md %{buildroot}%{_docd
install -Dpm0644 %{_builddir}/dms-qml/systemd/tmpfiles-dms-greeter.conf %{buildroot}%{_tmpfilesdir}/dms-greeter.conf install -Dpm0644 %{_builddir}/dms-qml/systemd/tmpfiles-dms-greeter.conf %{buildroot}%{_tmpfilesdir}/dms-greeter.conf
install -Dm644 %{_builddir}/dms-qml/systemd/sysusers-dms-greeter.conf %{buildroot}%{_sysusersdir}/dms-greeter.conf
install -Dm644 %{_builddir}/dms-qml/LICENSE %{buildroot}%{_docdir}/dms-greeter/LICENSE install -Dm644 %{_builddir}/dms-qml/LICENSE %{buildroot}%{_docdir}/dms-greeter/LICENSE
install -dm755 %{buildroot}%{_sharedstatedir}/greeter install -dm755 %{buildroot}%{_sharedstatedir}/greeter
@@ -78,6 +80,7 @@ fi
%{_bindir}/dms-greeter %{_bindir}/dms-greeter
%{_datadir}/quickshell/dms-greeter/ %{_datadir}/quickshell/dms-greeter/
%{_tmpfilesdir}/%{name}.conf %{_tmpfilesdir}/%{name}.conf
%{_sysusersdir}/dms-greeter.conf
%pre %pre
# Create greeter user/group if they don't exist # Create greeter user/group if they don't exist
+3
View File
@@ -53,6 +53,8 @@ install -Dm644 %{_builddir}/dms-qml/Modules/Greetd/README.md %{buildroot}%{_docd
install -Dpm0644 %{_builddir}/dms-qml/systemd/tmpfiles-dms-greeter.conf %{buildroot}%{_tmpfilesdir}/dms-greeter.conf install -Dpm0644 %{_builddir}/dms-qml/systemd/tmpfiles-dms-greeter.conf %{buildroot}%{_tmpfilesdir}/dms-greeter.conf
install -Dm644 %{_builddir}/dms-qml/systemd/sysusers-dms-greeter.conf %{buildroot}%{_sysusersdir}/dms-greeter.conf
install -Dm644 %{_builddir}/dms-qml/LICENSE %{buildroot}%{_docdir}/dms-greeter/LICENSE install -Dm644 %{_builddir}/dms-qml/LICENSE %{buildroot}%{_docdir}/dms-greeter/LICENSE
install -dm755 %{buildroot}%{_sharedstatedir}/greeter install -dm755 %{buildroot}%{_sharedstatedir}/greeter
@@ -78,6 +80,7 @@ fi
%dir %{_datadir}/quickshell %dir %{_datadir}/quickshell
%{_datadir}/quickshell/dms-greeter/ %{_datadir}/quickshell/dms-greeter/
%{_tmpfilesdir}/%{name}.conf %{_tmpfilesdir}/%{name}.conf
%{_sysusersdir}/dms-greeter.conf
%pre %pre
# Create greeter user/group if they don't exist # Create greeter user/group if they don't exist
+5
View File
@@ -40,6 +40,11 @@ override_dh_auto_install:
install -Dm644 DankMaterialShell-$(BASE_VERSION)/LICENSE \ install -Dm644 DankMaterialShell-$(BASE_VERSION)/LICENSE \
debian/dms-greeter/usr/share/doc/dms-greeter/LICENSE debian/dms-greeter/usr/share/doc/dms-greeter/LICENSE
install -Dpm0644 DankMaterialShell-$(BASE_VERSION)/quickshell/systemd/tmpfiles-dms-greeter.conf \
debian/dms-greeter/usr/lib/tmpfiles.d/dms-greeter.conf
install -Dm644 DankMaterialShell-$(BASE_VERSION)/quickshell/systemd/sysusers-dms-greeter.conf \
debian/dms-greeter/usr/lib/sysusers.d/dms-greeter.conf
# Create cache directory structure (will be created by postinst) # Create cache directory structure (will be created by postinst)
mkdir -p debian/dms-greeter/var/cache/dms-greeter mkdir -p debian/dms-greeter/var/cache/dms-greeter
+46
View File
@@ -212,6 +212,52 @@ dms ipc call lock lock
dms ipc call lock isLocked dms ipc call lock isLocked
``` ```
## Target: `sessions`
Logind session enumeration and seat-local session switching. Wraps `loginctl list-sessions` and `loginctl activate`. Only switches between sessions that are *already running* on the current seat — creating a fresh login as another user requires a multi-session greeter setup (greetd-flexiserver / GDM / LightDM) and is out of scope.
### Functions
**`list`**
- Print every session DMS knows about as tab-separated columns: `sessionId\tusername\tseat\ttty\ttype\tcurrent-marker`
- Returns: Multi-line string. The current session is marked with `*current*`.
**`refresh`**
- Re-enumerate sessions in the background (the picker also refreshes itself on open)
- Returns: `"ok"`
**`open`**
- Refresh and open the Switch User picker on the focused screen
- Returns: `"ok"`
**`activate <sessionId>`**
- Activate a session by its numeric logind ID (the `Id=` field from `loginctl show-session`). Performs a VT switch
- Parameters: `sessionId` - Numeric session ID
- Returns: `"ok"` on dispatch, `"ERROR: missing session id"` if blank
- Note: Failures from `loginctl activate` surface through the `switchFailed` QML signal and a Log warning — the IPC call returns success once the spawn is queued, not after activation completes
**`switchTo <target>`**
- Switch to another session by username *or* session ID. The first non-current session matching the username wins; if there's no match, the call fails through the same logging path as `activate`
- Parameters: `target` - Username (e.g. `testuser2`) or numeric session ID
- Returns: `"ok"` on dispatch, `"ERROR: missing target (username or session id)"` if blank
### Examples
```bash
# Inspect what's switchable
dms ipc call sessions list
# Open the picker (useful for a keybind)
dms ipc call sessions open
# Jump straight to another logged-in user without the picker
dms ipc call sessions switchTo testuser2
# Or by session ID, when the user has multiple sessions
dms ipc call sessions activate 4
```
The dedicated `dms switch-user [target]` CLI command wraps the same behavior with a friendlier error path (it prints the switchable list when no target matches).
## Target: `inhibit` ## Target: `inhibit`
Idle inhibitor control to prevent automatic sleep/lock. Idle inhibitor control to prevent automatic sleep/lock.
+7 -4
View File
@@ -8,9 +8,12 @@ const ACTION_TYPES = [
]; ];
const DMS_ACTIONS = [ const DMS_ACTIONS = [
{ id: "spawn dms ipc call spotlight toggle", label: "App Launcher: Toggle" }, { id: "spawn dms ipc call spotlight toggle", label: "Default Launcher: Toggle" },
{ id: "spawn dms ipc call spotlight open", label: "App Launcher: Open" }, { id: "spawn dms ipc call spotlight open", label: "Default Launcher: Open" },
{ id: "spawn dms ipc call spotlight close", label: "App Launcher: Close" }, { id: "spawn dms ipc call spotlight close", label: "Default Launcher: Close" },
{ id: "spawn dms ipc call spotlight-bar toggle", label: "Spotlight Bar: Toggle" },
{ id: "spawn dms ipc call spotlight-bar open", label: "Spotlight Bar: Open" },
{ id: "spawn dms ipc call spotlight-bar close", label: "Spotlight Bar: Close" },
{ id: "spawn dms ipc call clipboard toggle", label: "Clipboard: Toggle" }, { id: "spawn dms ipc call clipboard toggle", label: "Clipboard: Toggle" },
{ id: "spawn dms ipc call clipboard open", label: "Clipboard: Open" }, { id: "spawn dms ipc call clipboard open", label: "Clipboard: Open" },
{ id: "spawn dms ipc call clipboard close", label: "Clipboard: Close" }, { id: "spawn dms ipc call clipboard close", label: "Clipboard: Close" },
@@ -63,7 +66,7 @@ const DMS_ACTIONS = [
{ id: "spawn dms ipc call mpris increment 5", label: "Player Volume Up (5%)" }, { id: "spawn dms ipc call mpris increment 5", label: "Player Volume Up (5%)" },
{ id: "spawn dms ipc call mpris decrement 5", label: "Player Volume Down (5%)" }, { id: "spawn dms ipc call mpris decrement 5", label: "Player Volume Down (5%)" },
{ id: "spawn dms ipc call audio mute", label: "Volume Mute Toggle" }, { id: "spawn dms ipc call audio mute", label: "Volume Mute Toggle" },
{ id: "spawn dms ipc call audio micmute", label: "Microphone Mute Toggle" }, { id: "spawn dms ipc call mic mute", label: "Microphone Mute Toggle" },
{ id: "spawn dms ipc call audio cycleoutput", label: "Audio Output: Cycle" }, { id: "spawn dms ipc call audio cycleoutput", label: "Audio Output: Cycle" },
{ id: "spawn dms ipc call brightness increment 5 \"\"", label: "Brightness Up" }, { id: "spawn dms ipc call brightness increment 5 \"\"", label: "Brightness Up" },
{ id: "spawn dms ipc call brightness increment 1 \"\"", label: "Brightness Up (1%)" }, { id: "spawn dms ipc call brightness increment 1 \"\"", label: "Brightness Up (1%)" },
+11 -3
View File
@@ -9,9 +9,11 @@ Singleton {
property var currentOSDsByScreen: ({}) property var currentOSDsByScreen: ({})
Connections { Timer {
target: Quickshell id: screensChangedDelayTimer
function onScreensChanged() { interval: 3000 // 3 seconds
repeat: false
onTriggered: {
const activeNames = {}; const activeNames = {};
for (let i = 0; i < Quickshell.screens.length; i++) for (let i = 0; i < Quickshell.screens.length; i++)
activeNames[Quickshell.screens[i].name] = true; activeNames[Quickshell.screens[i].name] = true;
@@ -22,6 +24,12 @@ Singleton {
} }
} }
} }
Connections {
target: Quickshell
function onScreensChanged() {
screensChangedDelayTimer.restart();
}
}
function showOSD(osd) { function showOSD(osd) {
if (!osd || !osd.screen) if (!osd || !osd.screen)
+12
View File
@@ -187,6 +187,7 @@ Singleton {
property string timeLocale: "" property string timeLocale: ""
property string launcherLastMode: "all" property string launcherLastMode: "all"
property string launcherLastFileSearchType: "all"
property string launcherLastQuery: "" property string launcherLastQuery: ""
property var launcherQueryHistory: [] property var launcherQueryHistory: []
property string appDrawerLastMode: "apps" property string appDrawerLastMode: "apps"
@@ -1178,6 +1179,17 @@ Singleton {
saveSettings(); saveSettings();
} }
function getLauncherRestoreMode() {
if (!SettingsData.rememberLastMode)
return "all";
return launcherLastMode || "all";
}
function setLauncherLastFileSearchType(type) {
launcherLastFileSearchType = type;
saveSettings();
}
function setLauncherLastQuery(query) { function setLauncherLastQuery(query) {
launcherLastQuery = query; launcherLastQuery = query;
saveSettings(); saveSettings();
+8 -3
View File
@@ -258,8 +258,6 @@ Singleton {
onFrameLauncherEmergeSideChanged: saveSettings() onFrameLauncherEmergeSideChanged: saveSettings()
property bool frameLauncherArcExtender: false property bool frameLauncherArcExtender: false
onFrameLauncherArcExtenderChanged: saveSettings() onFrameLauncherArcExtenderChanged: saveSettings()
property bool frameUseSpotlightLauncher: false
onFrameUseSpotlightLauncherChanged: saveSettings()
readonly property string frameModalEmergeSide: frameLauncherEmergeSide === "top" ? "bottom" : "top" readonly property string frameModalEmergeSide: frameLauncherEmergeSide === "top" ? "bottom" : "top"
property string frameMode: "connected" property string frameMode: "connected"
onFrameModeChanged: saveSettings() onFrameModeChanged: saveSettings()
@@ -394,6 +392,7 @@ Singleton {
property string audioScrollMode: "volume" property string audioScrollMode: "volume"
property int audioWheelScrollAmount: 5 property int audioWheelScrollAmount: 5
property bool clockCompactMode: false property bool clockCompactMode: false
property int focusedWindowSize: 1
property bool focusedWindowCompactMode: false property bool focusedWindowCompactMode: false
property bool runningAppsCompactMode: true property bool runningAppsCompactMode: true
property int barMaxVisibleApps: 0 property int barMaxVisibleApps: 0
@@ -436,6 +435,7 @@ Singleton {
property int appLauncherGridColumns: 4 property int appLauncherGridColumns: 4
property bool spotlightCloseNiriOverview: true property bool spotlightCloseNiriOverview: true
property bool rememberLastQuery: false property bool rememberLastQuery: false
property bool rememberLastMode: true
property var spotlightSectionViewModes: ({}) property var spotlightSectionViewModes: ({})
onSpotlightSectionViewModesChanged: saveSettings() onSpotlightSectionViewModesChanged: saveSettings()
property var appDrawerSectionViewModes: ({}) property var appDrawerSectionViewModes: ({})
@@ -449,7 +449,9 @@ Singleton {
property bool dankLauncherV2UnloadOnClose: false property bool dankLauncherV2UnloadOnClose: false
property bool dankLauncherV2IncludeFilesInAll: false property bool dankLauncherV2IncludeFilesInAll: false
property bool dankLauncherV2IncludeFoldersInAll: false property bool dankLauncherV2IncludeFoldersInAll: false
property bool launcherUseOverlayLayer: false
property string launcherStyle: "full" property string launcherStyle: "full"
property bool spotlightBarShowModeChips: false
property string _legacyWeatherLocation: "New York, NY" property string _legacyWeatherLocation: "New York, NY"
property string _legacyWeatherCoordinates: "40.7128,-74.0060" property string _legacyWeatherCoordinates: "40.7128,-74.0060"
@@ -606,7 +608,7 @@ Singleton {
property bool showDock: false property bool showDock: false
property bool dockAutoHide: false property bool dockAutoHide: false
property bool dockSmartAutoHide: false property bool dockSmartAutoHide: false
property bool dockHideOnFullscreen: true property bool dockUseOverlayLayer: false
property bool dockGroupByApp: false property bool dockGroupByApp: false
property bool dockRestoreSpecialWorkspaceOnClick: false property bool dockRestoreSpecialWorkspaceOnClick: false
property bool dockOpenOnOverview: false property bool dockOpenOnOverview: false
@@ -686,6 +688,7 @@ Singleton {
property int notificationTimeoutNormal: 5000 property int notificationTimeoutNormal: 5000
property int notificationTimeoutCritical: 0 property int notificationTimeoutCritical: 0
property bool notificationCompactMode: false property bool notificationCompactMode: false
property bool notificationDedupeEnabled: true
property int notificationPopupPosition: SettingsData.Position.Top property int notificationPopupPosition: SettingsData.Position.Top
property int notificationAnimationSpeed: SettingsData.AnimationSpeed.Short property int notificationAnimationSpeed: SettingsData.AnimationSpeed.Short
property int notificationCustomAnimationDuration: 400 property int notificationCustomAnimationDuration: 400
@@ -706,6 +709,7 @@ Singleton {
property bool osdBrightnessEnabled: true property bool osdBrightnessEnabled: true
property bool osdIdleInhibitorEnabled: true property bool osdIdleInhibitorEnabled: true
property bool osdMicMuteEnabled: true property bool osdMicMuteEnabled: true
property bool osdMicVolumeEnabled: true
property bool osdCapsLockEnabled: true property bool osdCapsLockEnabled: true
property bool osdPowerProfileEnabled: true property bool osdPowerProfileEnabled: true
property bool osdAudioOutputEnabled: true property bool osdAudioOutputEnabled: true
@@ -787,6 +791,7 @@ Singleton {
"popupGapsAuto": true, "popupGapsAuto": true,
"popupGapsManual": 4, "popupGapsManual": 4,
"maximizeDetection": true, "maximizeDetection": true,
"useOverlayLayer": false,
"scrollEnabled": true, "scrollEnabled": true,
"scrollXBehavior": "column", "scrollXBehavior": "column",
"scrollYBehavior": "workspace", "scrollYBehavior": "workspace",
@@ -87,6 +87,7 @@ var SPEC = {
timeLocale: { def: "" }, timeLocale: { def: "" },
launcherLastMode: { def: "all" }, launcherLastMode: { def: "all" },
launcherLastFileSearchType: { def: "all" },
launcherLastQuery: { def: "" }, launcherLastQuery: { def: "" },
launcherQueryHistory: { def: [] }, launcherQueryHistory: { def: [] },
appDrawerLastMode: { def: "apps" }, appDrawerLastMode: { def: "apps" },
+7 -3
View File
@@ -153,6 +153,7 @@ var SPEC = {
audioWheelScrollAmount: { def: 5 }, audioWheelScrollAmount: { def: 5 },
clockCompactMode: { def: false }, clockCompactMode: { def: false },
focusedWindowCompactMode: { def: false }, focusedWindowCompactMode: { def: false },
focusedWindowSize: { def: 1 },
runningAppsCompactMode: { def: true }, runningAppsCompactMode: { def: true },
barMaxVisibleApps: { def: 0 }, barMaxVisibleApps: { def: 0 },
barMaxVisibleRunningApps: { def: 0 }, barMaxVisibleRunningApps: { def: 0 },
@@ -202,6 +203,7 @@ var SPEC = {
appLauncherGridColumns: { def: 4 }, appLauncherGridColumns: { def: 4 },
spotlightCloseNiriOverview: { def: true }, spotlightCloseNiriOverview: { def: true },
rememberLastQuery: { def: false }, rememberLastQuery: { def: false },
rememberLastMode: { def: true },
spotlightSectionViewModes: { def: {} }, spotlightSectionViewModes: { def: {} },
appDrawerSectionViewModes: { def: {} }, appDrawerSectionViewModes: { def: {} },
niriOverviewOverlayEnabled: { def: true }, niriOverviewOverlayEnabled: { def: true },
@@ -213,7 +215,9 @@ var SPEC = {
dankLauncherV2UnloadOnClose: { def: false }, dankLauncherV2UnloadOnClose: { def: false },
dankLauncherV2IncludeFilesInAll: { def: false }, dankLauncherV2IncludeFilesInAll: { def: false },
dankLauncherV2IncludeFoldersInAll: { def: false }, dankLauncherV2IncludeFoldersInAll: { def: false },
launcherUseOverlayLayer: { def: false },
launcherStyle: { def: "full" }, launcherStyle: { def: "full" },
spotlightBarShowModeChips: { def: false },
useAutoLocation: { def: false }, useAutoLocation: { def: false },
weatherEnabled: { def: true }, weatherEnabled: { def: true },
@@ -332,7 +336,7 @@ var SPEC = {
showDock: { def: false }, showDock: { def: false },
dockAutoHide: { def: false }, dockAutoHide: { def: false },
dockSmartAutoHide: { def: false }, dockSmartAutoHide: { def: false },
dockHideOnFullscreen: { def: true }, dockUseOverlayLayer: { def: false },
dockGroupByApp: { def: false }, dockGroupByApp: { def: false },
dockRestoreSpecialWorkspaceOnClick: { def: false }, dockRestoreSpecialWorkspaceOnClick: { def: false },
dockOpenOnOverview: { def: false }, dockOpenOnOverview: { def: false },
@@ -395,6 +399,7 @@ var SPEC = {
notificationTimeoutNormal: { def: 5000 }, notificationTimeoutNormal: { def: 5000 },
notificationTimeoutCritical: { def: 0 }, notificationTimeoutCritical: { def: 0 },
notificationCompactMode: { def: false }, notificationCompactMode: { def: false },
notificationDedupeEnabled: { def: true },
notificationPopupPosition: { def: 0 }, notificationPopupPosition: { def: 0 },
notificationAnimationSpeed: { def: 1 }, notificationAnimationSpeed: { def: 1 },
notificationCustomAnimationDuration: { def: 400 }, notificationCustomAnimationDuration: { def: 400 },
@@ -496,7 +501,7 @@ var SPEC = {
popupGapsAuto: true, popupGapsAuto: true,
popupGapsManual: 4, popupGapsManual: 4,
maximizeDetection: true, maximizeDetection: true,
fullscreenDetection: true, useOverlayLayer: false,
scrollEnabled: true, scrollEnabled: true,
scrollXBehavior: "column", scrollXBehavior: "column",
scrollYBehavior: "workspace", scrollYBehavior: "workspace",
@@ -573,7 +578,6 @@ var SPEC = {
frameCloseGaps: { def: true }, frameCloseGaps: { def: true },
frameLauncherEmergeSide: { def: "bottom" }, frameLauncherEmergeSide: { def: "bottom" },
frameLauncherArcExtender: { def: false }, frameLauncherArcExtender: { def: false },
frameUseSpotlightLauncher: { def: false },
frameMode: { def: "connected" } frameMode: { def: "connected" }
}; };
+150 -4
View File
@@ -30,6 +30,7 @@ import qs.Services
Item { Item {
id: root id: root
readonly property var log: Log.scoped("DMSShell") readonly property var log: Log.scoped("DMSShell")
readonly property var _sessionsServiceRef: SessionsService
property bool osdSurfacesLoaded: true property bool osdSurfacesLoaded: true
property int pendingOsdResumeReloads: 0 property int pendingOsdResumeReloads: 0
@@ -63,15 +64,27 @@ Item {
} }
} }
property bool wallpaperSurfacesLoaded: true
Loader { Loader {
id: blurredWallpaperBackgroundLoader id: blurredWallpaperBackgroundLoader
active: SettingsData.blurredWallpaperLayer && CompositorService.isNiri active: root.wallpaperSurfacesLoaded && SettingsData.blurredWallpaperLayer && CompositorService.isNiri
asynchronous: false asynchronous: false
sourceComponent: BlurredWallpaperBackground {} sourceComponent: BlurredWallpaperBackground {}
} }
WallpaperBackground {} DeferredAction {
id: wallpaperSurfaceReloadAction
onTriggered: root.wallpaperSurfacesLoaded = true
}
Loader {
id: wallpaperBackgroundLoader
active: root.wallpaperSurfacesLoaded
asynchronous: false
sourceComponent: WallpaperBackground {}
}
DesktopWidgetLayer {} DesktopWidgetLayer {}
@@ -168,6 +181,8 @@ Item {
property bool barSurfacesLoaded: true property bool barSurfacesLoaded: true
function recreateBarSurfaces() { function recreateBarSurfaces() {
log.info("Recreating bar surfaces, screens:", Quickshell.screens.length,
Quickshell.screens.map(s => s.name).join(","));
if (barSurfacesLoaded) if (barSurfacesLoaded)
barSurfacesLoaded = false; barSurfacesLoaded = false;
barSurfaceReloadAction.schedule(); barSurfaceReloadAction.schedule();
@@ -217,7 +232,18 @@ Item {
} }
} }
Frame {} property bool frameSurfacesLoaded: true
Loader {
active: root.frameSurfacesLoaded
asynchronous: false
sourceComponent: Frame {}
}
DeferredAction {
id: frameSurfaceReloadAction
onTriggered: root.frameSurfacesLoaded = true
}
Repeater { Repeater {
id: dankBarRepeater id: dankBarRepeater
@@ -301,6 +327,81 @@ Item {
onTriggered: root.osdSurfacesLoaded = true onTriggered: root.osdSurfacesLoaded = true
} }
property bool hadRealScreen: true
function _hasRealScreen() {
for (let i = 0; i < Quickshell.screens.length; i++) {
if (Quickshell.screens[i].name.length > 0)
return true;
}
return false;
}
function triggerSurfaceRecovery(source) {
log.info("Surface recovery triggered by:", source,
"screens:", Quickshell.screens.length,
Quickshell.screens.map(s => s.name).join(","),
"barLoaded:", root.barSurfacesLoaded,
"frameLoaded:", root.frameSurfacesLoaded,
"dockEnabled:", root.dockEnabled);
surfaceResumeRecoveryTimer.pass = 0;
surfaceResumeRecoveryTimer.interval = 800;
surfaceResumeRecoveryTimer.restart();
}
Connections {
target: Quickshell
function onScreensChanged() {
const hasReal = root._hasRealScreen();
log.info("Screens changed:", Quickshell.screens.length,
Quickshell.screens.map(s => "'" + s.name + "'").join(","),
"hasReal:", hasReal, "hadReal:", root.hadRealScreen);
if (!root.hadRealScreen && hasReal) {
log.info("Real screen reappeared after placeholder state, triggering surface recovery");
root.triggerSurfaceRecovery("screen-reconnect");
}
root.hadRealScreen = hasReal;
}
}
Timer {
id: surfaceResumeRecoveryTimer
interval: 800
repeat: false
property int pass: 0
onTriggered: {
pass++;
log.info("Surface recovery pass", pass,
"screens:", Quickshell.screens.length,
Quickshell.screens.map(s => s.name).join(","));
root.recreateBarSurfaces();
if (root.frameSurfacesLoaded) {
root.frameSurfacesLoaded = false;
frameSurfaceReloadAction.schedule();
}
if (root.wallpaperSurfacesLoaded) {
root.wallpaperSurfacesLoaded = false;
wallpaperSurfaceReloadAction.schedule();
}
root.dockEnabled = false;
Qt.callLater(() => {
root.dockEnabled = true;
});
if (pass < 2) {
interval = 2000;
restart();
} else {
pass = 0;
interval = 800;
}
}
}
Component.onCompleted: { Component.onCompleted: {
dockRecreateDebounce.start(); dockRecreateDebounce.start();
// Force PolkitService singleton to initialize // Force PolkitService singleton to initialize
@@ -725,6 +826,25 @@ Item {
} }
} }
LazyLoader {
id: spotlightBarModalLoader
active: false
Component.onCompleted: {
PopoutService.spotlightBarModalLoader = spotlightBarModalLoader;
}
DankLauncherV2ModalSpotlight {
id: spotlightBarModal
Component.onCompleted: {
PopoutService.spotlightBarModal = spotlightBarModal;
PopoutService._onSpotlightBarModalLoaded();
}
}
}
LazyLoader { LazyLoader {
id: clipboardHistoryPopoutLoader id: clipboardHistoryPopoutLoader
@@ -868,9 +988,17 @@ Item {
target: SessionService target: SessionService
function onSessionResumed() { function onSessionResumed() {
log.info("Session resumed: screens:", Quickshell.screens.length,
Quickshell.screens.map(s => s.name).join(","),
"barLoaded:", root.barSurfacesLoaded,
"frameLoaded:", root.frameSurfacesLoaded,
"dockEnabled:", root.dockEnabled);
root.pendingOsdResumeReloads = 2; root.pendingOsdResumeReloads = 2;
osdResumeRecreateTimer.interval = 400; osdResumeRecreateTimer.interval = 400;
osdResumeRecreateTimer.restart(); osdResumeRecreateTimer.restart();
root.triggerSurfaceRecovery("sessionResumed");
} }
} }
@@ -1019,12 +1147,30 @@ Item {
lock.activate(); lock.activate();
} }
onSwitchUserRequested: {
switchUserModalLoader.active = true;
Qt.callLater(() => {
if (switchUserModalLoader.item)
switchUserModalLoader.item.showFromPowerMenu();
});
}
Component.onCompleted: { Component.onCompleted: {
PopoutService.powerMenuModal = powerMenuModal; PopoutService.powerMenuModal = powerMenuModal;
} }
} }
} }
LazyLoader {
id: switchUserModalLoader
active: false
SwitchUserModal {
id: switchUserModal
}
}
LazyLoader { LazyLoader {
id: hyprKeybindsModalLoader id: hyprKeybindsModalLoader
@@ -1095,7 +1241,7 @@ Item {
Variants { Variants {
model: SettingsData.getFilteredScreens("osd") model: SettingsData.getFilteredScreens("osd")
delegate: MicMuteOSD { delegate: MicVolumeOSD {
modelData: item modelData: item
} }
} }
+49
View File
@@ -1340,6 +1340,25 @@ Item {
target: "spotlight" target: "spotlight"
} }
IpcHandler {
function open(): string {
PopoutService.openSpotlightBar();
return "SPOTLIGHT_BAR_OPEN_SUCCESS";
}
function close(): string {
PopoutService.closeSpotlightBar();
return "SPOTLIGHT_BAR_CLOSE_SUCCESS";
}
function toggle(): string {
PopoutService.toggleSpotlightBar();
return "SPOTLIGHT_BAR_TOGGLE_SUCCESS";
}
target: "spotlight-bar"
}
IpcHandler { IpcHandler {
function info(message: string): string { function info(message: string): string {
if (!message) if (!message)
@@ -1775,6 +1794,36 @@ Item {
target: "outputs" target: "outputs"
} }
IpcHandler {
target: "mic"
function setvolume(percentage: string): string {
return AudioService.setMicVolume(parseInt(percentage));
}
function increment(step: string): string {
return AudioService.incrementMicVolume(step);
}
function decrement(step: string): string {
return AudioService.decrementMicVolume(step);
}
function mute(): string {
return AudioService.toggleMicMute();
}
function status(): string {
if (!AudioService.source || !AudioService.source.audio) {
return "No audio source available";
}
const volume = Math.round(AudioService.source.audio.volume * 100);
const muteStatus = AudioService.source.audio.muted ? " (muted)" : "";
return `Microphone: ${volume}%${muteStatus}`;
}
}
IpcHandler { IpcHandler {
function findTrayItem(itemId: string): var { function findTrayItem(itemId: string): var {
if (!itemId) if (!itemId)
@@ -145,6 +145,7 @@ Item {
onDeleteRequested: clipboardContent.modal.deleteEntry(modelData) onDeleteRequested: clipboardContent.modal.deleteEntry(modelData)
onPinRequested: clipboardContent.modal.pinEntry(modelData) onPinRequested: clipboardContent.modal.pinEntry(modelData)
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData) onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
onEditRequested: clipboardContent.modal.editEntry(modelData)
} }
} }
@@ -204,6 +205,7 @@ Item {
onDeleteRequested: clipboardContent.modal.deletePinnedEntry(modelData) onDeleteRequested: clipboardContent.modal.deletePinnedEntry(modelData)
onPinRequested: clipboardContent.modal.pinEntry(modelData) onPinRequested: clipboardContent.modal.pinEntry(modelData)
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData) onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
onEditRequested: clipboardContent.modal.editEntry(modelData)
} }
} }
@@ -0,0 +1,519 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
required property var modal
property var keyController: null
property var entry: null
property string editorText: ""
function decodeEntryData(data) {
if (!data) {
return "";
}
if (typeof data !== "string") {
return String(data);
}
const sanitized = data.replace(/\s+/g, "");
if (sanitized.length === 0) {
return "";
}
try {
const chars = new Array(sanitized.length);
for (let i = 0; i < sanitized.length; i++) {
chars[i] = sanitized.charAt(i);
}
let buffer = null;
if (typeof Qt !== "undefined" && typeof Qt.atob === "function") {
buffer = Qt.atob(chars);
} else if (typeof atob === "function") {
const binary = atob(sanitized);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
buffer = bytes.buffer;
}
if (!buffer || buffer.byteLength === 0) {
return data;
}
const bytes = new Uint8Array(buffer);
let binary = "";
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
try {
return decodeURIComponent(escape(binary));
} catch (e) {
return binary;
}
} catch (e) {
return data;
}
}
function setEntry(newEntry) {
entry = newEntry;
editorText = newEntry?.text ?? newEntry?.preview ?? "";
if (editField) {
editField.text = editorText;
}
Qt.callLater(function () {
if (editField) {
editField.forceActiveFocus();
}
});
if (!newEntry || newEntry.isImage) {
return;
}
const requestedId = newEntry.id;
DMSService.sendRequest("clipboard.getEntry", {
"id": requestedId
}, function (response) {
if (response.error) {
return;
}
if (!root.entry || root.entry.id !== requestedId) {
return;
}
const result = response.result;
let fullText = "";
if (result?.data) {
fullText = root.decodeEntryData(result.data);
} else {
fullText = result?.preview ?? "";
}
if (!fullText || fullText.length === 0) {
return;
}
root.editorText = fullText;
if (editField) {
editField.text = fullText;
}
});
}
function saveEntry(action) {
const saveAction = action ?? "history";
DMSService.sendRequest("clipboard.copy", {
"text": root.editorText
}, function (response) {
if (response.error) {
ToastService.showError(I18n.tr("Failed to update clipboard"));
return;
}
if (saveAction === "history") {
modal.mode = "history";
Qt.callLater(function () {
ClipboardService.reset();
ClipboardService.refresh();
if (keyController) {
keyController.reset();
}
});
return;
}
if (saveAction === "close") {
modal.hide();
return;
}
if (saveAction === "paste") {
ClipboardService.pasteClipboard(modal.hide);
}
});
}
function positionSaveMenu() {
saveMenu.width = Math.max(saveMenuColumn.implicitWidth + saveMenu.padding * 2, saveButton.width);
const pos = saveButton.mapToItem(Overlay.overlay, 0, 0);
const popupW = saveMenu.width;
const popupH = saveMenu.height;
const overlayW = Overlay.overlay.width;
const overlayH = Overlay.overlay.height;
let x = pos.x + (saveButton.width - popupW) / 2;
let y = pos.y + saveButton.height + 4;
if (y + popupH > overlayH) {
y = pos.y - popupH - 4;
}
x = Math.max(8, Math.min(x, overlayW - popupW - 8));
y = Math.max(8, y);
saveMenu.x = x;
saveMenu.y = y;
}
function toggleSaveMenu() {
if (saveMenu.visible) {
saveMenu.close();
return;
}
saveMenu.open();
positionSaveMenu();
Qt.callLater(positionSaveMenu);
}
Shortcut {
sequences: ["Escape"]
enabled: modal.mode === "editor"
onActivated: modal.mode = "history"
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
Item {
id: editorHeader
width: parent.width
height: ClipboardConstants.headerHeight
DankActionButton {
iconName: "arrow_back"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
onClicked: modal.mode = "history"
}
StyledText {
text: I18n.tr("Edit Clipboard")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
onClicked: modal.mode = "history"
}
}
StyledRect {
id: editFieldContainer
width: parent.width
height: Math.max(Theme.fontSizeMedium * 8, parent.height - editorHeader.height - editorActions.height - Theme.spacingM * 2)
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: editField.activeFocus ? Theme.primary : Theme.outlineMedium
border.width: editField.activeFocus ? 2 : 1
clip: true
DankIcon {
id: editIcon
name: "edit"
size: Theme.iconSize
color: editField.activeFocus ? Theme.primary : Theme.surfaceVariantText
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.top: parent.top
anchors.topMargin: Theme.spacingM
}
DankFlickable {
id: editScroll
anchors.left: editIcon.right
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingS
clip: true
contentWidth: width
contentHeight: editField.height
TextEdit {
id: editField
width: editScroll.width
height: Math.max(editScroll.height, contentHeight)
text: root.editorText
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
wrapMode: TextEdit.Wrap
selectByMouse: true
onTextChanged: root.editorText = text
Keys.onPressed: function (event) {
const hasCtrl = (event.modifiers & Qt.ControlModifier) !== 0;
const hasShift = (event.modifiers & Qt.ShiftModifier) !== 0;
if (hasCtrl && event.key === Qt.Key_S) {
root.saveEntry(hasShift ? "close" : "history");
event.accepted = true;
return;
}
if (hasCtrl && hasShift && event.key === Qt.Key_V) {
root.saveEntry("paste");
event.accepted = true;
return;
}
}
}
}
StyledText {
text: I18n.tr("Edit clipboard text")
font.pixelSize: Theme.fontSizeMedium
color: Theme.outlineButton
anchors.left: editScroll.left
anchors.right: editScroll.right
anchors.top: editScroll.top
anchors.bottom: editScroll.bottom
visible: editField.text.length === 0 && !editField.activeFocus
wrapMode: Text.WordWrap
}
}
Row {
id: editorActions
width: parent.width
spacing: Theme.spacingS
Item {
id: buttonSpacer
width: Math.max(0, parent.width - cancelButton.width - saveButton.width - Theme.spacingS)
height: 1
}
DankButton {
id: cancelButton
text: I18n.tr("Cancel")
backgroundColor: Theme.surfaceContainerHigh
textColor: Theme.surfaceText
onClicked: modal.mode = "history"
}
Item {
id: saveButton
readonly property int buttonHeight: cancelButton.buttonHeight
readonly property int arrowWidth: Theme.iconSizeLarge
width: cancelButton.width
height: buttonHeight
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Theme.primary
}
Item {
id: saveMainArea
anchors.left: parent.left
anchors.right: saveArrowArea.left
anchors.top: parent.top
anchors.bottom: parent.bottom
}
StyledText {
text: I18n.tr("Save")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.onPrimary
anchors.centerIn: saveMainArea
}
Item {
id: saveArrowArea
width: saveButton.arrowWidth
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
}
Rectangle {
width: 1
height: parent.height - cancelButton.horizontalPadding
color: Theme.withAlpha(Theme.onPrimary, 0.2)
anchors.right: saveArrowArea.left
anchors.verticalCenter: parent.verticalCenter
}
DankIcon {
name: saveMenu.visible ? "expand_less" : "expand_more"
size: Theme.iconSizeSmall
color: Theme.onPrimary
anchors.centerIn: saveArrowArea
}
StateLayer {
z: 1
anchors.fill: saveMainArea
stateColor: Theme.onPrimary
onClicked: root.saveEntry("history")
}
StateLayer {
z: 1
anchors.fill: saveArrowArea
stateColor: Theme.onPrimary
onClicked: root.toggleSaveMenu()
}
}
}
Popup {
id: saveMenu
parent: Overlay.overlay
padding: Theme.spacingM
modal: true
dim: false
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: StyledRect {
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: Theme.outlineMedium
border.width: 1
}
contentItem: Column {
id: saveMenuColumn
spacing: Theme.spacingXS
StyledRect {
implicitWidth: saveMenuRow.implicitWidth + Theme.spacingS * 2
implicitHeight: saveMenuRow.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: saveMenuSaveArea.containsMouse ? Theme.surfaceVariant : "transparent"
Row {
id: saveMenuRow
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "save"
size: Theme.iconSizeSmall
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Save")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
}
}
MouseArea {
id: saveMenuSaveArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
saveMenu.close();
root.saveEntry("history");
}
}
}
StyledRect {
implicitWidth: saveMenuCloseRow.implicitWidth + Theme.spacingS * 2
implicitHeight: saveMenuCloseRow.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: saveMenuCloseArea.containsMouse ? Theme.surfaceVariant : "transparent"
Row {
id: saveMenuCloseRow
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "close"
size: Theme.iconSizeSmall
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Save and close")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
}
}
MouseArea {
id: saveMenuCloseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
saveMenu.close();
root.saveEntry("close");
}
}
}
StyledRect {
implicitWidth: saveMenuPasteRow.implicitWidth + Theme.spacingS * 2
implicitHeight: saveMenuPasteRow.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: saveMenuPasteArea.containsMouse ? Theme.surfaceVariant : "transparent"
opacity: modal.wtypeAvailable ? 1 : 0.5
Row {
id: saveMenuPasteRow
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "content_paste"
size: Theme.iconSizeSmall
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Save and paste")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
}
}
MouseArea {
id: saveMenuPasteArea
anchors.fill: parent
hoverEnabled: true
enabled: modal.wtypeAvailable
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
saveMenu.close();
root.saveEntry("paste");
}
}
}
}
}
}
}
+20 -2
View File
@@ -17,6 +17,7 @@ Rectangle {
signal deleteRequested signal deleteRequested
signal pinRequested signal pinRequested
signal unpinRequested signal unpinRequested
signal editRequested
readonly property string entryType: modal ? modal.getEntryType(entry) : "text" readonly property string entryType: modal ? modal.getEntryType(entry) : "text"
readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : "" readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : ""
@@ -70,6 +71,20 @@ Rectangle {
onClicked: entry.pinned ? unpinRequested() : pinRequested() onClicked: entry.pinned ? unpinRequested() : pinRequested()
} }
DankActionButton {
iconName: "edit"
iconSize: Theme.iconSize - 6
iconColor: Theme.surfaceText
onClicked: {
if (entryType === "image") {
// TODO - forward to editing software
} else {
editRequested();
}
}
}
DankActionButton { DankActionButton {
iconName: "close" iconName: "close"
iconSize: Theme.iconSize - 6 iconSize: Theme.iconSize - 6
@@ -142,8 +157,11 @@ Rectangle {
MouseArea { MouseArea {
id: mouseArea id: mouseArea
anchors.fill: parent anchors.left: parent.left
anchors.rightMargin: 80 anchors.right: actionButtons.left
anchors.rightMargin: Theme.spacingS
anchors.top: parent.top
anchors.bottom: parent.bottom
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onPressed: mouse => { onPressed: mouse => {
@@ -43,6 +43,18 @@ DankModal {
service: ClipboardService service: ClipboardService
} }
property string mode: "history"
onModeChanged: {
if (mode !== "history") {
return;
}
Qt.callLater(function () {
if (contentLoader.item?.searchField) {
contentLoader.item.searchField.forceActiveFocus();
}
});
}
function updateFilteredModel() { function updateFilteredModel() {
ClipboardService.updateFilteredModel(); ClipboardService.updateFilteredModel();
} }
@@ -61,6 +73,7 @@ DankModal {
function show() { function show() {
open(); open();
mode = "history";
activeImageLoads = 0; activeImageLoads = 0;
shouldHaveFocus = true; shouldHaveFocus = true;
ClipboardService.reset(); ClipboardService.reset();
@@ -130,6 +143,21 @@ DankModal {
return ClipboardService.getEntryType(entry); return ClipboardService.getEntryType(entry);
} }
function editEntry(entry) {
if (!entry) {
return;
}
if (entry.isImage) {
return;
}
const editor = contentLoader.item?.editorView;
if (!editor) {
return;
}
editor.setEntry(entry);
mode = "editor";
}
visible: false visible: false
modalWidth: ClipboardConstants.modalWidth modalWidth: ClipboardConstants.modalWidth
modalHeight: ClipboardConstants.modalHeight modalHeight: ClipboardConstants.modalHeight
@@ -138,6 +166,7 @@ DankModal {
borderColor: Theme.outlineMedium borderColor: Theme.outlineMedium
borderWidth: 1 borderWidth: 1
enableShadow: true enableShadow: true
closeOnEscapeKey: mode !== "editor"
onBackgroundClicked: hide() onBackgroundClicked: hide()
modalFocusScope.Keys.onPressed: function (event) { modalFocusScope.Keys.onPressed: function (event) {
keyboardController.handleKey(event); keyboardController.handleKey(event);
@@ -174,9 +203,109 @@ DankModal {
property var confirmDialog: clearConfirmDialog property var confirmDialog: clearConfirmDialog
clipboardContent: Component { clipboardContent: Component {
ClipboardContent { Item {
modal: clipboardHistoryModal id: viewContainer
clearConfirmDialog: clipboardHistoryModal.confirmDialog
property alias editorView: editorView
property alias searchField: historyContent.searchField
anchors.fill: parent
Item {
id: historyView
anchors.fill: parent
opacity: 1
scale: 1
visible: opacity > 0.01
enabled: clipboardHistoryModal.mode === "history"
ClipboardContent {
id: historyContent
anchors.fill: parent
modal: clipboardHistoryModal
clearConfirmDialog: clipboardHistoryModal.confirmDialog
}
}
ClipboardEditor {
id: editorView
anchors.fill: parent
opacity: 0
scale: 0.98
visible: opacity > 0.01
enabled: clipboardHistoryModal.mode === "editor"
focus: clipboardHistoryModal.mode === "editor"
modal: clipboardHistoryModal
keyController: keyboardController
}
states: [
State {
name: "history"
when: clipboardHistoryModal.mode === "history"
PropertyChanges {
target: historyView
opacity: 1
scale: 1
}
PropertyChanges {
target: editorView
opacity: 0
scale: 0.98
}
},
State {
name: "editor"
when: clipboardHistoryModal.mode === "editor"
PropertyChanges {
target: historyView
opacity: 0
scale: 0.98
}
PropertyChanges {
target: editorView
opacity: 1
scale: 1
}
}
]
transitions: [
Transition {
from: "history"
to: "editor"
ParallelAnimation {
NumberAnimation {
property: "opacity"
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
NumberAnimation {
property: "scale"
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
},
Transition {
from: "editor"
to: "history"
ParallelAnimation {
NumberAnimation {
property: "opacity"
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
NumberAnimation {
property: "scale"
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
}
]
} }
} }
} }
@@ -66,7 +66,24 @@ QtObject {
} }
} }
function editSelected() {
const entries = modal.activeTab === "saved" ? ClipboardService.pinnedEntries : ClipboardService.unpinnedEntries;
if (!entries || entries.length === 0) {
return;
}
const index = ClipboardService.selectedIndex >= 0 && ClipboardService.selectedIndex < entries.length ? ClipboardService.selectedIndex : 0;
modal.editEntry(entries[index]);
}
function handleKey(event) { function handleKey(event) {
if (modal.mode === "editor") {
if (event.key === Qt.Key_Escape) {
modal.mode = "history";
event.accepted = true;
}
return;
}
switch (event.key) { switch (event.key) {
case Qt.Key_Escape: case Qt.Key_Escape:
if (ClipboardService.keyboardNavigationActive) { if (ClipboardService.keyboardNavigationActive) {
@@ -152,6 +169,10 @@ QtObject {
event.accepted = true; event.accepted = true;
} }
return; return;
case Qt.Key_E:
editSelected();
event.accepted = true;
return;
} }
} }
@@ -10,7 +10,7 @@ Rectangle {
readonly property string hintsText: { readonly property string hintsText: {
if (!wtypeAvailable) if (!wtypeAvailable)
return I18n.tr("Ctrl+Tab: Switch Tab • Ctrl+S: Pin/Unpin • Shift+Del: Clear All • Esc: Close"); return I18n.tr("Ctrl+Tab: Switch Tab • Ctrl+S: Pin/Unpin • Shift+Del: Clear All • Esc: Close");
return enterToPaste ? I18n.tr("Ctrl+Tab: Switch Tab • Ctrl+S: Pin/Unpin • Shift+Enter: Copy • Shift+Del: Clear All • Esc: Close", "Keyboard hints when enter-to-paste is enabled") : I18n.tr("Ctrl+Tab: Switch Tab • Ctrl+S: Pin/Unpin • Shift+Enter: Paste • Shift+Del: Clear All • Esc: Close"); return enterToPaste ? I18n.tr("Ctrl+Tab: Switch Tabs • Ctrl+S: Pin/Unpin • Shift+Enter: Copy • Shift+Del: Clear All • F10: Help • Esc: Close", "Keyboard hints when enter-to-paste is enabled") : I18n.tr("Ctrl+Tab: Switch Tabs • Ctrl+S: Pin/Unpin • Shift+Enter: Paste • Shift+Del: Clear All • F10: Help • Esc: Close");
} }
height: ClipboardConstants.keyboardHintsHeight height: ClipboardConstants.keyboardHintsHeight
@@ -22,13 +22,17 @@ Rectangle {
z: 100 z: 100
Column { Column {
width: parent.width - Theme.spacingL * 2
anchors.centerIn: parent anchors.centerIn: parent
spacing: 2 spacing: 2
StyledText { StyledText {
text: keyboardHints.enterToPaste ? I18n.tr("↑/↓: Navigate • Enter: Paste • Del: Delete • F10: Help", "Keyboard hints when enter-to-paste is enabled") : I18n.tr("↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help") text: keyboardHints.enterToPaste ? I18n.tr("↑/↓: Navigate • Enter: Paste • Ctrl+C: Copy • Del: Delete • Ctrl+E: Edit • Ctrl+S: Pin/Unpin • F10: Help", "Keyboard hints when enter-to-paste is enabled") : I18n.tr("↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • Ctrl+E: Edit • Ctrl+S: Pin/Unpin • F10: Help")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText color: Theme.surfaceText
width: parent.width
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
} }
@@ -36,6 +40,9 @@ Rectangle {
text: keyboardHints.hintsText text: keyboardHints.hintsText
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText color: Theme.surfaceText
width: parent.width
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
} }
} }
@@ -38,7 +38,7 @@ Item {
readonly property string resolvedConnectedBarSide: frameConnectedMode ? preferredConnectedBarSide : "" readonly property string resolvedConnectedBarSide: frameConnectedMode ? preferredConnectedBarSide : ""
readonly property bool frameOwnsConnectedChrome: frameConnectedMode && resolvedConnectedBarSide !== "" && !allowStacking readonly property bool frameOwnsConnectedChrome: frameConnectedMode && resolvedConnectedBarSide !== "" && !allowStacking && CompositorService.usesConnectedFrameChromeForScreen(effectiveScreen)
function _dockOccupiesSide(side) { function _dockOccupiesSide(side) {
if (!SettingsData.showDock) if (!SettingsData.showDock)
@@ -58,7 +58,7 @@ Item {
readonly property bool _dockBlocksEmergence: frameOwnsConnectedChrome && _dockOccupiesSide(resolvedConnectedBarSide) readonly property bool _dockBlocksEmergence: frameOwnsConnectedChrome && _dockOccupiesSide(resolvedConnectedBarSide)
readonly property bool connectedMotionParity: Theme.isConnectedEffect readonly property bool connectedMotionParity: frameOwnsConnectedChrome
property int animationDuration: connectedMotionParity ? Theme.popoutAnimationDuration : Theme.modalAnimationDuration property int animationDuration: connectedMotionParity ? Theme.popoutAnimationDuration : Theme.modalAnimationDuration
property real animationScaleCollapsed: Theme.effectScaleCollapsed property real animationScaleCollapsed: Theme.effectScaleCollapsed
property real animationOffset: Theme.effectAnimOffset property real animationOffset: Theme.effectAnimOffset
@@ -68,7 +68,7 @@ Item {
property color borderColor: Theme.outlineMedium property color borderColor: Theme.outlineMedium
property real borderWidth: 0 property real borderWidth: 0
property real cornerRadius: Theme.cornerRadius property real cornerRadius: Theme.cornerRadius
readonly property bool connectedSurfaceOverride: Theme.isConnectedEffect readonly property bool connectedSurfaceOverride: frameOwnsConnectedChrome
readonly property color effectiveBackgroundColor: connectedSurfaceOverride ? Theme.connectedSurfaceColor : backgroundColor readonly property color effectiveBackgroundColor: connectedSurfaceOverride ? Theme.connectedSurfaceColor : backgroundColor
readonly property color effectiveBorderColor: connectedSurfaceOverride ? "transparent" : borderColor readonly property color effectiveBorderColor: connectedSurfaceOverride ? "transparent" : borderColor
readonly property real effectiveBorderWidth: connectedSurfaceOverride ? 0 : borderWidth readonly property real effectiveBorderWidth: connectedSurfaceOverride ? 0 : borderWidth
@@ -346,7 +346,7 @@ Item {
readonly property real shadowFallbackOffset: 6 readonly property real shadowFallbackOffset: 6
readonly property real shadowRenderPadding: (!frameOwnsConnectedChrome && root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0 readonly property real shadowRenderPadding: (!frameOwnsConnectedChrome && root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
readonly property real shadowMotionPadding: { readonly property real shadowMotionPadding: {
if (Theme.isConnectedEffect) if (frameOwnsConnectedChrome)
return 0; return 0;
if (animationType === "slide") if (animationType === "slide")
return 30; return 30;
@@ -10,10 +10,11 @@ Rectangle {
property var entry: null property var entry: null
property string cachedImageData: "" property string cachedImageData: ""
property string cachedMimeType: ""
property var _requestedEntryId: null property var _requestedEntryId: null
readonly property bool canLoadImage: !!entry?.isImage && (entry?.mimeType ?? "").startsWith("image/") readonly property bool canLoadImage: !!entry?.isImage && (entry?.mimeType ?? "").startsWith("image/")
readonly property string sourceUrl: cachedImageData.length > 0 ? "data:" + (entry?.mimeType ?? "image/png") + ";base64," + cachedImageData : "" readonly property string sourceUrl: resolvedSourceUrl(cachedImageData, cachedMimeType || (entry?.mimeType ?? ""))
radius: Math.max(6, Theme.cornerRadius - 2) radius: Math.max(6, Theme.cornerRadius - 2)
clip: true clip: true
@@ -24,8 +25,24 @@ Rectangle {
onEntryChanged: reloadPreview() onEntryChanged: reloadPreview()
Component.onCompleted: reloadPreview() Component.onCompleted: reloadPreview()
function isImageMimeType(mimeType) {
return (mimeType || "").toString().toLowerCase().startsWith("image/");
}
function resolvedSourceUrl(data, mimeType) {
const rawData = (data || "").toString();
if (rawData.length === 0)
return "";
if (rawData.startsWith("data:"))
return rawData.startsWith("data:image/") ? rawData : "";
if (!isImageMimeType(mimeType))
return "";
return "data:" + mimeType + ";base64," + rawData;
}
function reloadPreview() { function reloadPreview() {
cachedImageData = ""; cachedImageData = "";
cachedMimeType = "";
if (!canLoadImage || !entry?.id) { if (!canLoadImage || !entry?.id) {
_requestedEntryId = null; _requestedEntryId = null;
return; return;
@@ -40,9 +57,13 @@ Rectangle {
return; return;
if (response.error) if (response.error)
return; return;
const data = response.result?.data ?? ""; const result = response.result ?? {};
if (data.length > 0) const mimeType = (result.mimeType ?? entry?.mimeType ?? "").toString();
cachedImageData = data; const data = (result.data ?? "").toString();
if (data.length === 0 || !resolvedSourceUrl(data, mimeType))
return;
cachedMimeType = mimeType;
cachedImageData = data;
}); });
} }
+63 -23
View File
@@ -35,25 +35,28 @@ Item {
property int gridColumns: SettingsData.appLauncherGridColumns property int gridColumns: SettingsData.appLauncherGridColumns
property int viewModeVersion: 0 property int viewModeVersion: 0
property string viewModeContext: "spotlight" property string viewModeContext: "spotlight"
property bool forceLinearNavigation: false
signal itemExecuted signal itemExecuted
signal searchCompleted signal searchCompleted
signal modeChanged(string mode) signal modeChanged(string mode, bool userInitiated)
signal queryChanged(string query) signal queryChanged(string query)
signal viewModeChanged(string sectionId, string mode) signal viewModeChanged(string sectionId, string mode)
signal searchQueryRequested(string query) signal searchQueryRequested(string query)
Ref {
service: AppSearchService
}
onActiveChanged: { onActiveChanged: {
if (active) { if (!active) {
if (clipboardSearchEnabledInAll())
ClipboardService.ensureLauncherHistory();
} else {
SessionData.addLauncherHistory(searchQuery); SessionData.addLauncherHistory(searchQuery);
sections = []; sections = [];
flatModel = []; flatModel = [];
selectedItem = null; selectedItem = null;
_clearModeCache(); _clearModeCache();
ClipboardService.invalidateLauncherSearchCache();
} }
} }
@@ -88,11 +91,25 @@ Item {
Connections { Connections {
target: ClipboardService target: ClipboardService
function onInternalEntriesChanged() { function onLauncherSearchReady(query) {
if (!active || !clipboardSearchEnabledInAll()) if (!active)
return; return;
if (searchMode === "all" && searchQuery.length >= 2)
performSearch(); const clipboardBuiltInActive = activePluginId === "dms_clipboard_search";
if (!clipboardBuiltInActive && !clipboardSearchEnabledInAll())
return;
if (!clipboardBuiltInActive && searchMode !== "all")
return;
const trimmed = (searchQuery || "").trim();
if (trimmed.length < 2 && query.length > 0)
return;
const triggerMatch = detectTrigger(trimmed);
const effectiveQuery = clipboardBuiltInActive && triggerMatch.pluginId === "dms_clipboard_search" ? triggerMatch.query : trimmed;
if (query !== effectiveQuery)
return;
searchDebounce.restart();
} }
} }
@@ -403,8 +420,19 @@ Item {
searchQuery = query; searchQuery = query;
searchDebounce.restart(); searchDebounce.restart();
if (searchMode === "all" && clipboardSearchEnabledInAll() && query.length >= 2) if (searchMode !== "plugins" && query.startsWith("/")) {
ClipboardService.ensureLauncherHistory(); var prefix = Utils.parseFileSearchPrefix(query);
var explicitType = prefix && prefix.type !== null ? prefix.type : null;
var targetType = explicitType !== null ? explicitType : (SessionData.launcherLastFileSearchType || "all");
if (searchMode !== "files") {
setMode("files", true, targetType);
} else if (fileSearchType !== targetType) {
fileSearchType = targetType;
}
if (explicitType !== null && SessionData.launcherLastFileSearchType !== explicitType) {
SessionData.setLauncherLastFileSearchType(explicitType);
}
}
var filesInAll = searchMode === "all" && (SettingsData.dankLauncherV2IncludeFilesInAll || SettingsData.dankLauncherV2IncludeFoldersInAll); var filesInAll = searchMode === "all" && (SettingsData.dankLauncherV2IncludeFilesInAll || SettingsData.dankLauncherV2IncludeFoldersInAll);
if (searchMode !== "plugins" && (searchMode === "files" || query.startsWith("/") || filesInAll) && query.length > 0) { if (searchMode !== "plugins" && (searchMode === "files" || query.startsWith("/") || filesInAll) && query.length > 0) {
@@ -412,9 +440,14 @@ Item {
} }
} }
function setMode(mode, isAutoSwitch) { function setMode(mode, isAutoSwitch, fileTypeOverride, notPersist) {
if (searchMode === mode) if (searchMode === mode) {
if (mode === "files" && fileTypeOverride !== undefined && fileSearchType !== fileTypeOverride) {
fileSearchType = fileTypeOverride;
performFileSearch();
}
return; return;
}
if (isAutoSwitch) { if (isAutoSwitch) {
previousSearchMode = searchMode; previousSearchMode = searchMode;
autoSwitchedToFiles = true; autoSwitchedToFiles = true;
@@ -422,10 +455,11 @@ Item {
autoSwitchedToFiles = false; autoSwitchedToFiles = false;
} }
searchMode = mode; searchMode = mode;
modeChanged(mode); if (mode === "files") {
fileSearchType = fileTypeOverride !== undefined ? fileTypeOverride : (SessionData.launcherLastFileSearchType || "all");
}
modeChanged(mode, !isAutoSwitch && notPersist !== true);
performSearch(); performSearch();
if (mode === "all" && clipboardSearchEnabledInAll() && searchQuery.length >= 2)
ClipboardService.ensureLauncherHistory();
var filesInAll = mode === "all" && (SettingsData.dankLauncherV2IncludeFilesInAll || SettingsData.dankLauncherV2IncludeFoldersInAll) && searchQuery.length > 0; var filesInAll = mode === "all" && (SettingsData.dankLauncherV2IncludeFilesInAll || SettingsData.dankLauncherV2IncludeFoldersInAll) && searchQuery.length > 0;
if (mode === "files" || filesInAll) { if (mode === "files" || filesInAll) {
fileSearchDebounce.restart(); fileSearchDebounce.restart();
@@ -437,7 +471,7 @@ Item {
return; return;
autoSwitchedToFiles = false; autoSwitchedToFiles = false;
searchMode = previousSearchMode; searchMode = previousSearchMode;
modeChanged(previousSearchMode); modeChanged(previousSearchMode, false);
performSearch(); performSearch();
} }
@@ -533,6 +567,7 @@ Item {
if (fileSearchType === type) if (fileSearchType === type)
return; return;
fileSearchType = type; fileSearchType = type;
SessionData.setLauncherLastFileSearchType(type);
performFileSearch(); performFileSearch();
} }
@@ -703,7 +738,8 @@ Item {
clearActivePluginViewPreference(); clearActivePluginViewPreference();
if (searchMode === "files") { if (searchMode === "files") {
var fileQuery = searchQuery.startsWith("/") ? searchQuery.substring(1).trim() : searchQuery.trim(); var prefixInfo = Utils.parseFileSearchPrefix(searchQuery);
var fileQuery = prefixInfo ? prefixInfo.query : searchQuery.trim();
isFileSearching = fileQuery.length >= 2 && DSearchService.dsearchAvailable; isFileSearching = fileQuery.length >= 2 && DSearchService.dsearchAvailable;
sections = []; sections = [];
flatModel = []; flatModel = [];
@@ -993,7 +1029,8 @@ Item {
var includeFolders = SettingsData.dankLauncherV2IncludeFoldersInAll; var includeFolders = SettingsData.dankLauncherV2IncludeFoldersInAll;
if (searchQuery.startsWith("/")) { if (searchQuery.startsWith("/")) {
fileQuery = searchQuery.substring(1).trim(); var prefixInfo = Utils.parseFileSearchPrefix(searchQuery);
fileQuery = prefixInfo ? prefixInfo.query : searchQuery.substring(1).trim();
} else if (searchMode === "files") { } else if (searchMode === "files") {
fileQuery = searchQuery.trim(); fileQuery = searchQuery.trim();
} else if (searchMode === "all" && (includeFiles || includeFolders)) { } else if (searchMode === "all" && (includeFiles || includeFolders)) {
@@ -1209,7 +1246,6 @@ Item {
} }
if (clipboardSearchEnabledInAll()) { if (clipboardSearchEnabledInAll()) {
ClipboardService.ensureLauncherHistory();
var clipboardItems = AppSearchService.getBuiltInLauncherItems("dms_clipboard_search", query); var clipboardItems = AppSearchService.getBuiltInLauncherItems("dms_clipboard_search", query);
var clipboardLimit = Math.min(clipboardItems.length, 8); var clipboardLimit = Math.min(clipboardItems.length, 8);
for (var j = 0; j < clipboardLimit; j++) { for (var j = 0; j < clipboardLimit; j++) {
@@ -1713,7 +1749,9 @@ Item {
function selectNext() { function selectNext() {
keyboardNavigationActive = true; keyboardNavigationActive = true;
_cancelPendingSelectionReset(); _cancelPendingSelectionReset();
var newIndex = Nav.calculateNextIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode); var newIndex = forceLinearNavigation ? Nav.findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1) : Nav.calculateNextIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode);
if (newIndex === -1)
newIndex = selectedFlatIndex;
if (newIndex !== selectedFlatIndex) { if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex; selectedFlatIndex = newIndex;
updateSelectedItem(); updateSelectedItem();
@@ -1723,7 +1761,9 @@ Item {
function selectPrevious() { function selectPrevious() {
keyboardNavigationActive = true; keyboardNavigationActive = true;
_cancelPendingSelectionReset(); _cancelPendingSelectionReset();
var newIndex = Nav.calculatePrevIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode); var newIndex = forceLinearNavigation ? Nav.findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1) : Nav.calculatePrevIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode);
if (newIndex === -1)
newIndex = selectedFlatIndex;
if (newIndex !== selectedFlatIndex) { if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex; selectedFlatIndex = newIndex;
updateSelectedItem(); updateSelectedItem();
@@ -1857,7 +1897,7 @@ Item {
if (browseTrigger && browseTrigger.length > 0) { if (browseTrigger && browseTrigger.length > 0) {
searchQueryRequested(browseTrigger); searchQueryRequested(browseTrigger);
} else { } else {
setMode("plugins"); setMode("plugins", false, undefined, true);
pluginFilter = browsePluginId; pluginFilter = browsePluginId;
performSearch(); performSearch();
} }
@@ -159,3 +159,14 @@ function sortPluginsOrdered(plugins, order) {
return aOrder - bOrder; return aOrder - bOrder;
}); });
} }
function parseFileSearchPrefix(query) {
if (!query || !query.startsWith("/"))
return null;
var rest = query.substring(1);
if (rest === "d" || rest.startsWith("d ") || rest.startsWith("d\t"))
return { type: "dir", query: rest.substring(1).trim() };
if (rest === "f" || rest.startsWith("f ") || rest.startsWith("f\t"))
return { type: "file", query: rest.substring(1).trim() };
return { type: null, query: rest.trim() };
}
@@ -23,6 +23,7 @@ Item {
readonly property bool frameOwnsConnectedChrome: impl.item ? (impl.item.frameOwnsConnectedChrome ?? false) : false readonly property bool frameOwnsConnectedChrome: impl.item ? (impl.item.frameOwnsConnectedChrome ?? false) : false
readonly property string resolvedConnectedBarSide: impl.item ? (impl.item.resolvedConnectedBarSide ?? "") : "" readonly property string resolvedConnectedBarSide: impl.item ? (impl.item.resolvedConnectedBarSide ?? "") : ""
readonly property bool launcherArcExtenderActive: impl.item ? (impl.item.launcherArcExtenderActive ?? false) : false readonly property bool launcherArcExtenderActive: impl.item ? (impl.item.launcherArcExtenderActive ?? false) : false
property bool triggerUsesOverlayLayer: false
signal dialogClosed signal dialogClosed
@@ -61,7 +62,7 @@ Item {
impl.item.toggleWithMode(mode); impl.item.toggleWithMode(mode);
} }
readonly property bool useSpotlightBackend: SettingsData.connectedFrameModeActive ? SettingsData.frameUseSpotlightLauncher : SettingsData.launcherStyle === "spotlight" readonly property bool useSpotlightBackend: !SettingsData.connectedFrameModeActive && SettingsData.launcherStyle === "spotlight"
readonly property var _desiredBackend: useSpotlightBackend ? spotlightComp : (SettingsData.connectedFrameModeActive ? connectedComp : standaloneComp) readonly property var _desiredBackend: useSpotlightBackend ? spotlightComp : (SettingsData.connectedFrameModeActive ? connectedComp : standaloneComp)
property var _resolvedBackend: null property var _resolvedBackend: null
@@ -72,9 +73,6 @@ Item {
function onConnectedFrameModeActiveChanged() { function onConnectedFrameModeActiveChanged() {
root._maybeResolveBackend(); root._maybeResolveBackend();
} }
function onFrameUseSpotlightLauncherChanged() {
root._maybeResolveBackend();
}
function onLauncherStyleChanged() { function onLauncherStyleChanged() {
root._maybeResolveBackend(); root._maybeResolveBackend();
} }
@@ -116,6 +114,7 @@ Item {
if (!it) if (!it)
return; return;
it.modalHandle = root; it.modalHandle = root;
it.triggerUsesOverlayLayer = Qt.binding(() => root.triggerUsesOverlayLayer);
} }
Connections { Connections {
@@ -13,13 +13,14 @@ Item {
readonly property var log: Log.scoped("DankLauncherV2ModalConnected") readonly property var log: Log.scoped("DankLauncherV2ModalConnected")
property var modalHandle: root property var modalHandle: root
property bool triggerUsesOverlayLayer: false
visible: false visible: false
property bool spotlightOpen: false property bool spotlightOpen: false
property bool keyboardActive: false property bool keyboardActive: false
property bool contentVisible: false property bool contentVisible: false
readonly property bool launcherMotionVisible: Theme.isConnectedEffect ? _motionActive : (Theme.isDirectionalEffect ? spotlightOpen : _motionActive) readonly property bool launcherMotionVisible: frameOwnsConnectedChrome ? _motionActive : (Theme.isDirectionalEffect ? spotlightOpen : _motionActive)
property var spotlightContent: launcherContentLoader.item property var spotlightContent: launcherContentLoader.item
property bool openedFromOverview: false property bool openedFromOverview: false
property bool isClosing: false property bool isClosing: false
@@ -40,6 +41,21 @@ Item {
readonly property real screenWidth: effectiveScreen?.width ?? 1920 readonly property real screenWidth: effectiveScreen?.width ?? 1920
readonly property real screenHeight: effectiveScreen?.height ?? 1080 readonly property real screenHeight: effectiveScreen?.height ?? 1080
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1 readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
readonly property bool usesOverlayLayer: SettingsData.launcherUseOverlayLayer || triggerUsesOverlayLayer
readonly property var effectiveLauncherLayer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
log.error("'background' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "overlay":
return WlrLayershell.Overlay;
default:
return root.usesOverlayLayer ? WlrLayershell.Overlay : WlrLayershell.Top;
}
}
readonly property int baseWidth: { readonly property int baseWidth: {
switch (SettingsData.dankLauncherV2Size) { switch (SettingsData.dankLauncherV2Size) {
@@ -74,7 +90,7 @@ Item {
readonly property string resolvedConnectedBarSide: frameConnectedMode ? preferredConnectedBarSide : "" readonly property string resolvedConnectedBarSide: frameConnectedMode ? preferredConnectedBarSide : ""
readonly property bool frameOwnsConnectedChrome: frameConnectedMode && resolvedConnectedBarSide !== "" readonly property bool frameOwnsConnectedChrome: frameConnectedMode && resolvedConnectedBarSide !== "" && CompositorService.usesConnectedFrameChromeForScreen(effectiveScreen)
readonly property bool launcherArcExtenderActive: frameOwnsConnectedChrome && SettingsData.frameLauncherArcExtender && (resolvedConnectedBarSide === "top" || resolvedConnectedBarSide === "bottom") readonly property bool launcherArcExtenderActive: frameOwnsConnectedChrome && SettingsData.frameLauncherArcExtender && (resolvedConnectedBarSide === "top" || resolvedConnectedBarSide === "bottom")
function _dockOccupiesSide(side) { function _dockOccupiesSide(side) {
@@ -140,10 +156,10 @@ Item {
readonly property real modalX: frameOwnsConnectedChrome ? _connectedModalPos.x : ((screenWidth - modalWidth) / 2) readonly property real modalX: frameOwnsConnectedChrome ? _connectedModalPos.x : ((screenWidth - modalWidth) / 2)
readonly property real modalY: frameOwnsConnectedChrome ? _connectedModalPos.y : ((screenHeight - modalHeight) / 2) readonly property real modalY: frameOwnsConnectedChrome ? _connectedModalPos.y : ((screenHeight - modalHeight) / 2)
readonly property bool connectedSurfaceOverride: Theme.isConnectedEffect readonly property bool connectedSurfaceOverride: frameOwnsConnectedChrome
readonly property int launcherAnimationDuration: Theme.isConnectedEffect ? Theme.popoutAnimationDuration : Theme.modalAnimationDuration readonly property int launcherAnimationDuration: frameOwnsConnectedChrome ? Theme.popoutAnimationDuration : Theme.modalAnimationDuration
readonly property list<real> launcherEnterCurve: Theme.isConnectedEffect ? Theme.variantPopoutEnterCurve : Theme.variantModalEnterCurve readonly property list<real> launcherEnterCurve: frameOwnsConnectedChrome ? Theme.variantPopoutEnterCurve : Theme.variantModalEnterCurve
readonly property list<real> launcherExitCurve: Theme.isConnectedEffect ? Theme.variantPopoutExitCurve : Theme.variantModalExitCurve readonly property list<real> launcherExitCurve: frameOwnsConnectedChrome ? Theme.variantPopoutExitCurve : Theme.variantModalExitCurve
readonly property color backgroundColor: connectedSurfaceOverride ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) readonly property color backgroundColor: connectedSurfaceOverride ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
readonly property real cornerRadius: connectedSurfaceOverride ? Theme.connectedSurfaceRadius : Theme.cornerRadius readonly property real cornerRadius: connectedSurfaceOverride ? Theme.connectedSurfaceRadius : Theme.cornerRadius
readonly property color borderColor: { readonly property color borderColor: {
@@ -372,6 +388,7 @@ Item {
if (!spotlightContent) if (!spotlightContent)
return; return;
contentVisible = true; contentVisible = true;
spotlightContent.closeTransientUi?.();
// NOTE: forceActiveFocus() is deliberately NOT called here. // NOTE: forceActiveFocus() is deliberately NOT called here.
// It is deferred to after animation starts to avoid compositor IPC stalls. // It is deferred to after animation starts to avoid compositor IPC stalls.
@@ -379,12 +396,12 @@ Item {
spotlightContent.searchField.text = query; spotlightContent.searchField.text = query;
} }
if (spotlightContent.controller) { if (spotlightContent.controller) {
var targetMode = mode || SessionData.launcherLastMode || "all"; var targetMode = mode || SessionData.getLauncherRestoreMode();
spotlightContent.controller.searchMode = targetMode; spotlightContent.controller.searchMode = targetMode;
spotlightContent.controller.activePluginId = ""; spotlightContent.controller.activePluginId = "";
spotlightContent.controller.activePluginName = ""; spotlightContent.controller.activePluginName = "";
spotlightContent.controller.pluginFilter = ""; spotlightContent.controller.pluginFilter = "";
spotlightContent.controller.fileSearchType = "all"; spotlightContent.controller.fileSearchType = SessionData.launcherLastFileSearchType || "all";
spotlightContent.controller.fileSearchExt = ""; spotlightContent.controller.fileSearchExt = "";
spotlightContent.controller.fileSearchFolder = ""; spotlightContent.controller.fileSearchFolder = "";
spotlightContent.controller.fileSearchSort = "score"; spotlightContent.controller.fileSearchSort = "score";
@@ -464,6 +481,7 @@ Item {
function hide() { function hide() {
if (!spotlightOpen) if (!spotlightOpen)
return; return;
spotlightContent?.closeTransientUi?.();
openedFromOverview = false; openedFromOverview = false;
isClosing = true; isClosing = true;
// For directional effects, defer contentVisible=false so content stays rendered during exit slide // For directional effects, defer contentVisible=false so content stays rendered during exit slide
@@ -521,8 +539,8 @@ Item {
Connections { Connections {
target: spotlightContent?.controller ?? null target: spotlightContent?.controller ?? null
function onModeChanged(mode) { function onModeChanged(mode, userInitiated) {
if (spotlightContent.controller.autoSwitchedToFiles) if (!userInitiated || !SettingsData.rememberLastMode)
return; return;
SessionData.setLauncherLastMode(mode); SessionData.setLauncherLastMode(mode);
} }
@@ -596,7 +614,7 @@ Item {
readonly property real _rightMargin: contentContainer.dockRight ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 3 ? Theme.px(42, root.dpr) : 0) readonly property real _rightMargin: contentContainer.dockRight ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 3 ? Theme.px(42, root.dpr) : 0)
WlrLayershell.namespace: "dms:spotlight:bg" WlrLayershell.namespace: "dms:spotlight:bg"
WlrLayershell.layer: WlrLayershell.Top WlrLayershell.layer: root.effectiveLauncherLayer
WlrLayershell.exclusiveZone: -1 WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
@@ -669,20 +687,7 @@ Item {
} }
WlrLayershell.namespace: "dms:spotlight" WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: { WlrLayershell.layer: root.effectiveLauncherLayer
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
log.error("'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.exclusiveZone: -1
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
@@ -923,8 +928,12 @@ Item {
} }
} }
Keys.onPressed: event => root.spotlightContent?.activeContextMenu?.handleKey(event)
Keys.onEscapePressed: event => { Keys.onEscapePressed: event => {
root.hide(); root.spotlightContent?.activeContextMenu?.handleKey(event);
if (!event.accepted)
root.hide();
event.accepted = true; event.accepted = true;
} }
} }
@@ -11,6 +11,7 @@ Item {
readonly property var log: Log.scoped("DankLauncherV2ModalSpotlight") readonly property var log: Log.scoped("DankLauncherV2ModalSpotlight")
property var modalHandle: root property var modalHandle: root
property bool triggerUsesOverlayLayer: false
visible: false visible: false
@@ -29,13 +30,29 @@ Item {
readonly property real screenWidth: effectiveScreen?.width ?? 1920 readonly property real screenWidth: effectiveScreen?.width ?? 1920
readonly property real screenHeight: effectiveScreen?.height ?? 1080 readonly property real screenHeight: effectiveScreen?.height ?? 1080
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1 readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
readonly property bool useBackgroundDarken: !SettingsData.frameEnabled && SettingsData.modalDarkenBackground
readonly property bool usesOverlayLayer: useBackgroundDarken || SettingsData.launcherUseOverlayLayer || triggerUsesOverlayLayer
readonly property var effectiveLauncherLayer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
log.error("'background' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "overlay":
return WlrLayershell.Overlay;
default:
return root.usesOverlayLayer ? WlrLayershell.Overlay : WlrLayershell.Top;
}
}
readonly property int _openDuration: 80 readonly property int _openDuration: 50
readonly property int _closeDuration: 70 readonly property int _closeDuration: 40
readonly property int _motionDuration: 90 readonly property int _motionDuration: 60
// Connected frame mode clamps the centered surface inside frame insets. // Connected frame mode clamps the centered surface inside frame insets.
readonly property bool frameConnected: SettingsData.connectedFrameModeActive && !!effectiveScreen && SettingsData.isScreenInPreferences(effectiveScreen, SettingsData.frameScreenPreferences) readonly property bool frameConnected: CompositorService.usesConnectedFrameChromeForScreen(effectiveScreen)
function _frameEdgeInset(side) { function _frameEdgeInset(side) {
if (!effectiveScreen || !frameConnected) if (!effectiveScreen || !frameConnected)
@@ -58,7 +75,7 @@ Item {
const searchBarH = 56; const searchBarH = 56;
const usableH = Math.max(searchBarH, screenHeight - insetT - insetB); const usableH = Math.max(searchBarH, screenHeight - insetT - insetB);
const preferred = insetT + Math.max(0, usableH * 0.33 - searchBarH / 2); const preferred = insetT + Math.max(0, usableH * 0.33 - searchBarH / 2);
const maxY = Math.max(insetT, screenHeight - insetB - _contentImplicitH); const maxY = Math.max(insetT, screenHeight - insetB - 56);
return Math.max(insetT, Math.min(preferred, maxY)); return Math.max(insetT, Math.min(preferred, maxY));
} }
@@ -125,9 +142,10 @@ Item {
if (!spotlightContent) if (!spotlightContent)
return; return;
contentVisible = true; contentVisible = true;
spotlightContent.closeTransientUi?.();
const targetQuery = query || (SettingsData.rememberLastQuery ? (SessionData.launcherLastQuery || "") : ""); const targetQuery = query || (SettingsData.rememberLastQuery ? (SessionData.launcherLastQuery || "") : "");
const targetMode = mode || SessionData.launcherLastMode || "all"; const targetMode = mode || SessionData.getLauncherRestoreMode();
if (spotlightContent.searchField) { if (spotlightContent.searchField) {
spotlightContent.searchField.text = targetQuery; spotlightContent.searchField.text = targetQuery;
@@ -185,6 +203,7 @@ Item {
function hide() { function hide() {
if (!spotlightOpen) if (!spotlightOpen)
return; return;
spotlightContent?.closeTransientUi?.();
openedFromOverview = false; openedFromOverview = false;
isClosing = true; isClosing = true;
contentVisible = false; contentVisible = false;
@@ -259,11 +278,11 @@ Item {
PanelWindow { PanelWindow {
id: clickCatcher id: clickCatcher
screen: launcherWindow.screen screen: launcherWindow.screen
visible: spotlightOpen || isClosing visible: (spotlightOpen || isClosing) && !root.useBackgroundDarken
color: "transparent" color: "transparent"
WlrLayershell.namespace: "dms:spotlight:clickcatcher" WlrLayershell.namespace: "dms:spotlight:clickcatcher"
WlrLayershell.layer: WlrLayershell.Top WlrLayershell.layer: root.effectiveLauncherLayer
WlrLayershell.exclusiveZone: -1 WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
@@ -324,31 +343,26 @@ Item {
} }
WlrLayershell.namespace: "dms:spotlight" WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: { WlrLayershell.layer: root.effectiveLauncherLayer
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "overlay":
return WlrLayershell.Overlay;
default:
return WlrLayershell.Top;
}
}
WlrLayershell.exclusiveZone: -1 WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
anchors { anchors {
top: true top: true
left: true left: true
right: root.useBackgroundDarken
bottom: root.useBackgroundDarken
} }
WlrLayershell.margins { WlrLayershell.margins {
left: root.windowX left: root.useBackgroundDarken ? 0 : root.windowX
top: root.windowY top: root.useBackgroundDarken ? 0 : root.windowY
right: 0 right: 0
bottom: 0 bottom: 0
} }
implicitWidth: root.windowWidth implicitWidth: root.useBackgroundDarken ? 0 : root.windowWidth
implicitHeight: root.windowHeight implicitHeight: root.useBackgroundDarken ? 0 : root.windowHeight
mask: Region { mask: Region {
item: inputMask item: inputMask
@@ -358,19 +372,44 @@ Item {
id: inputMask id: inputMask
visible: false visible: false
color: "transparent" color: "transparent"
x: modalContainer.x x: root.useBackgroundDarken ? 0 : modalContainer.x
y: modalContainer.y + modalContainer.slideOffset y: root.useBackgroundDarken ? 0 : modalContainer.y + modalContainer.slideOffset
width: root.alignedWidth width: root.useBackgroundDarken ? launcherWindow.width : root.alignedWidth
height: root._contentImplicitH height: root.useBackgroundDarken ? launcherWindow.height : root._contentImplicitH
}
MouseArea {
anchors.fill: parent
enabled: root.useBackgroundDarken && spotlightOpen
z: -2
onClicked: root.hide()
}
Rectangle {
id: backgroundDarken
anchors.fill: parent
color: "black"
opacity: contentVisible && root.useBackgroundDarken ? 0.5 : 0
visible: (spotlightOpen || isClosing) && (root.useBackgroundDarken || opacity > 0)
z: -3
Behavior on opacity {
NumberAnimation {
duration: contentVisible ? root._openDuration : root._closeDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: contentVisible ? [0.0, 0.0, 0.2, 1.0, 1.0, 1.0] : [0.4, 0.0, 1.0, 1.0, 1.0, 1.0]
}
}
} }
Item { Item {
id: modalContainer id: modalContainer
x: root.contentX x: root.useBackgroundDarken ? root.alignedX : root.contentX
y: root.contentY y: root.useBackgroundDarken ? root.alignedY : root.contentY
width: root.alignedWidth width: root.alignedWidth
height: root._animatedContentH height: root._animatedContentH
visible: _renderActive visible: _renderActive
z: 0
property bool _renderActive: contentVisible property bool _renderActive: contentVisible
property real slideOffset: contentVisible ? 0 : -root._animHeadroom property real slideOffset: contentVisible ? 0 : -root._animHeadroom
@@ -450,8 +489,12 @@ Item {
} }
} }
Keys.onPressed: event => root.spotlightContent?.activeContextMenu?.handleKey(event)
Keys.onEscapePressed: event => { Keys.onEscapePressed: event => {
root.hide(); root.spotlightContent?.activeContextMenu?.handleKey(event);
if (!event.accepted)
root.hide();
event.accepted = true; event.accepted = true;
} }
} }
@@ -11,6 +11,7 @@ Item {
readonly property var log: Log.scoped("DankLauncherV2ModalStandalone") readonly property var log: Log.scoped("DankLauncherV2ModalStandalone")
property var modalHandle: root property var modalHandle: root
property bool triggerUsesOverlayLayer: false
visible: false visible: false
@@ -31,7 +32,7 @@ Item {
readonly property real screenHeight: effectiveScreen?.height ?? 1080 readonly property real screenHeight: effectiveScreen?.height ?? 1080
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1 readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
readonly property bool frameOwnsConnectedChrome: SettingsData.connectedFrameModeActive && !!effectiveScreen && SettingsData.isScreenInPreferences(effectiveScreen, SettingsData.frameScreenPreferences) readonly property bool frameOwnsConnectedChrome: CompositorService.usesConnectedFrameChromeForScreen(effectiveScreen)
readonly property string resolvedConnectedBarSide: frameOwnsConnectedChrome ? (SettingsData.frameLauncherEmergeSide || "bottom") : "" readonly property string resolvedConnectedBarSide: frameOwnsConnectedChrome ? (SettingsData.frameLauncherEmergeSide || "bottom") : ""
readonly property int baseWidth: { readonly property int baseWidth: {
@@ -79,6 +80,21 @@ Item {
readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
readonly property bool useBackgroundDarken: !SettingsData.frameEnabled && SettingsData.modalDarkenBackground readonly property bool useBackgroundDarken: !SettingsData.frameEnabled && SettingsData.modalDarkenBackground
readonly property bool usesOverlayLayer: useBackgroundDarken || SettingsData.launcherUseOverlayLayer || triggerUsesOverlayLayer
readonly property var effectiveLauncherLayer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
log.error("'background' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "overlay":
return WlrLayershell.Overlay;
default:
return root.usesOverlayLayer ? WlrLayershell.Overlay : WlrLayershell.Top;
}
}
readonly property real cornerRadius: Theme.cornerRadius readonly property real cornerRadius: Theme.cornerRadius
readonly property color borderColor: { readonly property color borderColor: {
if (!SettingsData.dankLauncherV2BorderEnabled) if (!SettingsData.dankLauncherV2BorderEnabled)
@@ -117,6 +133,7 @@ Item {
if (!spotlightContent) if (!spotlightContent)
return; return;
contentVisible = true; contentVisible = true;
spotlightContent.closeTransientUi?.();
spotlightContent.searchField.forceActiveFocus(); spotlightContent.searchField.forceActiveFocus();
var targetQuery = ""; var targetQuery = "";
@@ -131,12 +148,12 @@ Item {
spotlightContent.searchField.text = targetQuery; spotlightContent.searchField.text = targetQuery;
} }
if (spotlightContent.controller) { if (spotlightContent.controller) {
var targetMode = mode || SessionData.launcherLastMode || "all"; var targetMode = mode || SessionData.getLauncherRestoreMode();
spotlightContent.controller.searchMode = targetMode; spotlightContent.controller.searchMode = targetMode;
spotlightContent.controller.activePluginId = ""; spotlightContent.controller.activePluginId = "";
spotlightContent.controller.activePluginName = ""; spotlightContent.controller.activePluginName = "";
spotlightContent.controller.pluginFilter = ""; spotlightContent.controller.pluginFilter = "";
spotlightContent.controller.fileSearchType = "all"; spotlightContent.controller.fileSearchType = SessionData.launcherLastFileSearchType || "all";
spotlightContent.controller.fileSearchExt = ""; spotlightContent.controller.fileSearchExt = "";
spotlightContent.controller.fileSearchFolder = ""; spotlightContent.controller.fileSearchFolder = "";
spotlightContent.controller.fileSearchSort = "score"; spotlightContent.controller.fileSearchSort = "score";
@@ -195,6 +212,7 @@ Item {
function hide() { function hide() {
if (!spotlightOpen) if (!spotlightOpen)
return; return;
spotlightContent?.closeTransientUi?.();
openedFromOverview = false; openedFromOverview = false;
isClosing = true; isClosing = true;
contentVisible = false; contentVisible = false;
@@ -242,8 +260,8 @@ Item {
Connections { Connections {
target: spotlightContent?.controller ?? null target: spotlightContent?.controller ?? null
function onModeChanged(mode) { function onModeChanged(mode, userInitiated) {
if (spotlightContent.controller.autoSwitchedToFiles) if (!userInitiated || !SettingsData.rememberLastMode || (mode !== "all" && mode !== "apps"))
return; return;
SessionData.setLauncherLastMode(mode); SessionData.setLauncherLastMode(mode);
} }
@@ -296,12 +314,11 @@ Item {
PanelWindow { PanelWindow {
id: clickCatcher id: clickCatcher
screen: launcherWindow.screen screen: launcherWindow.screen
visible: spotlightOpen || isClosing visible: (spotlightOpen || isClosing) && !root.useBackgroundDarken
color: "transparent" color: "transparent"
updatesEnabled: root.useBackgroundDarken && (spotlightOpen || isClosing)
WlrLayershell.namespace: "dms:spotlight:clickcatcher" WlrLayershell.namespace: "dms:spotlight:clickcatcher"
WlrLayershell.layer: WlrLayershell.Top WlrLayershell.layer: root.effectiveLauncherLayer
WlrLayershell.exclusiveZone: -1 WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
@@ -342,22 +359,6 @@ Item {
enabled: spotlightOpen enabled: spotlightOpen
onClicked: root.hide() onClicked: root.hide()
} }
Rectangle {
id: backgroundDarken
anchors.fill: parent
color: "black"
opacity: contentVisible && root.useBackgroundDarken ? 0.5 : 0
visible: (spotlightOpen || isClosing) && (root.useBackgroundDarken || opacity > 0)
Behavior on opacity {
NumberAnimation {
easing.type: Easing.BezierSpline
duration: Theme.modalAnimationDuration
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
}
} }
PanelWindow { PanelWindow {
@@ -369,7 +370,7 @@ Item {
WindowBlur { WindowBlur {
targetWindow: launcherWindow targetWindow: launcherWindow
readonly property real s: Math.min(1, modalContainer.publishedScale) readonly property real s: Math.min(1, modalContainer.publishedScale)
readonly property real op: Math.max(0, Math.min(1, (modalContainer.opacity - 0.06) * 2)) readonly property real op: Math.max(0, Math.min(1, (modalContainer.publishedOpacity - 0.06) * 2))
blurX: modalContainer.x + modalContainer.width * (1 - s * op) * 0.5 blurX: modalContainer.x + modalContainer.width * (1 - s * op) * 0.5
blurY: modalContainer.y + modalContainer.height * (1 - s * op) * 0.5 blurY: modalContainer.y + modalContainer.height * (1 - s * op) * 0.5
blurWidth: contentVisible ? modalContainer.width * s * op : 0 blurWidth: contentVisible ? modalContainer.width * s * op : 0
@@ -378,39 +379,26 @@ Item {
} }
WlrLayershell.namespace: "dms:spotlight" WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: { WlrLayershell.layer: root.effectiveLauncherLayer
if (root.useBackgroundDarken)
return WlrLayershell.Overlay;
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
log.error("'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
log.error("'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.exclusiveZone: -1
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
anchors { anchors {
top: true top: true
left: true left: true
right: root.useBackgroundDarken
bottom: root.useBackgroundDarken
} }
WlrLayershell.margins { WlrLayershell.margins {
left: root.windowX left: root.useBackgroundDarken ? 0 : root.windowX
top: root.windowY top: root.useBackgroundDarken ? 0 : root.windowY
right: 0 right: 0
bottom: 0 bottom: 0
} }
implicitWidth: root.windowWidth implicitWidth: root.useBackgroundDarken ? 0 : root.windowWidth
implicitHeight: root.windowHeight implicitHeight: root.useBackgroundDarken ? 0 : root.windowHeight
mask: Region { mask: Region {
item: launcherInputMask item: launcherInputMask
@@ -420,22 +408,48 @@ Item {
id: launcherInputMask id: launcherInputMask
visible: false visible: false
color: "transparent" color: "transparent"
x: modalContainer.x x: root.useBackgroundDarken ? 0 : modalContainer.x
y: modalContainer.y y: root.useBackgroundDarken ? 0 : modalContainer.y
width: modalContainer.width width: root.useBackgroundDarken ? launcherWindow.width : modalContainer.width
height: modalContainer.height height: root.useBackgroundDarken ? launcherWindow.height : modalContainer.height
}
MouseArea {
anchors.fill: parent
enabled: root.useBackgroundDarken && spotlightOpen
z: -2
onClicked: root.hide()
}
Rectangle {
id: backgroundDarken
anchors.fill: parent
color: "black"
opacity: contentVisible && root.useBackgroundDarken ? 0.5 : 0
visible: (spotlightOpen || isClosing) && (root.useBackgroundDarken || opacity > 0)
z: -3
Behavior on opacity {
NumberAnimation {
easing.type: Easing.BezierSpline
duration: Theme.modalAnimationDuration
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
} }
Item { Item {
id: modalContainer id: modalContainer
x: root.contentX x: root.useBackgroundDarken ? root.alignedX : root.contentX
y: root.contentY y: root.useBackgroundDarken ? root.alignedY : root.contentY
width: root.alignedWidth width: root.alignedWidth
height: root.alignedHeight height: root.alignedHeight
visible: _renderActive visible: _renderActive
z: 0
property bool _renderActive: contentVisible property bool _renderActive: contentVisible
property real publishedScale: contentVisible ? 1 : 0.96 property real publishedScale: contentVisible ? 1 : 0.96
property real publishedOpacity: contentVisible ? 1 : 0
opacity: contentVisible ? 1 : 0 opacity: contentVisible ? 1 : 0
scale: contentVisible ? 1 : 0.96 scale: contentVisible ? 1 : 0.96
@@ -467,6 +481,14 @@ Item {
} }
} }
Behavior on publishedOpacity {
NumberAnimation {
easing.type: Easing.BezierSpline
duration: Theme.modalAnimationDuration
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
Connections { Connections {
target: root target: root
function onContentVisibleChanged() { function onContentVisibleChanged() {
@@ -514,8 +536,12 @@ Item {
} }
} }
Keys.onPressed: event => root.spotlightContent?.activeContextMenu?.handleKey(event)
Keys.onEscapePressed: event => { Keys.onEscapePressed: event => {
root.hide(); root.spotlightContent?.activeContextMenu?.handleKey(event);
if (!event.accepted)
root.hide();
event.accepted = true; event.accepted = true;
} }
} }
@@ -17,10 +17,22 @@ FocusScope {
property alias controller: controller property alias controller: controller
property alias resultsList: resultsList property alias resultsList: resultsList
property alias actionPanel: actionPanel property alias actionPanel: actionPanel
readonly property alias activeContextMenu: contextMenu
property bool editMode: false property bool editMode: false
property var editingApp: null property var editingApp: null
property string editAppId: "" property string editAppId: ""
readonly property bool _blurActive: Theme.blurForegroundLayers || Theme.transparentBlurLayers
readonly property real _launcherFieldAlpha: {
if (Theme.transparentBlurLayers)
return 0.28;
if (Theme.blurForegroundLayers)
return Math.max(Theme.popupTransparency, 0.62);
return Theme.popupTransparency;
}
readonly property color _launcherSearchFieldColor: Theme.withAlpha(Theme.surfaceContainerHigh, _launcherFieldAlpha)
readonly property color _launcherSearchBorderColor: Theme.withAlpha(Theme.outline, _blurActive ? 0.16 : Theme.layerOutlineOpacity)
readonly property color _launcherSearchFocusedBorderColor: Theme.withAlpha(Theme.primary, _blurActive ? 0.72 : 1.0)
function resetScroll() { function resetScroll() {
resultsList.resetScroll(); resultsList.resetScroll();
@@ -30,6 +42,12 @@ FocusScope {
searchField.forceActiveFocus(); searchField.forceActiveFocus();
} }
function closeTransientUi() {
contextMenu.hide();
actionPanel.hide();
root.enabled = true;
}
function openEditMode(app) { function openEditMode(app) {
if (!app) if (!app)
return; return;
@@ -111,6 +129,21 @@ FocusScope {
} }
} }
Connections {
target: root.parentModal
ignoreUnknownSignals: true
function onSpotlightOpenChanged() {
if (!root.parentModal?.spotlightOpen)
root.closeTransientUi();
}
function onContentVisibleChanged() {
if (!root.parentModal?.contentVisible)
root.closeTransientUi();
}
}
Keys.onPressed: event => { Keys.onPressed: event => {
if (editMode) { if (editMode) {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
@@ -257,13 +290,6 @@ FocusScope {
} }
event.accepted = false; event.accepted = false;
return; return;
case Qt.Key_Slash:
if (event.modifiers === Qt.NoModifier && searchField.text.length === 0) {
controller.setMode("files", true);
return;
}
event.accepted = false;
return;
default: default:
event.accepted = false; event.accepted = false;
} }
@@ -284,7 +310,7 @@ FocusScope {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.leftMargin: root.parentModal?.borderWidth ?? 1 anchors.leftMargin: root.parentModal?.borderWidth ?? 1
anchors.rightMargin: root.parentModal?.borderWidth ?? 1 anchors.rightMargin: root.parentModal?.borderWidth ?? 1
anchors.bottomMargin: _connectedBottomEmerge ? Theme.spacingS : (root.parentModal?.borderWidth ?? 1) anchors.bottomMargin: _connectedBottomEmerge ? 0 : (root.parentModal?.borderWidth ?? 1)
height: showFooter ? (_connectedArcAtFooter ? 76 : 36) : 0 height: showFooter ? (_connectedArcAtFooter ? 76 : 36) : 0
visible: showFooter visible: showFooter
clip: true clip: true
@@ -293,7 +319,7 @@ FocusScope {
anchors.fill: parent anchors.fill: parent
anchors.topMargin: -Theme.cornerRadius anchors.topMargin: -Theme.cornerRadius
// In connected mode the launcher provides the surface so update the toolbar for arcs // In connected mode the launcher provides the surface so update the toolbar for arcs
visible: !(root.parentModal?.frameOwnsConnectedChrome ?? false) visible: !(root.parentModal?.frameOwnsConnectedChrome ?? false) && !root._blurActive
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
} }
@@ -458,9 +484,11 @@ FocusScope {
id: searchField id: searchField
width: parent.width - (pluginBadge.visible ? pluginBadge.width + Theme.spacingS : 0) width: parent.width - (pluginBadge.visible ? pluginBadge.width + Theme.spacingS : 0)
cornerRadius: Theme.cornerRadius cornerRadius: Theme.cornerRadius
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) backgroundColor: root._launcherSearchFieldColor
normalBorderColor: Theme.outlineMedium normalBorderColor: root._launcherSearchBorderColor
focusedBorderColor: Theme.primary focusedBorderColor: root._launcherSearchFocusedBorderColor
borderWidth: 1
focusedBorderWidth: 2
leftIconName: controller.activePluginId ? "extension" : controller.searchQuery.startsWith("/") ? "folder" : "search" leftIconName: controller.activePluginId ? "extension" : controller.searchQuery.startsWith("/") ? "folder" : "search"
leftIconSize: Theme.iconSize leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText leftIconColor: Theme.surfaceVariantText
@@ -1,35 +1,72 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Controls import Quickshell
import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
Popup { Item {
id: root id: root
visible: false
width: 0
height: 0
property var item: null property var item: null
property var controller: null property var controller: null
property var searchField: null property var searchField: null
property var parentHandler: null property var parentHandler: null
property bool allowEditActions: true property bool allowEditActions: true
property real menuMargin: 8
property var targetScreen: null
property real anchorX: 0
property real anchorY: 0
property bool openState: false
property bool renderActive: false
readonly property bool blurActive: renderActive && openState && BlurService.enabled && Theme.connectedSurfaceBlurEnabled
readonly property real minMenuWidth: 180
readonly property real maxMenuWidth: Math.max(0, (targetScreen?.width ?? 500) - menuMargin * 2)
readonly property real maxMenuHeight: Math.max(0, (targetScreen?.height ?? 600) - menuMargin * 2)
readonly property string longestMenuText: {
let longest = "";
for (let i = 0; i < menuItems.length; i++) {
const text = menuItems[i].text || "";
if (text.length > longest.length)
longest = text;
}
return longest;
}
readonly property real naturalMenuWidth: Math.max(minMenuWidth, menuTextMetrics.width + Theme.iconSize + Theme.spacingS * 5)
readonly property real effectiveMenuWidth: Math.max(0, Math.min(maxMenuWidth, naturalMenuWidth))
readonly property real naturalMenuHeight: menuItemsHeight() + Theme.spacingS * 2
readonly property real effectiveMenuHeight: Math.min(maxMenuHeight, naturalMenuHeight)
readonly property bool menuScrolls: naturalMenuHeight > effectiveMenuHeight + 0.5
signal hideRequested signal hideRequested
signal editAppRequested(var app) signal editAppRequested(var app)
TextMetrics {
id: menuTextMetrics
text: root.longestMenuText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Normal
}
function hasContextMenuActions(spotlightItem) { function hasContextMenuActions(spotlightItem) {
if (!spotlightItem) if (!spotlightItem)
return false; return false;
if (spotlightItem.type === "app") if (spotlightItem.type === "app")
return true; return true;
if (spotlightItem.type === "plugin" && spotlightItem.pluginId) { if (spotlightItem.type === "plugin" && spotlightItem.pluginId) {
var instance = PluginService.pluginInstances[spotlightItem.pluginId]; const instance = PluginService.pluginInstances[spotlightItem.pluginId];
if (!instance) if (!instance)
return false; return false;
if (typeof instance.getContextMenuActions !== "function") if (typeof instance.getContextMenuActions !== "function")
return false; return false;
var actions = instance.getContextMenuActions(spotlightItem.data); const actions = instance.getContextMenuActions(spotlightItem.data);
return Array.isArray(actions) && actions.length > 0; return Array.isArray(actions) && actions.length > 0;
} }
if (spotlightItem.actions && spotlightItem.actions.length > 0) if (spotlightItem.actions && spotlightItem.actions.length > 0)
@@ -54,13 +91,13 @@ Popup {
if (!isPluginItem || !item?.pluginId) if (!isPluginItem || !item?.pluginId)
return []; return [];
var instance = PluginService.pluginInstances[item.pluginId]; const instance = PluginService.pluginInstances[item.pluginId];
if (!instance) if (!instance)
return []; return [];
if (typeof instance.getContextMenuActions !== "function") if (typeof instance.getContextMenuActions !== "function")
return []; return [];
var actions = instance.getContextMenuActions(item.data); const actions = instance.getContextMenuActions(item.data);
if (!Array.isArray(actions)) if (!Array.isArray(actions))
return []; return [];
@@ -68,8 +105,8 @@ Popup {
} }
function executePluginAction(actionOrObj) { function executePluginAction(actionOrObj) {
var actionFunc = typeof actionOrObj === "function" ? actionOrObj : actionOrObj?.action; const actionFunc = typeof actionOrObj === "function" ? actionOrObj : actionOrObj?.action;
var closeLauncher = typeof actionOrObj === "object" && actionOrObj?.closeLauncher; const closeLauncher = typeof actionOrObj === "object" && actionOrObj?.closeLauncher;
if (typeof actionFunc === "function") if (typeof actionFunc === "function")
actionFunc(); actionFunc();
@@ -90,12 +127,12 @@ Popup {
} }
readonly property var menuItems: { readonly property var menuItems: {
var items = []; const items = [];
if (isPluginItem) { if (isPluginItem) {
var pluginActions = getPluginContextMenuActions(); const pluginActions = getPluginContextMenuActions();
for (var i = 0; i < pluginActions.length; i++) { for (let i = 0; i < pluginActions.length; i++) {
var act = pluginActions[i]; const act = pluginActions[i];
items.push({ items.push({
type: "item", type: "item",
icon: act.icon || "play_arrow", icon: act.icon || "play_arrow",
@@ -107,8 +144,8 @@ Popup {
} }
if (item?.type !== "app" && item?.actions && item.actions.length > 0) { if (item?.type !== "app" && item?.actions && item.actions.length > 0) {
for (var i = 0; i < item.actions.length; i++) { for (let i = 0; i < item.actions.length; i++) {
var genericAct = item.actions[i]; const genericAct = item.actions[i];
items.push({ items.push({
type: "item", type: "item",
icon: genericAct.icon || "play_arrow", icon: genericAct.icon || "play_arrow",
@@ -149,8 +186,8 @@ Popup {
items.push({ items.push({
type: "separator" type: "separator"
}); });
for (var i = 0; i < item.actions.length; i++) { for (let i = 0; i < item.actions.length; i++) {
var act = item.actions[i]; const act = item.actions[i];
items.push({ items.push({
type: "item", type: "item",
icon: act.icon || "play_arrow", icon: act.icon || "play_arrow",
@@ -183,43 +220,52 @@ Popup {
return items; return items;
} }
function menuItemsHeight() {
let h = 0;
for (let i = 0; i < menuItems.length; i++) {
h += menuItems[i].type === "separator" ? 5 : 32;
}
if (menuItems.length > 1)
h += menuItems.length - 1;
return h;
}
function show(x, y, spotlightItem, fromKeyboard) { function show(x, y, spotlightItem, fromKeyboard) {
if (!spotlightItem?.data) if (!spotlightItem?.data)
return; return;
item = spotlightItem; item = spotlightItem;
selectedMenuIndex = fromKeyboard ? 0 : -1; selectedMenuIndex = fromKeyboard ? 0 : -1;
keyboardNavigation = fromKeyboard; keyboardNavigation = fromKeyboard;
const modal = parentHandler?.parentModal ?? null;
const screenRef = modal?.effectiveScreen ?? parentHandler?.Window?.window?.screen ?? searchField?.Window?.window?.screen ?? null;
const screenX = screenRef?.x || 0;
const screenY = screenRef?.y || 0;
const screenRelativeX = modal ? ((modal.alignedX ?? 0) + x) : ((parentHandler ? parentHandler.mapToGlobal(x, y).x : x) - screenX);
const screenRelativeY = modal ? ((modal.alignedY ?? 0) + y) : ((parentHandler ? parentHandler.mapToGlobal(x, y).y : y) - screenY);
targetScreen = screenRef;
anchorX = screenRelativeX + 4;
anchorY = screenRelativeY + 4;
renderActive = true;
openState = true;
if (parentHandler) if (parentHandler)
parentHandler.enabled = false; parentHandler.enabled = false;
Qt.callLater(() => { Qt.callLater(() => {
var parentW = parent?.width ?? 500; menuFlickable.contentY = 0;
var parentH = parent?.height ?? 600; keyboardHandler.forceActiveFocus();
var menuW = width > 0 ? width : 200; ensureSelectedVisible();
var menuH = height > 0 ? height : 200;
var margin = 8;
var posX = x + 4;
var posY = y + 4;
if (posX + menuW > parentW - margin) {
posX = Math.max(margin, parentW - menuW - margin);
}
if (posY + menuH > parentH - margin) {
posY = Math.max(margin, parentH - menuH - margin);
}
root.x = posX;
root.y = posY;
open();
}); });
} }
function hide() { function hide() {
if (parentHandler) if (!renderActive)
parentHandler.enabled = true; return;
close(); openState = false;
hideRequested();
} }
function togglePin() { function togglePin() {
@@ -286,31 +332,96 @@ Popup {
property bool keyboardNavigation: false property bool keyboardNavigation: false
readonly property int visibleItemCount: { readonly property int visibleItemCount: {
var count = 0; let count = 0;
for (var i = 0; i < menuItems.length; i++) { for (let i = 0; i < menuItems.length; i++) {
if (menuItems[i].type === "item") if (menuItems[i].type === "item")
count++; count++;
} }
return count; return count;
} }
function handleKey(event) {
if (!openState)
return;
switch (event.key) {
case Qt.Key_Down:
selectNext();
event.accepted = true;
return;
case Qt.Key_Up:
selectPrevious();
event.accepted = true;
return;
case Qt.Key_Return:
case Qt.Key_Enter:
activateSelected();
event.accepted = true;
return;
case Qt.Key_Left:
case Qt.Key_Escape:
hide();
event.accepted = true;
return;
}
}
function selectNext() { function selectNext() {
if (visibleItemCount > 0) if (visibleItemCount > 0) {
keyboardNavigation = true;
selectedMenuIndex = (selectedMenuIndex + 1) % visibleItemCount; selectedMenuIndex = (selectedMenuIndex + 1) % visibleItemCount;
ensureSelectedVisible();
}
} }
function selectPrevious() { function selectPrevious() {
if (visibleItemCount > 0) if (visibleItemCount > 0) {
keyboardNavigation = true;
selectedMenuIndex = (selectedMenuIndex - 1 + visibleItemCount) % visibleItemCount; selectedMenuIndex = (selectedMenuIndex - 1 + visibleItemCount) % visibleItemCount;
ensureSelectedVisible();
}
}
function selectedDelegateIndex() {
let itemIndex = 0;
for (let i = 0; i < menuItems.length; i++) {
if (menuItems[i].type !== "item")
continue;
if (itemIndex === selectedMenuIndex)
return i;
itemIndex++;
}
return -1;
}
function ensureSelectedVisible() {
Qt.callLater(() => {
if (!menuFlickable || !menuRepeater)
return;
const delegateIndex = selectedDelegateIndex();
if (delegateIndex < 0)
return;
const delegate = menuRepeater.itemAt(delegateIndex);
if (!delegate)
return;
const top = delegate.y;
const bottom = top + delegate.height;
const viewTop = menuFlickable.contentY;
const viewBottom = viewTop + menuFlickable.height;
if (top < viewTop) {
menuFlickable.contentY = Math.max(0, top);
} else if (bottom > viewBottom) {
menuFlickable.contentY = Math.min(Math.max(0, menuFlickable.contentHeight - menuFlickable.height), bottom - menuFlickable.height);
}
});
} }
function activateSelected() { function activateSelected() {
var itemIndex = 0; let itemIndex = 0;
for (var i = 0; i < menuItems.length; i++) { for (let i = 0; i < menuItems.length; i++) {
if (menuItems[i].type !== "item") if (menuItems[i].type !== "item")
continue; continue;
if (itemIndex === selectedMenuIndex) { if (itemIndex === selectedMenuIndex) {
var menuItem = menuItems[i]; const menuItem = menuItems[i];
if (menuItem.action) if (menuItem.action)
menuItem.action(); menuItem.action();
else if (menuItem.pluginAction) else if (menuItem.pluginAction)
@@ -325,209 +436,233 @@ Popup {
} }
} }
width: menuContainer.implicitWidth PanelWindow {
height: menuContainer.implicitHeight id: menuWindow
padding: 0
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
modal: true
dim: false
background: Item {}
onOpened: { screen: root.targetScreen
Qt.callLater(() => keyboardHandler.forceActiveFocus()); visible: root.renderActive
} color: "transparent"
onClosed: { WlrLayershell.namespace: "dms:launcher-context-menu"
if (parentHandler) WlrLayershell.layer: WlrLayershell.Overlay
parentHandler.enabled = true; WlrLayershell.exclusiveZone: -1
if (searchField?.visible) { WlrLayershell.keyboardFocus: root.renderActive ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
Qt.callLater(() => searchField.forceActiveFocus());
}
}
enter: Transition { anchors {
NumberAnimation { top: true
property: "opacity" left: true
from: 0 right: true
to: 1 bottom: true
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
exit: Transition {
NumberAnimation {
property: "opacity"
from: 1
to: 0
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
contentItem: Item {
id: keyboardHandler
focus: true
implicitWidth: menuContainer.implicitWidth
implicitHeight: menuContainer.implicitHeight
Keys.onPressed: event => {
switch (event.key) {
case Qt.Key_Down:
root.selectNext();
event.accepted = true;
return;
case Qt.Key_Up:
root.selectPrevious();
event.accepted = true;
return;
case Qt.Key_Return:
case Qt.Key_Enter:
root.activateSelected();
event.accepted = true;
return;
case Qt.Key_Escape:
case Qt.Key_Left:
root.hide();
event.accepted = true;
return;
}
} }
Rectangle { WindowBlur {
id: menuContainer targetWindow: menuWindow
blurX: root.blurActive ? menuContainer.x : 0
blurY: root.blurActive ? menuContainer.y : 0
blurWidth: root.blurActive ? menuContainer.width : 0
blurHeight: root.blurActive ? menuContainer.height : 0
blurRadius: Theme.cornerRadius
}
MouseArea {
anchors.fill: parent anchors.fill: parent
implicitWidth: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2) z: -1
implicitHeight: menuColumn.implicitHeight + Theme.spacingS * 2 enabled: root.renderActive
color: Theme.floatingSurface onClicked: root.hide()
radius: Theme.cornerRadius }
border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: BlurService.enabled ? BlurService.borderWidth : 1 Item {
id: keyboardHandler
anchors.fill: parent
focus: root.openState
Keys.onPressed: event => {
switch (event.key) {
case Qt.Key_Down:
root.selectNext();
event.accepted = true;
return;
case Qt.Key_Up:
root.selectPrevious();
event.accepted = true;
return;
case Qt.Key_Return:
case Qt.Key_Enter:
root.activateSelected();
event.accepted = true;
return;
case Qt.Key_Escape:
case Qt.Key_Left:
root.hide();
event.accepted = true;
return;
}
}
Rectangle { Rectangle {
anchors.fill: parent id: menuContainer
anchors.topMargin: 4 x: Math.max(root.menuMargin, Math.min(menuWindow.width - width - root.menuMargin, root.anchorX))
anchors.leftMargin: 2 y: Math.max(root.menuMargin, Math.min(menuWindow.height - height - root.menuMargin, root.anchorY))
anchors.rightMargin: -2 width: root.effectiveMenuWidth
anchors.bottomMargin: -4 height: root.effectiveMenuHeight
radius: parent.radius color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
color: Qt.rgba(0, 0, 0, 0.15) radius: Theme.cornerRadius
z: -1 border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
} border.width: BlurService.enabled ? BlurService.borderWidth : 1
opacity: root.openState ? 1 : 0
Column { Behavior on opacity {
id: menuColumn NumberAnimation {
anchors.fill: parent duration: Theme.shortDuration
anchors.margins: Theme.spacingS easing.type: Theme.emphasizedEasing
spacing: 1 onRunningChanged: {
if (!running && !root.openState) {
Repeater { root.renderActive = false;
model: root.menuItems if (root.parentHandler)
root.parentHandler.enabled = true;
Item { if (root.searchField?.visible)
id: menuItemDelegate Qt.callLater(() => root.searchField.forceActiveFocus());
required property var modelData
required property int index
width: menuColumn.width
height: modelData.type === "separator" ? 5 : 32
readonly property int itemIndex: {
var count = 0;
for (var i = 0; i < index; i++) {
if (root.menuItems[i].type === "item")
count++;
}
return count;
}
Rectangle {
visible: menuItemDelegate.modelData.type === "separator"
width: parent.width - Theme.spacingS * 2
height: parent.height
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
} }
} }
}
}
Rectangle { Rectangle {
visible: menuItemDelegate.modelData.type === "item" anchors.fill: parent
width: parent.width anchors.topMargin: 4
height: parent.height anchors.leftMargin: 2
radius: Theme.cornerRadius anchors.rightMargin: -2
color: { anchors.bottomMargin: -4
if (root.keyboardNavigation && root.selectedMenuIndex === menuItemDelegate.itemIndex) { radius: parent.radius
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2); color: Qt.rgba(0, 0, 0, 0.15)
z: -1
}
Flickable {
id: menuFlickable
anchors.fill: parent
anchors.margins: Theme.spacingS
clip: true
contentWidth: width
contentHeight: menuColumn.implicitHeight
boundsBehavior: Flickable.StopAtBounds
interactive: root.menuScrolls
Column {
id: menuColumn
width: menuFlickable.width
spacing: 1
Repeater {
id: menuRepeater
model: root.menuItems
Item {
id: menuItemDelegate
required property var modelData
required property int index
width: menuColumn.width
height: modelData.type === "separator" ? 5 : 32
readonly property int itemIndex: {
let count = 0;
for (let i = 0; i < index; i++) {
if (root.menuItems[i].type === "item")
count++;
}
return count;
} }
return itemMouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
}
Row { Rectangle {
anchors.left: parent.left visible: menuItemDelegate.modelData.type === "separator"
anchors.leftMargin: Theme.spacingS width: parent.width - Theme.spacingS * 2
anchors.right: parent.right height: parent.height
anchors.rightMargin: Theme.spacingS anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter color: "transparent"
spacing: Theme.spacingS
Item { Rectangle {
width: Theme.iconSize - 2 anchors.centerIn: parent
height: Theme.iconSize - 2 width: parent.width
anchors.verticalCenter: parent.verticalCenter height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
DankIcon {
visible: (menuItemDelegate.modelData?.icon ?? "").length > 0
name: menuItemDelegate.modelData?.icon ?? ""
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
} }
} }
StyledText { Rectangle {
text: menuItemDelegate.modelData.text || "" visible: menuItemDelegate.modelData.type === "item"
font.pixelSize: Theme.fontSizeSmall width: parent.width
color: Theme.surfaceText height: parent.height
font.weight: Font.Normal radius: Theme.cornerRadius
anchors.verticalCenter: parent.verticalCenter color: {
elide: Text.ElideRight if (root.keyboardNavigation && root.selectedMenuIndex === menuItemDelegate.itemIndex) {
width: parent.width - (Theme.iconSize - 2) - Theme.spacingS return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2);
} }
} return itemMouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
}
DankRipple { Row {
id: menuItemRipple anchors.left: parent.left
rippleColor: Theme.surfaceText anchors.leftMargin: Theme.spacingS
cornerRadius: Theme.cornerRadius anchors.right: parent.right
} anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
MouseArea { Item {
id: itemMouseArea width: Theme.iconSize - 2
anchors.fill: parent height: Theme.iconSize - 2
hoverEnabled: true anchors.verticalCenter: parent.verticalCenter
cursorShape: Qt.PointingHandCursor
onEntered: { DankIcon {
root.keyboardNavigation = false; visible: (menuItemDelegate.modelData?.icon ?? "").length > 0
root.selectedMenuIndex = menuItemDelegate.itemIndex; name: menuItemDelegate.modelData?.icon ?? ""
} size: Theme.iconSize - 2
onPressed: mouse => menuItemRipple.trigger(mouse.x, mouse.y) color: Theme.surfaceText
onClicked: { opacity: 0.7
var menuItem = menuItemDelegate.modelData; anchors.verticalCenter: parent.verticalCenter
if (menuItem.action) }
menuItem.action(); }
else if (menuItem.pluginAction)
root.executePluginAction(menuItem.pluginAction); StyledText {
else if (menuItem.launcherActionData) text: menuItemDelegate.modelData.text || ""
root.executeLauncherAction(menuItem.launcherActionData); font.pixelSize: Theme.fontSizeSmall
else if (menuItem.actionData) color: Theme.surfaceText
root.executeDesktopAction(menuItem.actionData); font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
width: parent.width - (Theme.iconSize - 2) - Theme.spacingS
}
}
DankRipple {
id: menuItemRipple
rippleColor: Theme.surfaceText
cornerRadius: Theme.cornerRadius
}
MouseArea {
id: itemMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
root.keyboardNavigation = false;
root.selectedMenuIndex = menuItemDelegate.itemIndex;
}
onPressed: mouse => menuItemRipple.trigger(mouse.x, mouse.y)
onClicked: {
const menuItem = menuItemDelegate.modelData;
if (menuItem.action)
menuItem.action();
else if (menuItem.pluginAction)
root.executePluginAction(menuItem.pluginAction);
else if (menuItem.launcherActionData)
root.executeLauncherAction(menuItem.launcherActionData);
else if (menuItem.actionData)
root.executeDesktopAction(menuItem.actionData);
}
}
} }
} }
} }
@@ -12,11 +12,11 @@ FocusScope {
property var parentModal: null property var parentModal: null
property alias searchField: searchInput property alias searchField: searchInput
property alias controller: searchController property alias controller: searchController
readonly property alias activeContextMenu: contextMenu
readonly property bool _hasQuery: searchInput.text.length > 0 readonly property bool _hasQuery: searchInput.text.length > 0
readonly property real _searchBarH: 56 readonly property real _searchBarH: 56
readonly property real _surfaceInset: BlurService.enabled ? (_hasQuery ? Theme.spacingS : Theme.spacingXS) : 0 readonly property real _searchAreaH: _searchBarH
readonly property real _searchAreaH: _searchBarH + _surfaceInset * 2
readonly property real _statusH: 92 readonly property real _statusH: 92
readonly property real _rowH: 64 readonly property real _rowH: 64
readonly property real _maxResultsH: Math.min(430, (parentModal?.screenHeight ?? 900) * 0.55) readonly property real _maxResultsH: Math.min(430, (parentModal?.screenHeight ?? 900) * 0.55)
@@ -25,13 +25,34 @@ FocusScope {
readonly property real _resultsH: _hasQuery ? Math.min(_resultsContentH, _maxResultsH) : 0 readonly property real _resultsH: _hasQuery ? Math.min(_resultsContentH, _maxResultsH) : 0
readonly property int _fastDuration: 90 readonly property int _fastDuration: 90
readonly property int _resizeDuration: 110 readonly property int _resizeDuration: 110
readonly property bool _blurActive: Theme.blurForegroundLayers || Theme.transparentBlurLayers
readonly property real _searchSurfaceAlpha: {
if (Theme.transparentBlurLayers)
return _hasQuery ? 0.34 : 0.28;
if (Theme.blurForegroundLayers)
return Math.max(Theme.popupTransparency, _hasQuery ? 0.68 : 0.74);
return _hasQuery ? Theme.popupTransparency : Math.max(0.68, Theme.popupTransparency * 0.9);
}
readonly property color _searchSurfaceColor: Theme.withAlpha(_hasQuery ? Theme.surfaceContainerHigh : Theme.surfaceContainer, _searchSurfaceAlpha)
readonly property color _searchWellColor: {
if (searchInput.activeFocus)
return Theme.withAlpha(Theme.primaryContainer, Theme.transparentBlurLayers ? 0.42 : 1.0);
if (Theme.transparentBlurLayers)
return Theme.ccPillInactiveBg;
return Theme.surfaceContainer;
}
implicitHeight: _searchAreaH + (_resultsH > 0 ? 1 + _resultsH : 0) implicitHeight: _searchAreaH + _resultsH
function resetScroll() { function resetScroll() {
resultsList.resetScroll(); resultsList.resetScroll();
} }
function closeTransientUi() {
contextMenu.hide();
root.enabled = true;
}
function _buildRows() { function _buildRows() {
const flat = searchController.flatModel || []; const flat = searchController.flatModel || [];
const sections = searchController.sections || []; const sections = searchController.sections || [];
@@ -122,13 +143,11 @@ FocusScope {
} }
break; break;
case Qt.Key_Tab: case Qt.Key_Tab:
if (_hasQuery) _cycleCategory(false);
_cycleCategory(false);
event.accepted = true; event.accepted = true;
return; return;
case Qt.Key_Backtab: case Qt.Key_Backtab:
if (_hasQuery) _cycleCategory(true);
_cycleCategory(true);
event.accepted = true; event.accepted = true;
return; return;
case Qt.Key_Return: case Qt.Key_Return:
@@ -177,13 +196,6 @@ FocusScope {
return; return;
} }
break; break;
case Qt.Key_Slash:
if (event.modifiers === Qt.NoModifier && searchInput.text.length === 0) {
searchController.setMode("files", true);
event.accepted = true;
return;
}
break;
} }
event.accepted = false; event.accepted = false;
@@ -193,6 +205,7 @@ FocusScope {
id: searchController id: searchController
active: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true active: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
viewModeContext: "spotlight" viewModeContext: "spotlight"
forceLinearNavigation: true
onItemExecuted: { onItemExecuted: {
root.parentModal?.hide(); root.parentModal?.hide();
@@ -210,10 +223,25 @@ FocusScope {
allowEditActions: false allowEditActions: false
} }
Connections {
target: root.parentModal
ignoreUnknownSignals: true
function onSpotlightOpenChanged() {
if (!root.parentModal?.spotlightOpen)
root.closeTransientUi();
}
function onContentVisibleChanged() {
if (!root.parentModal?.contentVisible)
root.closeTransientUi();
}
}
Connections { Connections {
target: searchController target: searchController
function onModeChanged(mode) { function onModeChanged(mode, userInitiated) {
if (searchController.autoSwitchedToFiles) if (!userInitiated || !SettingsData.rememberLastMode)
return; return;
SessionData.setLauncherLastMode(mode); SessionData.setLauncherLastMode(mode);
} }
@@ -233,11 +261,8 @@ FocusScope {
Rectangle { Rectangle {
id: searchBarSurface id: searchBarSurface
anchors.fill: parent anchors.fill: parent
anchors.margins: root._surfaceInset radius: Theme.cornerRadius
radius: height / 2 color: root._searchSurfaceColor
color: Theme.withAlpha(root._hasQuery ? Theme.surfaceContainerHigh : Theme.surfaceContainer, root._hasQuery ? Theme.popupTransparency : Math.max(0.68, Theme.popupTransparency * 0.9))
border.color: BlurService.enabled && !root._hasQuery ? Theme.withAlpha(Theme.outline, 0.08) : "transparent"
border.width: BlurService.enabled && !root._hasQuery ? 1 : 0
Behavior on color { Behavior on color {
ColorAnimation { ColorAnimation {
@@ -254,7 +279,7 @@ FocusScope {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Theme.spacingM anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color: searchInput.activeFocus ? Theme.primaryContainer : Theme.surfaceContainer color: root._searchWellColor
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
@@ -273,8 +298,8 @@ FocusScope {
Row { Row {
id: categoryRow id: categoryRow
visible: SettingsData.spotlightBarShowModeChips || root._hasQuery
spacing: Theme.spacingXS spacing: Theme.spacingXS
visible: root._hasQuery
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
Repeater { Repeater {
@@ -380,28 +405,9 @@ FocusScope {
} }
} }
Rectangle {
anchors.top: searchBarItem.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: root._surfaceInset
anchors.rightMargin: root._surfaceInset
height: 1
color: Theme.outlineMedium
opacity: root._resultsH > 0 ? 0.55 : 0
Behavior on opacity {
NumberAnimation {
duration: root._fastDuration
easing.type: Theme.standardEasing
}
}
}
Item { Item {
id: resultsContainer id: resultsContainer
anchors.top: searchBarItem.bottom anchors.top: searchBarItem.bottom
anchors.topMargin: 1
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
clip: true clip: true
@@ -12,6 +12,7 @@ Item {
property var controller: null property var controller: null
property bool hasQuery: false property bool hasQuery: false
property var rows: [] property var rows: []
readonly property real bottomInset: Theme.spacingS
signal itemRightClicked(int index, var item, real mouseX, real mouseY) signal itemRightClicked(int index, var item, real mouseX, real mouseY)
@@ -53,7 +54,11 @@ Item {
DankListView { DankListView {
id: mainListView id: mainListView
anchors.fill: parent anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.bottomMargin: root.bottomInset
clip: true clip: true
visible: root.rows.length > 0 visible: root.rows.length > 0
@@ -64,11 +69,6 @@ Item {
objectProp: "_rowId" objectProp: "_rowId"
} }
add: null
remove: null
displaced: null
move: null
delegate: Item { delegate: Item {
id: delegateRoot id: delegateRoot
required property var modelData required property var modelData
@@ -103,7 +103,11 @@ Item {
} }
Item { Item {
anchors.fill: parent anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.bottomMargin: root.bottomInset
visible: root.hasQuery && root.rows.length === 0 visible: root.hasQuery && root.rows.length === 0
Row { Row {
+13
View File
@@ -81,6 +81,8 @@ DankModal {
executeAction(action); executeAction(action);
} }
signal switchUserRequested
function executeAction(action) { function executeAction(action) {
if (action === "lock") { if (action === "lock") {
close(); close();
@@ -92,6 +94,11 @@ DankModal {
Quickshell.execDetached(["dms", "restart"]); Quickshell.execDetached(["dms", "restart"]);
return; return;
} }
if (action === "switchuser") {
close();
switchUserRequested();
return;
}
close(); close();
root.powerActionRequested(action, "", ""); root.powerActionRequested(action, "", "");
} }
@@ -216,6 +223,12 @@ DankModal {
"label": I18n.tr("Restart DMS"), "label": I18n.tr("Restart DMS"),
"key": "D" "key": "D"
}; };
case "switchuser":
return {
"icon": "switch_account",
"label": I18n.tr("Switch User"),
"key": "U"
};
default: default:
return { return {
"icon": "help", "icon": "help",
@@ -64,6 +64,7 @@ FocusScope {
sourceComponent: KeybindsTab { sourceComponent: KeybindsTab {
parentModal: root.parentModal parentModal: root.parentModal
requestedSearchQuery: root.parentModal?.keybindSearchQuery ?? ""
} }
onActiveChanged: { onActiveChanged: {
@@ -554,5 +555,20 @@ FocusScope {
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
} }
} }
Loader {
id: usersLoader
anchors.fill: parent
active: root.currentIndex === 35
visible: active
focus: active
sourceComponent: UsersTab {}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
} }
} }
@@ -37,6 +37,7 @@ FloatingWindow {
property bool isCompactMode: width < 700 property bool isCompactMode: width < 700
property bool menuVisible: !isCompactMode property bool menuVisible: !isCompactMode
property bool enableAnimations: true property bool enableAnimations: true
property string keybindSearchQuery: ""
signal closingModal signal closingModal
@@ -73,6 +74,11 @@ FloatingWindow {
return sidebar.resolveTabIndex(tabName); return sidebar.resolveTabIndex(tabName);
} }
function showKeybindsSearch(query: string) {
keybindSearchQuery = query || "";
showWithTabName("keybinds");
}
function toggleMenu() { function toggleMenu() {
enableAnimations = true; enableAnimations = true;
menuVisible = !menuVisible; menuVisible = !menuVisible;
@@ -293,6 +293,12 @@ Rectangle {
"tabIndex": 20, "tabIndex": 20,
"updaterOnly": true "updaterOnly": true
}, },
{
"id": "users",
"text": I18n.tr("Users"),
"icon": "manage_accounts",
"tabIndex": 35
},
{ {
"id": "window_rules", "id": "window_rules",
"text": I18n.tr("Window Rules"), "text": I18n.tr("Window Rules"),
+272
View File
@@ -0,0 +1,272 @@
import QtQuick
import qs.Common
import qs.Modals.Common
import qs.Services
import qs.Widgets
DankModal {
id: root
property bool lockOnSwitch: false
function showFromPowerMenu() {
root.lockOnSwitch = false;
SessionsService.refresh();
open();
}
function showFromLockScreen() {
root.lockOnSwitch = true;
SessionsService.refresh();
open();
}
function _formatTty(s) {
if (s.tty && s.tty.length > 0)
return s.tty;
if (s.seat && s.seat.length > 0)
return s.seat;
return I18n.tr("remote");
}
function _formatType(s) {
if (!s.type || s.type.length === 0)
return "";
switch (s.type) {
case "wayland":
return "Wayland";
case "x11":
return "X11";
case "tty":
return "TTY";
default:
return s.type.charAt(0).toUpperCase() + s.type.substring(1);
}
}
function _doSwitch(sessionId, username) {
if (root.lockOnSwitch && typeof SessionService !== "undefined" && SessionService.loginctlAvailable)
SessionService.lock();
SessionsService.activate(sessionId, null);
close();
}
layerNamespace: "dms:switch-user-modal"
shouldBeVisible: false
allowStacking: true
modalWidth: 420
modalHeight: contentLoader.item ? Math.min(540, contentLoader.item.implicitHeight + Theme.spacingM * 2) : 320
enableShadow: true
shouldHaveFocus: true
onBackgroundClicked: close()
Connections {
target: SessionsService
function onSwitchRequested() {
root.showFromPowerMenu();
}
}
content: Component {
Item {
anchors.fill: parent
implicitHeight: mainColumn.implicitHeight
Column {
id: mainColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
anchors.topMargin: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "switch_account"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Switch User")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
width: parent.width
text: I18n.tr("Select an active session to switch to. The current session stays running in the background.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
visible: SessionsService.otherSessions().length > 0
}
Column {
width: parent.width
spacing: Theme.spacingS
visible: SessionsService.otherSessions().length > 0
Repeater {
model: SessionsService.otherSessions()
Rectangle {
id: sessionRow
required property var modelData
width: parent.width
height: 64
radius: Theme.cornerRadius
color: sessionMouse.containsMouse ? Theme.surfacePressed : Theme.surfaceContainerHighest
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
DankIcon {
name: "account_circle"
size: Theme.iconSize + 4
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - 4 - chevron.width - Theme.spacingM * 2
spacing: 2
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: sessionRow.modelData.username
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: {
const tty = root._formatTty(sessionRow.modelData);
const type = root._formatType(sessionRow.modelData);
const parts = [];
if (type)
parts.push(type);
parts.push(I18n.tr("session %1").arg(sessionRow.modelData.sessionId));
if (tty)
parts.push(tty);
return parts.join(" · ");
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
DankIcon {
id: chevron
name: "chevron_right"
size: Theme.iconSize
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: sessionMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root._doSwitch(sessionRow.modelData.sessionId, sessionRow.modelData.username)
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
visible: SessionsService.otherSessions().length === 0
Rectangle {
width: parent.width
height: bodyCol.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHighest
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
DankIcon {
name: "info"
size: Theme.iconSize
color: Theme.surfaceVariantText
anchors.top: parent.top
anchors.topMargin: 2
}
Column {
id: bodyCol
width: parent.width - Theme.iconSize - Theme.spacingM
spacing: 4
StyledText {
text: I18n.tr("No other active sessions on this seat")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
width: parent.width
text: I18n.tr("To sign in as a different user, log out and pick the account from the greeter. Creating a fresh session in parallel needs a multi-session greeter (greetd-flexiserver / GDM / LightDM).")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
}
}
}
}
Row {
width: parent.width
spacing: Theme.spacingM
layoutDirection: Qt.RightToLeft
DankButton {
text: I18n.tr("Close")
backgroundColor: Theme.surfaceVariantAlpha
textColor: Theme.surfaceText
onClicked: root.close()
}
DankButton {
visible: SessionsService.otherSessions().length === 0 && !root.lockOnSwitch
text: I18n.tr("Log out")
iconName: "logout"
backgroundColor: Theme.primary
textColor: Theme.primaryText
onClicked: {
if (typeof SessionService !== "undefined")
SessionService.logout();
root.close();
}
}
}
Item {
width: 1
height: Theme.spacingS
}
}
}
}
}
@@ -59,21 +59,19 @@ Item {
ignoreUnknownSignals: true ignoreUnknownSignals: true
function onDeviceNameChanged(newDeviceName) { function onDeviceNameChanged(newDeviceName) {
if (root.expandedWidgetData && root.expandedWidgetData.id === "brightnessSlider") { if (!root.expandedWidgetData || root.expandedWidgetData.id !== "brightnessSlider") {
const widgets = SettingsData.controlCenterWidgets || []; return;
const newWidgets = widgets.map(w => {
if (w.id === "brightnessSlider" && w.instanceId === root.expandedWidgetData.instanceId) {
const updatedWidget = Object.assign({}, w);
updatedWidget.deviceName = newDeviceName;
return updatedWidget;
}
return w;
});
SettingsData.set("controlCenterWidgets", newWidgets);
if (root.collapseCallback) {
root.collapseCallback();
}
} }
const widgets = SettingsData.controlCenterWidgets || [];
const newWidgets = widgets.map(w => {
if (w.id === "brightnessSlider" && w.instanceId === root.expandedWidgetData.instanceId) {
const updatedWidget = Object.assign({}, w);
updatedWidget.deviceName = newDeviceName;
return updatedWidget;
}
return w;
});
SettingsData.set("controlCenterWidgets", newWidgets);
} }
} }
@@ -301,12 +301,22 @@ Column {
property var widgetDef: root.model?.getWidgetForId(widgetData.id || "") property var widgetDef: root.model?.getWidgetForId(widgetData.id || "")
width: parent.width width: parent.width
height: 60 height: 60
iconBlinking: {
const id = widgetData.id || "";
if (id === "wifi")
return NetworkService.isWifiConnecting;
if (id === "bluetooth")
return BluetoothService.connecting;
return false;
}
iconName: { iconName: {
switch (widgetData.id || "") { switch (widgetData.id || "") {
case "wifi": case "wifi":
{ {
if (NetworkService.wifiToggling) if (NetworkService.wifiToggling)
return "sync"; return "sync";
if (NetworkService.isConnecting && !NetworkService.ethernetConnected)
return NetworkService.wifiSignalIcon;
const status = NetworkService.networkStatus; const status = NetworkService.networkStatus;
if (status === "ethernet") if (status === "ethernet")
@@ -360,6 +370,8 @@ Column {
{ {
if (NetworkService.wifiToggling) if (NetworkService.wifiToggling)
return NetworkService.wifiEnabled ? I18n.tr("Disabling WiFi...", "network status") : I18n.tr("Enabling WiFi...", "network status"); return NetworkService.wifiEnabled ? I18n.tr("Disabling WiFi...", "network status") : I18n.tr("Enabling WiFi...", "network status");
if (NetworkService.isConnecting && !NetworkService.ethernetConnected)
return NetworkService.connectingSSID || I18n.tr("Connecting...", "network status");
const status = NetworkService.networkStatus; const status = NetworkService.networkStatus;
if (status === "ethernet") if (status === "ethernet")
@@ -400,6 +412,8 @@ Column {
{ {
if (NetworkService.wifiToggling) if (NetworkService.wifiToggling)
return I18n.tr("Please wait...", "network status"); return I18n.tr("Please wait...", "network status");
if (NetworkService.isConnecting && !NetworkService.ethernetConnected)
return I18n.tr("Connecting...", "network status");
const status = NetworkService.networkStatus; const status = NetworkService.networkStatus;
if (status === "ethernet") if (status === "ethernet")
@@ -422,6 +436,8 @@ Column {
return I18n.tr("No adapters", "bluetooth status"); return I18n.tr("No adapters", "bluetooth status");
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) if (!BluetoothService.adapter || !BluetoothService.adapter.enabled)
return I18n.tr("Off", "bluetooth status"); return I18n.tr("Off", "bluetooth status");
if (BluetoothService.connecting)
return I18n.tr("Connecting...", "bluetooth status");
const primaryDevice = (() => { const primaryDevice = (() => {
if (!BluetoothService.adapter || !BluetoothService.adapter.devices) if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
return null; return null;
@@ -23,79 +23,103 @@ Rectangle {
if (!screenName) if (!screenName)
return ""; return "";
const screen = Quickshell.screens.find(s => s.name === screenName); const screen = Quickshell.screens.find(s => s.name === screenName);
if (screen) { if (screen)
return SettingsData.getScreenDisplayName(screen); return SettingsData.getScreenDisplayName(screen);
} if (SettingsData.displayNameMode === "model" && screenModel && screenModel.length > 0)
if (SettingsData.displayNameMode === "model" && screenModel && screenModel.length > 0) {
return screenModel; return screenModel;
}
return screenName; return screenName;
} }
function resolveDeviceName() { function resolveCurrentDevice() {
if (!DisplayService.brightnessAvailable || !DisplayService.devices || DisplayService.devices.length === 0) { const devices = DisplayService.devices || [];
if (!DisplayService.brightnessAvailable || devices.length === 0)
return ""; return "";
}
const pinKey = getScreenPinKey(); const pinKey = getScreenPinKey();
if (pinKey.length > 0) { if (pinKey.length > 0) {
const pins = SettingsData.brightnessDevicePins || {}; const pins = SettingsData.brightnessDevicePins || {};
const pinnedDevice = pins[pinKey]; const pinnedDevice = pins[pinKey];
if (pinnedDevice && pinnedDevice.length > 0) { if (pinnedDevice && pinnedDevice.length > 0) {
const found = DisplayService.devices.find(dev => dev.name === pinnedDevice); const found = devices.find(d => d.name === pinnedDevice);
if (found) if (found)
return found.name; return found.name;
} }
} }
if (instanceId) {
const widgets = SettingsData.controlCenterWidgets || [];
const widget = widgets.find(w => w.id === "brightnessSlider" && w.instanceId === instanceId);
if (widget && typeof widget.deviceName === "string" && widget.deviceName.length > 0) {
const found = devices.find(d => d.name === widget.deviceName);
if (found)
return found.name;
}
}
if (DisplayService.currentDevice) {
const found = devices.find(d => d.name === DisplayService.currentDevice);
if (found)
return found.name;
}
if (initialDeviceName && initialDeviceName.length > 0) { if (initialDeviceName && initialDeviceName.length > 0) {
const found = DisplayService.devices.find(dev => dev.name === initialDeviceName); const found = devices.find(d => d.name === initialDeviceName);
if (found) if (found)
return found.name; return found.name;
} }
const currentDeviceNameFromService = DisplayService.currentDevice; const backlight = devices.find(d => d.class === "backlight");
if (currentDeviceNameFromService) {
const found = DisplayService.devices.find(dev => dev.name === currentDeviceNameFromService);
if (found)
return found.name;
}
const backlight = DisplayService.devices.find(d => d.class === "backlight");
if (backlight) if (backlight)
return backlight.name; return backlight.name;
const ddc = DisplayService.devices.find(d => d.class === "ddc"); const ddc = devices.find(d => d.class === "ddc");
if (ddc) if (ddc)
return ddc.name; return ddc.name;
return DisplayService.devices.length > 0 ? DisplayService.devices[0].name : ""; return devices[0].name;
}
function selectDevice(deviceName) {
if (!deviceName || deviceName === root.currentDeviceName) {
return;
}
const pinKey = getScreenPinKey();
if (pinKey.length > 0) {
const pins = SettingsData.brightnessDevicePins || {};
const existing = pins[pinKey];
if (existing && existing !== deviceName) {
const next = JSON.parse(JSON.stringify(pins));
delete next[pinKey];
SettingsData.set("brightnessDevicePins", next);
}
}
root.currentDeviceName = deviceName;
DisplayService.setCurrentDevice(deviceName, true);
Qt.callLater(() => root.deviceNameChanged(deviceName));
} }
Component.onCompleted: { Component.onCompleted: {
currentDeviceName = resolveDeviceName(); root.currentDeviceName = resolveCurrentDevice();
} }
property bool isPinnedToScreen: { function isDevicePinnedToScreen(deviceName) {
const pinKey = getScreenPinKey(); const pinKey = getScreenPinKey();
if (!pinKey || pinKey.length === 0) if (!pinKey || !deviceName)
return false; return false;
const pins = SettingsData.brightnessDevicePins || {}; const pins = SettingsData.brightnessDevicePins || {};
return pins[pinKey] === currentDeviceName; return pins[pinKey] === deviceName;
} }
function togglePinToScreen() { function togglePinForDevice(deviceName) {
const pinKey = getScreenPinKey(); const pinKey = getScreenPinKey();
if (!pinKey || pinKey.length === 0 || !currentDeviceName || currentDeviceName.length === 0) if (!pinKey || !deviceName)
return; return;
const pins = JSON.parse(JSON.stringify(SettingsData.brightnessDevicePins || {})); const pins = JSON.parse(JSON.stringify(SettingsData.brightnessDevicePins || {}));
if (pins[pinKey] === deviceName) {
if (isPinnedToScreen) {
delete pins[pinKey]; delete pins[pinKey];
} else { } else {
pins[pinKey] = currentDeviceName; pins[pinKey] = deviceName;
} }
SettingsData.set("brightnessDevicePins", pins); SettingsData.set("brightnessDevicePins", pins);
} }
@@ -153,18 +177,23 @@ Rectangle {
} }
Rectangle { Rectangle {
id: monitorHeader
width: parent.width width: parent.width
height: 40 height: 40
visible: screenName && screenName.length > 0 && DisplayService.devices && DisplayService.devices.length > 1 visible: screenName && screenName.length > 0 && DisplayService.devices && DisplayService.devices.length > 1
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
property bool currentDevicePinned: root.isDevicePinnedToScreen(currentDeviceName)
Item { Item {
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingM anchors.margins: Theme.spacingM
Row { Row {
anchors.left: parent.left anchors.left: parent.left
anchors.right: globalPinButton.left
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM spacing: Theme.spacingM
@@ -180,47 +209,51 @@ Rectangle {
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
width: parent.width - Theme.iconSize - Theme.spacingM
} }
} }
Rectangle { Rectangle {
id: globalPinButton
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: pinRow.width + Theme.spacingS * 2 width: globalPinRow.width + Theme.spacingS * 2
height: 28 height: 28
radius: height / 2 radius: height / 2
color: isPinnedToScreen ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05) color: monitorHeader.currentDevicePinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Theme.withAlpha(Theme.surfaceText, 0.05)
Row { Row {
id: pinRow id: globalPinRow
anchors.centerIn: parent anchors.centerIn: parent
spacing: 4 spacing: 4
DankIcon { DankIcon {
name: isPinnedToScreen ? "push_pin" : "push_pin" name: "push_pin"
size: 16 size: 16
color: isPinnedToScreen ? Theme.primary : Theme.surfaceText color: monitorHeader.currentDevicePinned ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
StyledText { StyledText {
text: isPinnedToScreen ? I18n.tr("Pinned") : I18n.tr("Pin") text: monitorHeader.currentDevicePinned ? I18n.tr("Pinned") : I18n.tr("Pin")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: isPinnedToScreen ? Theme.primary : Theme.surfaceText color: monitorHeader.currentDevicePinned ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
DankRipple { DankRipple {
id: pinRipple id: globalPinRipple
cornerRadius: parent.radius cornerRadius: parent.radius
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y) enabled: currentDeviceName && currentDeviceName.length > 0
onClicked: root.togglePinToScreen() onPressed: mouse => globalPinRipple.trigger(mouse.x, mouse.y)
onClicked: root.togglePinForDevice(currentDeviceName)
} }
} }
} }
@@ -229,9 +262,17 @@ Rectangle {
Repeater { Repeater {
model: DisplayService.devices || [] model: DisplayService.devices || []
delegate: Rectangle { delegate: Rectangle {
id: deviceCard
required property var modelData required property var modelData
required property int index required property int index
readonly property bool selected: !!(modelData && modelData.name === root.currentDeviceName)
readonly property bool devicePinnedHere: {
SettingsData.brightnessDevicePins;
return root.isDevicePinnedToScreen(modelData ? modelData.name : "");
}
property real deviceBrightness: { property real deviceBrightness: {
DisplayService.brightnessVersion; DisplayService.brightnessVersion;
return DisplayService.getDeviceBrightness(modelData.name); return DisplayService.getDeviceBrightness(modelData.name);
@@ -241,8 +282,8 @@ Rectangle {
height: 100 height: 100
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
border.color: modelData.name === currentDeviceName ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) border.color: selected ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: modelData.name === currentDeviceName ? 2 : 0 border.width: selected ? 2 : 0
Column { Column {
anchors.fill: parent anchors.fill: parent
@@ -251,10 +292,12 @@ Rectangle {
Item { Item {
width: parent.width width: parent.width
height: Math.max(deviceIconColumn.height, deviceInfoColumn.height, exponentControls.height) height: Math.max(deviceIconColumn.height, deviceInfoColumn.height, rightControls.height)
Row { Row {
anchors.left: parent.left anchors.left: parent.left
anchors.right: rightControls.left
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM spacing: Theme.spacingM
@@ -281,7 +324,7 @@ Rectangle {
} }
} }
size: Theme.iconSize size: Theme.iconSize
color: modelData.name === currentDeviceName ? Theme.primary : Theme.surfaceText color: deviceCard.selected ? Theme.primary : Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
} }
@@ -296,7 +339,7 @@ Rectangle {
Column { Column {
id: deviceInfoColumn id: deviceInfoColumn
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: parent.parent.width - deviceIconColumn.width - exponentControls.width - Theme.spacingM * 3 width: parent.width - deviceIconColumn.width - Theme.spacingM
StyledText { StyledText {
text: { text: {
@@ -309,7 +352,7 @@ Rectangle {
} }
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText color: Theme.surfaceText
font.weight: modelData.name === currentDeviceName ? Font.Medium : Font.Normal font.weight: deviceCard.selected ? Font.Medium : Font.Normal
elide: Text.ElideRight elide: Text.ElideRight
width: parent.width width: parent.width
horizontalAlignment: Text.AlignLeft horizontalAlignment: Text.AlignLeft
@@ -345,80 +388,107 @@ Rectangle {
} }
Row { Row {
id: exponentControls id: rightControls
width: 140
height: 28 height: 28
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS spacing: Theme.spacingS
visible: SessionData.getBrightnessExponential(modelData.name)
z: 1 z: 1
StyledRect { Row {
width: 28 id: exponentControls
height: 28 height: 28
radius: Theme.cornerRadius spacing: Theme.spacingXS
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) visible: SessionData.getBrightnessExponential(modelData.name)
opacity: SessionData.getBrightnessExponent(modelData.name) > 1.0 ? 1.0 : 0.4
DankIcon { StyledRect {
anchors.centerIn: parent width: 28
name: "remove" height: 28
size: 14 radius: Theme.cornerRadius
color: Theme.surfaceText color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
opacity: SessionData.getBrightnessExponent(modelData.name) > 1.0 ? 1.0 : 0.4
DankIcon {
anchors.centerIn: parent
name: "remove"
size: 14
color: Theme.surfaceText
}
StateLayer {
stateColor: Theme.primary
cornerRadius: parent.radius
enabled: SessionData.getBrightnessExponent(modelData.name) > 1.0
onClicked: {
const current = SessionData.getBrightnessExponent(modelData.name);
const newValue = Math.max(1.0, Math.round((current - 0.1) * 10) / 10);
SessionData.setBrightnessExponent(modelData.name, newValue);
}
}
} }
StateLayer { StyledRect {
stateColor: Theme.primary width: 50
cornerRadius: parent.radius height: 28
enabled: SessionData.getBrightnessExponent(modelData.name) > 1.0 radius: Theme.cornerRadius
onClicked: { color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
const current = SessionData.getBrightnessExponent(modelData.name); border.width: 0
const newValue = Math.max(1.0, Math.round((current - 0.1) * 10) / 10);
SessionData.setBrightnessExponent(modelData.name, newValue); StyledText {
anchors.centerIn: parent
text: SessionData.getBrightnessExponent(modelData.name).toFixed(1)
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.primary
}
}
StyledRect {
width: 28
height: 28
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
opacity: SessionData.getBrightnessExponent(modelData.name) < 2.5 ? 1.0 : 0.4
DankIcon {
anchors.centerIn: parent
name: "add"
size: 14
color: Theme.surfaceText
}
StateLayer {
stateColor: Theme.primary
cornerRadius: parent.radius
enabled: SessionData.getBrightnessExponent(modelData.name) < 2.5
onClicked: {
const current = SessionData.getBrightnessExponent(modelData.name);
const newValue = Math.min(2.5, Math.round((current + 0.1) * 10) / 10);
SessionData.setBrightnessExponent(modelData.name, newValue);
}
} }
} }
} }
StyledRect { StyledRect {
width: 50 id: pinButton
height: 28
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
border.width: 0
StyledText {
anchors.centerIn: parent
text: SessionData.getBrightnessExponent(modelData.name).toFixed(1)
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.primary
}
}
StyledRect {
width: 28 width: 28
height: 28 height: 28
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) visible: root.screenName && root.screenName.length > 0 && DisplayService.devices && DisplayService.devices.length > 1
opacity: SessionData.getBrightnessExponent(modelData.name) < 2.5 ? 1.0 : 0.4 color: devicePinnedHere ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: "add" name: "push_pin"
size: 14 size: 14
color: Theme.surfaceText color: devicePinnedHere ? Theme.primary : Theme.surfaceText
} }
StateLayer { StateLayer {
stateColor: Theme.primary stateColor: Theme.primary
cornerRadius: parent.radius cornerRadius: parent.radius
enabled: SessionData.getBrightnessExponent(modelData.name) < 2.5 onClicked: root.togglePinForDevice(modelData.name)
onClicked: {
const current = SessionData.getBrightnessExponent(modelData.name);
const newValue = Math.min(2.5, Math.round((current + 0.1) * 10) / 10);
SessionData.setBrightnessExponent(modelData.name, newValue);
}
} }
} }
} }
@@ -474,22 +544,11 @@ Rectangle {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
anchors.bottomMargin: 28 anchors.bottomMargin: 28
anchors.rightMargin: SessionData.getBrightnessExponential(modelData.name) ? 145 : 0 anchors.rightMargin: rightControls.width + Theme.spacingS
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onPressed: mouse => deviceRipple.trigger(mouse.x, mouse.y) onPressed: mouse => deviceRipple.trigger(mouse.x, mouse.y)
onClicked: { onClicked: root.selectDevice(modelData.name)
const pinKey = root.getScreenPinKey();
if (pinKey.length > 0 && modelData.name !== currentDeviceName) {
const pins = JSON.parse(JSON.stringify(SettingsData.brightnessDevicePins || {}));
if (pins[pinKey]) {
delete pins[pinKey];
SettingsData.set("brightnessDevicePins", pins);
}
}
currentDeviceName = modelData.name;
deviceNameChanged(modelData.name);
}
} }
} }
} }
@@ -541,7 +541,11 @@ Rectangle {
return -1; return -1;
if (b.ssid === ssid) if (b.ssid === ssid)
return 1; return 1;
return b.signal - a.signal; const aBucket = Math.floor((a.signal || 0) / 25);
const bBucket = Math.floor((b.signal || 0) / 25);
if (aBucket !== bBucket)
return bBucket - aBucket;
return (a.ssid || "").localeCompare(b.ssid || "");
}); });
return sorted; return sorted;
} }
@@ -1,4 +1,5 @@
import QtQuick import QtQuick
import Quickshell
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -31,8 +32,10 @@ Row {
} }
if (screenName && screenName.length > 0) { if (screenName && screenName.length > 0) {
const screen = Quickshell.screens.find(s => s.name === screenName);
const pinKey = screen ? SettingsData.getScreenDisplayName(screen) : screenName;
const pins = SettingsData.brightnessDevicePins || {}; const pins = SettingsData.brightnessDevicePins || {};
const pinnedDevice = pins[screenName]; const pinnedDevice = pins[pinKey];
if (pinnedDevice && pinnedDevice.length > 0) { if (pinnedDevice && pinnedDevice.length > 0) {
const found = DisplayService.devices.find(dev => dev.name === pinnedDevice); const found = DisplayService.devices.find(dev => dev.name === pinnedDevice);
if (found) { if (found) {
@@ -10,6 +10,7 @@ Rectangle {
property string iconName: "" property string iconName: ""
property color iconColor: Theme.surfaceText property color iconColor: Theme.surfaceText
property bool iconBlinking: false
property string primaryText: "" property string primaryText: ""
property string secondaryText: "" property string secondaryText: ""
property bool expanded: false property bool expanded: false
@@ -109,10 +110,16 @@ Rectangle {
} }
DankIcon { DankIcon {
id: pillIcon
anchors.centerIn: parent anchors.centerIn: parent
name: iconName name: iconName
size: Theme.iconSize size: Theme.iconSize
color: isActive ? _tileIconActive : _tileIconInactive color: isActive ? _tileIconActive : _tileIconInactive
DankBlink {
target: pillIcon
running: root.iconBlinking
}
} }
DankRipple { DankRipple {
+42 -11
View File
@@ -10,13 +10,15 @@ Item {
required property var axis required property var axis
required property var barConfig required property var barConfig
visible: !SettingsData.frameEnabled readonly property bool frameShapesBar: SettingsData.frameEnabled && barWindow.usesFrameBarChrome
visible: !frameShapesBar
anchors.fill: parent anchors.fill: parent
anchors.left: parent.left anchors.left: parent.left
anchors.top: parent.top anchors.top: parent.top
readonly property bool gothEnabled: (barConfig?.gothCornersEnabled ?? false) && !barWindow.hasMaximizedToplevel readonly property bool gothEnabled: (barConfig?.gothCornersEnabled ?? false) && !(barWindow.flattenForMaximizedWindow && barWindow.hasMaximizedToplevel)
anchors.leftMargin: -(gothEnabled && axis.isVertical && axis.edge === "right" ? barWindow._wingR : 0) anchors.leftMargin: -(gothEnabled && axis.isVertical && axis.edge === "right" ? barWindow._wingR : 0)
anchors.rightMargin: -(gothEnabled && axis.isVertical && axis.edge === "left" ? barWindow._wingR : 0) anchors.rightMargin: -(gothEnabled && axis.isVertical && axis.edge === "left" ? barWindow._wingR : 0)
anchors.topMargin: -(gothEnabled && !axis.isVertical && axis.edge === "bottom" ? barWindow._wingR : 0) anchors.topMargin: -(gothEnabled && !axis.isVertical && axis.edge === "bottom" ? barWindow._wingR : 0)
@@ -39,11 +41,11 @@ Item {
} }
property real rt: { property real rt: {
if (SettingsData.frameEnabled) if (frameShapesBar)
return SettingsData.frameRounding; return SettingsData.frameRounding;
if (barConfig?.squareCorners ?? false) if (barConfig?.squareCorners ?? false)
return 0; return 0;
if (barWindow.hasMaximizedToplevel) if (barWindow.flattenForMaximizedWindow && barWindow.hasMaximizedToplevel)
return 0; return 0;
return Theme.cornerRadius; return Theme.cornerRadius;
} }
@@ -113,9 +115,32 @@ Item {
readonly property real shadowOffsetX: Theme.elevationOffsetXFor(hasPerBarOverride ? null : elevLevel, effectiveShadowDirection, shadowOffsetMagnitude) readonly property real shadowOffsetX: Theme.elevationOffsetXFor(hasPerBarOverride ? null : elevLevel, effectiveShadowDirection, shadowOffsetMagnitude)
readonly property real shadowOffsetY: Theme.elevationOffsetYFor(hasPerBarOverride ? null : elevLevel, effectiveShadowDirection, shadowOffsetMagnitude) readonly property real shadowOffsetY: Theme.elevationOffsetYFor(hasPerBarOverride ? null : elevLevel, effectiveShadowDirection, shadowOffsetMagnitude)
readonly property string mainPath: generatePathForPosition(width, height) readonly property string mainPath: {
readonly property string borderFullPath: generateBorderFullPath(width, height) frameShapesBar;
readonly property string borderEdgePath: generateBorderEdgePath(width, height) rt;
wing;
barWindow.flattenForMaximizedWindow;
barWindow.hasMaximizedToplevel;
width;
height;
return generatePathForPosition(width, height);
}
readonly property string borderFullPath: {
frameShapesBar;
rt;
wing;
width;
height;
return generateBorderFullPath(width, height);
}
readonly property string borderEdgePath: {
frameShapesBar;
rt;
wing;
width;
height;
return generateBorderEdgePath(width, height);
}
property bool mainPathCorrectShape: false property bool mainPathCorrectShape: false
property bool borderFullPathCorrectShape: false property bool borderFullPathCorrectShape: false
property bool borderEdgePathCorrectShape: false property bool borderEdgePathCorrectShape: false
@@ -136,6 +161,12 @@ Item {
} }
} }
onFrameShapesBarChanged: {
mainPathCorrectShape = false;
borderFullPathCorrectShape = false;
borderEdgePathCorrectShape = false;
}
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
@@ -259,7 +290,7 @@ Item {
h = h - wing; h = h - wing;
const r = wing; const r = wing;
const cr = rt; const cr = rt;
const crE = SettingsData.frameEnabled ? 0 : cr; const crE = frameShapesBar ? 0 : cr;
let d = `M ${crE} 0`; let d = `M ${crE} 0`;
d += ` L ${w - crE} 0`; d += ` L ${w - crE} 0`;
@@ -290,7 +321,7 @@ Item {
h = h - wing; h = h - wing;
const r = wing; const r = wing;
const cr = rt; const cr = rt;
const crE = SettingsData.frameEnabled ? 0 : cr; const crE = frameShapesBar ? 0 : cr;
let d = `M ${crE} ${fullH}`; let d = `M ${crE} ${fullH}`;
d += ` L ${w - crE} ${fullH}`; d += ` L ${w - crE} ${fullH}`;
@@ -320,7 +351,7 @@ Item {
w = w - wing; w = w - wing;
const r = wing; const r = wing;
const cr = rt; const cr = rt;
const crE = SettingsData.frameEnabled ? 0 : cr; const crE = frameShapesBar ? 0 : cr;
let d = `M 0 ${crE}`; let d = `M 0 ${crE}`;
d += ` L 0 ${h - crE}`; d += ` L 0 ${h - crE}`;
@@ -351,7 +382,7 @@ Item {
w = w - wing; w = w - wing;
const r = wing; const r = wing;
const cr = rt; const cr = rt;
const crE = SettingsData.frameEnabled ? 0 : cr; const crE = frameShapesBar ? 0 : cr;
let d = `M ${fullW} ${crE}`; let d = `M ${fullW} ${crE}`;
d += ` L ${fullW} ${h - crE}`; d += ` L ${fullW} ${h - crE}`;
+10 -9
View File
@@ -24,8 +24,9 @@ Item {
readonly property real innerPadding: barConfig?.innerPadding ?? 4 readonly property real innerPadding: barConfig?.innerPadding ?? 4
readonly property real outlineThickness: (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0 readonly property real outlineThickness: (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0
readonly property real _edgeBaseMargin: Math.max(Theme.spacingXS, innerPadding * 0.8) readonly property real _edgeBaseMargin: Math.max(Theme.spacingXS, innerPadding * 0.8)
readonly property real _frameEdgeFloorInset: SettingsData.frameEnabled ? Math.max(0, SettingsData.frameThickness - _edgeBaseMargin) : 0
readonly property bool _hasBarWindow: barWindow !== undefined && barWindow !== null readonly property bool _hasBarWindow: barWindow !== undefined && barWindow !== null
readonly property bool _usesFrameBarChrome: _hasBarWindow && (barWindow.usesFrameBarChrome ?? false)
readonly property real _frameEdgeFloorInset: (SettingsData.frameEnabled && _usesFrameBarChrome) ? Math.max(0, SettingsData.frameThickness - _edgeBaseMargin) : 0
readonly property bool _barIsVertical: _hasBarWindow ? barWindow.isVertical : false readonly property bool _barIsVertical: _hasBarWindow ? barWindow.isVertical : false
readonly property string _barScreenName: _hasBarWindow ? (barWindow.screenName || "") : "" readonly property string _barScreenName: _hasBarWindow ? (barWindow.screenName || "") : ""
readonly property bool hasAdjacentTopBarLive: _hasBarWindow && barWindow.hasAdjacentTopBar readonly property bool hasAdjacentTopBarLive: _hasBarWindow && barWindow.hasAdjacentTopBar
@@ -47,22 +48,22 @@ Item {
_hadAdjacentRightBar = true _hadAdjacentRightBar = true
readonly property real _frameLeftInset: { readonly property real _frameLeftInset: {
if (!_hasBarWindow || !SettingsData.frameEnabled || _barIsVertical) if (!_hasBarWindow || !SettingsData.frameEnabled || !_usesFrameBarChrome || _barIsVertical)
return 0; return 0;
return hasAdjacentLeftBarLive ? SettingsData.frameBarSize : (_hadAdjacentLeftBar ? _frameEdgeFloorInset : 0); return hasAdjacentLeftBarLive ? SettingsData.frameBarSize : (_hadAdjacentLeftBar ? _frameEdgeFloorInset : 0);
} }
readonly property real _frameRightInset: { readonly property real _frameRightInset: {
if (!_hasBarWindow || !SettingsData.frameEnabled || _barIsVertical) if (!_hasBarWindow || !SettingsData.frameEnabled || !_usesFrameBarChrome || _barIsVertical)
return 0; return 0;
return hasAdjacentRightBarLive ? SettingsData.frameBarSize : (_hadAdjacentRightBar ? _frameEdgeFloorInset : 0); return hasAdjacentRightBarLive ? SettingsData.frameBarSize : (_hadAdjacentRightBar ? _frameEdgeFloorInset : 0);
} }
readonly property real _frameTopInset: { readonly property real _frameTopInset: {
if (!_hasBarWindow || !SettingsData.frameEnabled || !_barIsVertical) if (!_hasBarWindow || !SettingsData.frameEnabled || !_usesFrameBarChrome || !_barIsVertical)
return 0; return 0;
return hasAdjacentTopBarLive ? SettingsData.frameThickness : (_hadAdjacentTopBar ? _frameEdgeFloorInset : 0); return hasAdjacentTopBarLive ? SettingsData.frameThickness : (_hadAdjacentTopBar ? _frameEdgeFloorInset : 0);
} }
readonly property real _frameBottomInset: { readonly property real _frameBottomInset: {
if (!_hasBarWindow || !SettingsData.frameEnabled || !_barIsVertical) if (!_hasBarWindow || !SettingsData.frameEnabled || !_usesFrameBarChrome || !_barIsVertical)
return 0; return 0;
return hasAdjacentBottomBarLive ? SettingsData.frameThickness : (_hadAdjacentBottomBar ? _frameEdgeFloorInset : 0); return hasAdjacentBottomBarLive ? SettingsData.frameThickness : (_hadAdjacentBottomBar ? _frameEdgeFloorInset : 0);
} }
@@ -95,7 +96,7 @@ Item {
} }
Behavior on anchors.leftMargin { Behavior on anchors.leftMargin {
enabled: _animateFrameInsets && SettingsData.frameEnabled enabled: _animateFrameInsets && _usesFrameBarChrome
NumberAnimation { NumberAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Easing.OutCubic easing.type: Easing.OutCubic
@@ -103,7 +104,7 @@ Item {
} }
Behavior on anchors.rightMargin { Behavior on anchors.rightMargin {
enabled: _animateFrameInsets && SettingsData.frameEnabled enabled: _animateFrameInsets && _usesFrameBarChrome
NumberAnimation { NumberAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Easing.OutCubic easing.type: Easing.OutCubic
@@ -111,7 +112,7 @@ Item {
} }
Behavior on anchors.topMargin { Behavior on anchors.topMargin {
enabled: _animateFrameInsets && SettingsData.frameEnabled enabled: _animateFrameInsets && _usesFrameBarChrome
NumberAnimation { NumberAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Easing.OutCubic easing.type: Easing.OutCubic
@@ -119,7 +120,7 @@ Item {
} }
Behavior on anchors.bottomMargin { Behavior on anchors.bottomMargin {
enabled: _animateFrameInsets && SettingsData.frameEnabled enabled: _animateFrameInsets && _usesFrameBarChrome
NumberAnimation { NumberAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Easing.OutCubic easing.type: Easing.OutCubic
+33 -29
View File
@@ -108,6 +108,8 @@ PanelWindow {
triggerDashTab(2); triggerDashTab(2);
} }
readonly property bool usesOverlayLayer: CompositorService.framePeerSurfacesUseOverlayForScreen(barWindow.screen) || (barConfig?.useOverlayLayer ?? false)
readonly property var dBarLayer: { readonly property var dBarLayer: {
switch (Quickshell.env("DMS_DANKBAR_LAYER")) { switch (Quickshell.env("DMS_DANKBAR_LAYER")) {
case "bottom": case "bottom":
@@ -119,10 +121,7 @@ PanelWindow {
case "top": case "top":
return WlrLayer.Top; return WlrLayer.Top;
default: default:
// Elevate to Overlay when Frame is enabled so the bar stays above return barWindow.usesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top;
// the FrameWindow (WlrLayer.Top) when it is re-mapped on mode switch,
// but drop back to Top while a true fullscreen app owns this screen.
return SettingsData.frameEnabled && !barWindow.hasFullscreenToplevel ? WlrLayer.Overlay : WlrLayer.Top;
} }
} }
@@ -152,6 +151,16 @@ PanelWindow {
onTriggered: barBlur.rebuild() onTriggered: barBlur.rebuild()
} }
Connections {
target: barWindow
function onUsesConnectedFrameChromeChanged() {
_blurRebuildTimer.restart();
}
function onUsesFrameBarChromeChanged() {
_blurRebuildTimer.restart();
}
}
Component { Component {
id: blurRegionComp id: blurRegionComp
Region {} Region {}
@@ -179,7 +188,7 @@ PanelWindow {
// In frame mode, FrameWindow owns the blur region for the entire screen edge // 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 // (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. // so that frameBlurEnabled acts as the single control for all blur in frame mode.
if (SettingsData.frameEnabled) if (SettingsData.frameEnabled && barWindow.usesFrameBarChrome)
return; return;
const widgets = barWindow._blurWidgetItems.filter(w => w && w.visible && w.width > 0 && w.height > 0); const widgets = barWindow._blurWidgetItems.filter(w => w && w.visible && w.width > 0 && w.height > 0);
@@ -292,7 +301,7 @@ PanelWindow {
readonly property color _surfaceContainer: Theme.surfaceContainer readonly property color _surfaceContainer: Theme.surfaceContainer
readonly property string _barId: barConfig?.id ?? "default" readonly property string _barId: barConfig?.id ?? "default"
property real _backgroundAlpha: barConfig?.transparency ?? 1.0 property real _backgroundAlpha: barConfig?.transparency ?? 1.0
readonly property color _bgColor: SettingsData.frameEnabled ? Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity) : Theme.withAlpha(_surfaceContainer, _backgroundAlpha) readonly property color _bgColor: (SettingsData.frameEnabled && usesFrameBarChrome) ? Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity) : Theme.withAlpha(_surfaceContainer, _backgroundAlpha)
function _updateBackgroundAlpha() { function _updateBackgroundAlpha() {
const live = SettingsData.barConfigs.find(c => c.id === _barId); const live = SettingsData.barConfigs.find(c => c.id === _barId);
@@ -316,16 +325,14 @@ PanelWindow {
property string screenName: modelData.name property string screenName: modelData.name
readonly property bool usesConnectedFrameChrome: CompositorService.usesConnectedFrameChromeForScreen(screenName)
readonly property bool usesFrameBarChrome: CompositorService.frameWindowVisibleForScreen(screenName)
// Flatten/spacing collapse for maximized windows is only for frame-integrated layout.
// When the bar draws its own pill, keep rounded corners and spacing like the dock.
readonly property bool flattenForMaximizedWindow: !SettingsData.frameEnabled || usesFrameBarChrome
property bool hasMaximizedToplevel: false property bool hasMaximizedToplevel: false
readonly property bool hasFullscreenToplevel: {
if (!(barConfig?.fullscreenDetection ?? true))
return false;
CompositorService.sortedToplevels;
ToplevelManager.activeToplevel;
if (CompositorService.isNiri)
NiriService.allWorkspaces;
return CompositorService.hasFullscreenToplevelOnScreen(screenName);
}
property bool shouldHideForWindows: false property bool shouldHideForWindows: false
function _updateHasMaximizedToplevel() { function _updateHasMaximizedToplevel() {
@@ -427,7 +434,7 @@ PanelWindow {
shouldHideForWindows = filtered.length > 0; shouldHideForWindows = filtered.length > 0;
} }
property real effectiveSpacing: SettingsData.frameEnabled ? 0 : (hasMaximizedToplevel ? 0 : (barConfig?.spacing ?? 4)) property real effectiveSpacing: (SettingsData.frameEnabled && usesFrameBarChrome) ? 0 : ((flattenForMaximizedWindow && hasMaximizedToplevel) ? 0 : (barConfig?.spacing ?? 4))
Behavior on effectiveSpacing { Behavior on effectiveSpacing {
enabled: barWindow.visible enabled: barWindow.visible
@@ -438,7 +445,7 @@ PanelWindow {
} }
readonly property int notificationCount: NotificationService.notifications.length readonly property int notificationCount: NotificationService.notifications.length
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 real effectiveBarThickness: (SettingsData.frameEnabled && usesFrameBarChrome) ? 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 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 real widgetThickness: Theme.snap(Math.max(20, 26 + (barConfig?.innerPadding ?? 4) * 0.6), _dpr)
@@ -636,9 +643,9 @@ PanelWindow {
anchors.left: !isVertical ? true : (barPos === SettingsData.Position.Left) anchors.left: !isVertical ? true : (barPos === SettingsData.Position.Left)
anchors.right: !isVertical ? true : (barPos === SettingsData.Position.Right) anchors.right: !isVertical ? true : (barPos === SettingsData.Position.Right)
readonly property bool reserveExclusiveWhenAutoHidden: SettingsData.connectedFrameModeActive && !!barWindow.screen && SettingsData.isScreenInPreferences(barWindow.screen, SettingsData.frameScreenPreferences) readonly property bool reserveExclusiveWhenAutoHidden: SettingsData.frameEnabled && usesFrameBarChrome && !!barWindow.screen && SettingsData.isScreenInPreferences(barWindow.screen, SettingsData.frameScreenPreferences)
exclusiveZone: (barWindow.hasFullscreenToplevel || !(barConfig?.visible ?? true) || (topBarCore.autoHide && !barWindow.reserveExclusiveWhenAutoHidden)) ? -1 : (barWindow.effectiveBarThickness + effectiveSpacing + (Theme.isConnectedEffect ? 0 : (barConfig?.bottomGap ?? 0))) exclusiveZone: (!(barConfig?.visible ?? true) || (topBarCore.autoHide && !barWindow.reserveExclusiveWhenAutoHidden)) ? -1 : (barWindow.effectiveBarThickness + effectiveSpacing + (usesFrameBarChrome ? 0 : (barConfig?.bottomGap ?? 0)))
Item { Item {
id: inputMask id: inputMask
@@ -647,9 +654,9 @@ PanelWindow {
readonly property bool inOverviewWithShow: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview readonly property bool inOverviewWithShow: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview
readonly property bool effectiveVisible: (barConfig?.visible ?? true) || inOverviewWithShow readonly property bool effectiveVisible: (barConfig?.visible ?? true) || inOverviewWithShow
readonly property bool showing: effectiveVisible && !barWindow.hasFullscreenToplevel && (topBarCore.reveal || inOverviewWithShow || !topBarCore.autoHide) readonly property bool showing: effectiveVisible && (topBarCore.reveal || inOverviewWithShow || !topBarCore.autoHide)
readonly property int maskThickness: barWindow.hasFullscreenToplevel ? 0 : (showing ? barThickness : 1) readonly property int maskThickness: showing ? barThickness : 1
x: { x: {
if (!axis.isVertical) { if (!axis.isVertical) {
@@ -719,7 +726,7 @@ PanelWindow {
item: clickThroughEnabled ? null : inputMask item: clickThroughEnabled ? null : inputMask
Region { Region {
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._leftSection, false, barWindow._revealProgress) : { readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._leftSection, false, barWindow._revealProgress + barWindow.width * 0) : {
"x": 0, "x": 0,
"y": 0, "y": 0,
"w": 0, "w": 0,
@@ -732,7 +739,7 @@ PanelWindow {
} }
Region { Region {
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._centerSection, true, barWindow._revealProgress) : { readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._centerSection, true, barWindow._revealProgress + barWindow.width * 0) : {
"x": 0, "x": 0,
"y": 0, "y": 0,
"w": 0, "w": 0,
@@ -745,7 +752,7 @@ PanelWindow {
} }
Region { Region {
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._rightSection, false, barWindow._revealProgress) : { readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._rightSection, false, barWindow._revealProgress + barWindow.width * 0) : {
"x": 0, "x": 0,
"y": 0, "y": 0,
"w": 0, "w": 0,
@@ -826,9 +833,6 @@ PanelWindow {
} }
property bool reveal: { property bool reveal: {
if (barWindow.hasFullscreenToplevel)
return false;
const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview; const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview;
if (inOverviewWithShow) if (inOverviewWithShow)
return true; return true;
@@ -897,9 +901,9 @@ PanelWindow {
bottom: barWindow.isVertical ? parent.bottom : undefined bottom: barWindow.isVertical ? parent.bottom : undefined
} }
readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview
hoverEnabled: (barConfig?.autoHide ?? false) && !inOverview && !barWindow.hasFullscreenToplevel && !topBarCore.popoutPinsReveal hoverEnabled: (barConfig?.autoHide ?? false) && !inOverview && !topBarCore.popoutPinsReveal
acceptedButtons: Qt.NoButton acceptedButtons: Qt.NoButton
enabled: (barConfig?.autoHide ?? false) && !inOverview && !barWindow.hasFullscreenToplevel enabled: (barConfig?.autoHide ?? false) && !inOverview
Item { Item {
id: topBarContainer id: topBarContainer
@@ -131,9 +131,19 @@ BasePill {
function getNetworkIconColor() { function getNetworkIconColor() {
if (NetworkService.wifiToggling) if (NetworkService.wifiToggling)
return Theme.primary; return Theme.primary;
if (NetworkService.isConnecting && !NetworkService.ethernetConnected)
return Theme.primary;
return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.surfaceText; return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.surfaceText;
} }
function getIconBlinking(id) {
if (id === "network")
return NetworkService.isWifiConnecting;
if (id === "bluetooth")
return BluetoothService.connecting;
return false;
}
function getVolumeIconName() { function getVolumeIconName() {
if (!AudioService.sink?.audio) if (!AudioService.sink?.audio)
return "volume_up"; return "volume_up";
@@ -485,6 +495,7 @@ BasePill {
} }
DankIcon { DankIcon {
id: vIconOnlyItem
anchors.centerIn: parent anchors.centerIn: parent
visible: !verticalGroupItem.modelData.composite visible: !verticalGroupItem.modelData.composite
name: { name: {
@@ -515,7 +526,7 @@ BasePill {
case "vpn": case "vpn":
return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText; return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText;
case "bluetooth": case "bluetooth":
return BluetoothService.connected ? Theme.primary : Theme.surfaceText; return (BluetoothService.connected || BluetoothService.connecting) ? Theme.primary : Theme.surfaceText;
case "battery": case "battery":
return root.getBatteryIconColor(); return root.getBatteryIconColor();
case "printer": case "printer":
@@ -524,6 +535,11 @@ BasePill {
return Theme.widgetIconColor; return Theme.widgetIconColor;
} }
} }
DankBlink {
target: vIconOnlyItem
running: root.getIconBlinking(verticalGroupItem.modelData.id)
}
} }
DankIcon { DankIcon {
@@ -687,7 +703,7 @@ BasePill {
case "vpn": case "vpn":
return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText; return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText;
case "bluetooth": case "bluetooth":
return BluetoothService.connected ? Theme.primary : Theme.surfaceText; return (BluetoothService.connected || BluetoothService.connecting) ? Theme.primary : Theme.surfaceText;
case "battery": case "battery":
return root.getBatteryIconColor(); return root.getBatteryIconColor();
case "printer": case "printer":
@@ -696,6 +712,11 @@ BasePill {
return Theme.widgetIconColor; return Theme.widgetIconColor;
} }
} }
DankBlink {
target: iconOnlyItem
running: root.getIconBlinking(horizontalGroupItem.modelData.id)
}
} }
Rectangle { Rectangle {
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Effects import QtQuick.Effects
import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Widgets import Quickshell.Widgets
@@ -14,9 +15,20 @@ BasePill {
property var widgetData: null property var widgetData: null
property bool compactMode: widgetData?.focusedWindowCompactMode !== undefined ? widgetData.focusedWindowCompactMode : SettingsData.focusedWindowCompactMode property bool compactMode: widgetData?.focusedWindowCompactMode !== undefined ? widgetData.focusedWindowCompactMode : SettingsData.focusedWindowCompactMode
property int availableWidth: 400 readonly property int maxWidth: {
readonly property int maxNormalWidth: 456 const size = widgetData?.focusedWindowSize !== undefined ? widgetData.focusedWindowSize : SettingsData.focusedWindowSize;
readonly property int maxCompactWidth: 288 switch (size) {
case 0:
return 288;
case 2:
return 656;
case 3:
return 856;
default:
return 456;
}
}
property int availableWidth: maxWidth
property Toplevel activeWindow: null property Toplevel activeWindow: null
property var activeDesktopEntry: null property var activeDesktopEntry: null
property bool isHovered: mouseArea.containsMouse property bool isHovered: mouseArea.containsMouse
@@ -171,8 +183,7 @@ BasePill {
return 0; return 0;
if (root.isVerticalOrientation) if (root.isVerticalOrientation)
return root.widgetThickness - root.horizontalPadding * 2; return root.widgetThickness - root.horizontalPadding * 2;
const baseWidth = contentRow.implicitWidth; return contentRow.implicitWidth;
return compactMode ? Math.min(baseWidth, maxCompactWidth - root.horizontalPadding * 2) : Math.min(baseWidth, maxNormalWidth - root.horizontalPadding * 2);
} }
implicitHeight: root.widgetThickness - root.horizontalPadding * 2 implicitHeight: root.widgetThickness - root.horizontalPadding * 2
clip: false clip: false
@@ -222,7 +233,7 @@ BasePill {
color: Theme.widgetTextColor color: Theme.widgetTextColor
} }
Row { RowLayout {
id: contentRow id: contentRow
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingS spacing: Theme.spacingS
@@ -231,24 +242,23 @@ BasePill {
StyledText { StyledText {
id: appText id: appText
text: { text: {
if (!activeWindow || !activeWindow.appId) if (compactMode || !activeWindow || !activeWindow.appId)
return ""; return "";
return Paths.getAppName(activeWindow.appId, activeDesktopEntry); return Paths.getAppName(activeWindow.appId, activeDesktopEntry);
} }
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight elide: Text.ElideRight
maximumLineCount: 1 maximumLineCount: 1
width: Math.min(implicitWidth, compactMode ? 80 : 180) Layout.maximumWidth: compactMode ? 80 : 180
visible: !compactMode && text.length > 0 visible: text.length > 0
} }
StyledText { StyledText {
text: "•" id: appSeparator
text: compactMode ? "" : "•"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.outlineButton color: Theme.outlineButton
anchors.verticalCenter: parent.verticalCenter
visible: !compactMode && appText.text && titleText.text visible: !compactMode && appText.text && titleText.text
} }
@@ -276,10 +286,9 @@ BasePill {
} }
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight elide: Text.ElideRight
maximumLineCount: 1 maximumLineCount: 1
width: Math.min(implicitWidth, compactMode ? 280 : 250) Layout.maximumWidth: maxWidth - appText.implicitWidth - appSeparator.implicitWidth
visible: text.length > 0 visible: text.length > 0
} }
} }
@@ -11,13 +11,14 @@ BasePill {
id: root id: root
readonly property string focusedScreenName: (CompositorService.isHyprland && typeof Hyprland !== "undefined" && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor ? (Hyprland.focusedWorkspace.monitor.name || "") : CompositorService.isNiri && typeof NiriService !== "undefined" && NiriService.currentOutput ? NiriService.currentOutput : "") readonly property string focusedScreenName: (CompositorService.isHyprland && typeof Hyprland !== "undefined" && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor ? (Hyprland.focusedWorkspace.monitor.name || "") : CompositorService.isNiri && typeof NiriService !== "undefined" && NiriService.currentOutput ? NiriService.currentOutput : "")
readonly property string targetScreenName: parentScreen?.name || focusedScreenName
function resolveNotepadInstance() { function resolveNotepadInstance() {
if (typeof notepadSlideoutVariants === "undefined" || !notepadSlideoutVariants || !notepadSlideoutVariants.instances) { if (typeof notepadSlideoutVariants === "undefined" || !notepadSlideoutVariants || !notepadSlideoutVariants.instances) {
return null; return null;
} }
const targetScreen = focusedScreenName; const targetScreen = targetScreenName;
if (targetScreen) { if (targetScreen) {
for (var i = 0; i < notepadSlideoutVariants.instances.length; i++) { for (var i = 0; i < notepadSlideoutVariants.instances.length; i++) {
var slideout = notepadSlideoutVariants.instances[i]; var slideout = notepadSlideoutVariants.instances[i];
@@ -34,6 +35,12 @@ BasePill {
readonly property bool isActive: notepadInstance?.isVisible ?? false readonly property bool isActive: notepadInstance?.isVisible ?? false
property bool isAutoHideBar: false property bool isAutoHideBar: false
function prepareNotepadInstance(instance) {
if (instance)
instance.triggerUsesOverlayLayer = root.barUsesOverlayLayer;
return instance;
}
readonly property real minTooltipY: { readonly property real minTooltipY: {
if (!parentScreen || !(axis?.isVertical ?? false)) { if (!parentScreen || !(axis?.isVertical ?? false)) {
return 0; return 0;
@@ -68,8 +75,9 @@ BasePill {
function openTabByIndex(tabIndex) { function openTabByIndex(tabIndex) {
if (tabIndex < 0) if (tabIndex < 0)
return; return;
if (root.notepadInstance && typeof root.notepadInstance.show === "function") { const instance = prepareNotepadInstance(root.notepadInstance);
root.notepadInstance.show(); if (instance && typeof instance.show === "function") {
instance.show();
} }
Qt.callLater(() => { Qt.callLater(() => {
NotepadStorageService.switchToTab(tabIndex); NotepadStorageService.switchToTab(tabIndex);
@@ -77,8 +85,9 @@ BasePill {
} }
function openNewNote() { function openNewNote() {
if (root.notepadInstance && typeof root.notepadInstance.show === "function") { const instance = prepareNotepadInstance(root.notepadInstance);
root.notepadInstance.show(); if (instance && typeof instance.show === "function") {
instance.show();
} }
Qt.callLater(() => { Qt.callLater(() => {
NotepadStorageService.createNewTab(); NotepadStorageService.createNewTab();
@@ -138,7 +147,7 @@ BasePill {
openContextMenu(); openContextMenu();
return; return;
} }
const inst = root.notepadInstance; const inst = prepareNotepadInstance(root.notepadInstance);
if (inst) { if (inst) {
inst.toggle(); inst.toggle();
} }
@@ -978,7 +978,7 @@ BasePill {
visible: root.useOverflowPopup && root.menuOpen visible: root.useOverflowPopup && root.menuOpen
screen: root.parentScreen screen: root.parentScreen
WlrLayershell.layer: WlrLayershell.Top WlrLayershell.layer: root.barUsesOverlayLayer ? WlrLayershell.Overlay : WlrLayershell.Top
WlrLayershell.exclusiveZone: -1 WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: { WlrLayershell.keyboardFocus: {
if (!root.menuOpen) if (!root.menuOpen)
@@ -1446,7 +1446,7 @@ BasePill {
WlrLayershell.namespace: "dms:tray-menu-window" WlrLayershell.namespace: "dms:tray-menu-window"
visible: menuRoot.showMenu && (menuRoot.trayItem?.hasMenu ?? false) visible: menuRoot.showMenu && (menuRoot.trayItem?.hasMenu ?? false)
screen: menuRoot.parentScreen screen: menuRoot.parentScreen
WlrLayershell.layer: WlrLayershell.Top WlrLayershell.layer: root.barUsesOverlayLayer ? WlrLayershell.Overlay : WlrLayershell.Top
WlrLayershell.exclusiveZone: -1 WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: { WlrLayershell.keyboardFocus: {
if (!menuRoot.showMenu) if (!menuRoot.showMenu)
+36 -106
View File
@@ -20,16 +20,16 @@ Variants {
WindowBlur { WindowBlur {
targetWindow: dock targetWindow: dock
blurEnabled: dock.effectiveBlurEnabled && !SettingsData.connectedFrameModeActive blurEnabled: dock.effectiveBlurEnabled && !dock.usesConnectedFrameChrome
blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x
blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y
blurWidth: dock.hasApps && dock.reveal ? dockBackground.width : 0 blurWidth: dock.hasApps && dock.reveal ? dockBackground.width : 0
blurHeight: dock.hasApps && dock.reveal ? dockBackground.height : 0 blurHeight: dock.hasApps && dock.reveal ? dockBackground.height : 0
blurRadius: Theme.isConnectedEffect ? Theme.connectedCornerRadius : dock.surfaceRadius blurRadius: dock.usesConnectedFrameChrome ? Theme.connectedCornerRadius : dock.surfaceRadius
} }
WlrLayershell.namespace: "dms:dock" WlrLayershell.namespace: "dms:dock"
WlrLayershell.layer: SettingsData.frameEnabled && !dock.hasFullscreenToplevel ? WlrLayer.Overlay : WlrLayer.Top WlrLayershell.layer: dock.usesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top
readonly property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right readonly property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
@@ -50,16 +50,16 @@ Variants {
readonly property bool connectedBarActiveOnEdge: dockGeometry.connectedBarActiveOnEdge readonly property bool connectedBarActiveOnEdge: dockGeometry.connectedBarActiveOnEdge
readonly property real connectedJoinInset: dockGeometry.connectedJoinInset readonly property real connectedJoinInset: dockGeometry.connectedJoinInset
readonly property real dockFrameInset: dockGeometry.frameInset readonly property real dockFrameInset: dockGeometry.frameInset
readonly property real surfaceRadius: Theme.connectedSurfaceRadius readonly property real surfaceRadius: usesConnectedFrameChrome ? Theme.connectedSurfaceRadius : Theme.cornerRadius
readonly property color surfaceColor: Theme.isConnectedEffect ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency) readonly property color surfaceColor: usesConnectedFrameChrome ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency)
readonly property color surfaceBorderColor: Theme.isConnectedEffect ? "transparent" : BlurService.borderColor readonly property color surfaceBorderColor: usesConnectedFrameChrome ? "transparent" : BlurService.borderColor
readonly property real surfaceBorderWidth: Theme.isConnectedEffect ? 0 : BlurService.borderWidth readonly property real surfaceBorderWidth: usesConnectedFrameChrome ? 0 : BlurService.borderWidth
readonly property real surfaceTopLeftRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius readonly property real surfaceTopLeftRadius: usesConnectedFrameChrome && (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 surfaceTopRightRadius: usesConnectedFrameChrome && (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 surfaceBottomLeftRadius: usesConnectedFrameChrome && (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 surfaceBottomRightRadius: usesConnectedFrameChrome && (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 horizontalConnectorExtent: usesConnectedFrameChrome && !isVertical ? Theme.connectedCornerRadius : 0
readonly property real verticalConnectorExtent: Theme.isConnectedEffect && isVertical ? Theme.connectedCornerRadius : 0 readonly property real verticalConnectorExtent: usesConnectedFrameChrome && isVertical ? Theme.connectedCornerRadius : 0
readonly property int hasApps: dockApps.implicitWidth > 0 || dockApps.implicitHeight > 0 readonly property int hasApps: dockApps.implicitWidth > 0 || dockApps.implicitHeight > 0
@@ -149,7 +149,6 @@ Variants {
edge: dock.connectedBarSide edge: dock.connectedBarSide
dockVisible: dock.visible dockVisible: dock.visible
autoHide: dock.autoHide autoHide: dock.autoHide
hasFullscreenToplevel: dock.hasFullscreenToplevel
iconSize: dock.widgetHeight iconSize: dock.widgetHeight
spacing: SettingsData.dockSpacing spacing: SettingsData.dockSpacing
borderThickness: dock.borderThickness borderThickness: dock.borderThickness
@@ -176,25 +175,13 @@ Variants {
} }
readonly property string _dockScreenName: dock.modelData ? dock.modelData.name : (dock.screen ? dock.screen.name : "") readonly property string _dockScreenName: dock.modelData ? dock.modelData.name : (dock.screen ? dock.screen.name : "")
readonly property bool hasFullscreenToplevel: { readonly property bool usesConnectedFrameChrome: CompositorService.usesConnectedFrameChromeForScreen(dock._dockScreenName)
if (!SettingsData.dockHideOnFullscreen) readonly property bool usesOverlayLayer: CompositorService.framePeerSurfacesUseOverlayForScreen(dock._dockScreenName) || SettingsData.dockUseOverlayLayer
return false;
CompositorService.sortedToplevels;
ToplevelManager.activeToplevel;
if (CompositorService.isNiri) {
NiriService.currentOutput;
NiriService.windows;
NiriService.allWorkspaces;
}
if (CompositorService.isHyprland)
Hyprland.focusedWorkspace;
return CompositorService.hasFullscreenToplevelOnScreen(dock._dockScreenName);
}
function _syncDockChromeState() { function _syncDockChromeState() {
if (!dock._dockScreenName) if (!dock._dockScreenName)
return; return;
if (!SettingsData.connectedFrameModeActive) { if (!dock.usesConnectedFrameChrome) {
ConnectedModeState.clearDockState(dock._dockScreenName); ConnectedModeState.clearDockState(dock._dockScreenName);
return; return;
} }
@@ -212,19 +199,19 @@ Variants {
} }
function _syncDockSlide() { function _syncDockSlide() {
if (!dock._dockScreenName || !SettingsData.connectedFrameModeActive) if (!dock._dockScreenName || !dock.usesConnectedFrameChrome)
return; return;
ConnectedModeState.setDockSlide(dock._dockScreenName, dockSlide.x, dockSlide.y); ConnectedModeState.setDockSlide(dock._dockScreenName, dockSlide.x, dockSlide.y);
} }
DeferredAction { DeferredAction {
id: dockSlideSync id: dockSlideSync
enabled: SettingsData.connectedFrameModeActive enabled: dock.usesConnectedFrameChrome
onTriggered: dock._syncDockSlide() onTriggered: dock._syncDockSlide()
} }
function _queueSlideSync() { function _queueSlideSync() {
if (!SettingsData.connectedFrameModeActive) if (!dock.usesConnectedFrameChrome)
return; return;
dockSlideSync.schedule(); dockSlideSync.schedule();
} }
@@ -304,65 +291,10 @@ Variants {
return false; return false;
} }
// Hyprland implementation // Hyprland implementation (current workspace + visible special workspaces)
Hyprland.focusedWorkspace; Hyprland.focusedWorkspace;
const filtered = CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName); Hyprland.toplevels;
return CompositorService.hyprlandDockOverlapForSmartAutoHide(screenName, SettingsData.dockPosition, dockThickness, screenWidth, screenHeight);
if (filtered.length === 0)
return false;
for (let i = 0; i < filtered.length; i++) {
const toplevel = filtered[i];
let hyprToplevel = null;
if (Hyprland.toplevels) {
const hyprToplevels = Array.from(Hyprland.toplevels.values);
for (let j = 0; j < hyprToplevels.length; j++) {
if (hyprToplevels[j].wayland === toplevel) {
hyprToplevel = hyprToplevels[j];
break;
}
}
}
if (!hyprToplevel?.lastIpcObject)
continue;
const ipc = hyprToplevel.lastIpcObject;
const at = ipc.at;
const size = ipc.size;
if (!at || !size)
continue;
const monX = hyprToplevel.monitor?.x ?? 0;
const monY = hyprToplevel.monitor?.y ?? 0;
const winX = at[0] - monX;
const winY = at[1] - monY;
const winW = size[0];
const winH = size[1];
switch (SettingsData.dockPosition) {
case SettingsData.Position.Top:
if (winY < dockThickness)
return true;
break;
case SettingsData.Position.Bottom:
if (winY + winH > screenHeight - dockThickness)
return true;
break;
case SettingsData.Position.Left:
if (winX < dockThickness)
return true;
break;
case SettingsData.Position.Right:
if (winX + winW > screenWidth - dockThickness)
return true;
break;
}
}
return false;
} }
Timer { Timer {
@@ -383,9 +315,6 @@ Variants {
if (_modalRetractActive) if (_modalRetractActive)
return false; return false;
if (dock.hasFullscreenToplevel)
return false;
if (CompositorService.isNiri && NiriService.inOverview && SettingsData.dockOpenOnOverview) { if (CompositorService.isNiri && NiriService.inOverview && SettingsData.dockOpenOnOverview) {
return true; return true;
} }
@@ -421,7 +350,7 @@ Variants {
onVisibleChanged: dock._syncDockChromeState() onVisibleChanged: dock._syncDockChromeState()
onHasAppsChanged: dock._syncDockChromeState() onHasAppsChanged: dock._syncDockChromeState()
onConnectedBarSideChanged: dock._syncDockChromeState() onConnectedBarSideChanged: dock._syncDockChromeState()
onHasFullscreenToplevelChanged: dock._syncDockChromeState() onUsesConnectedFrameChromeChanged: dock._syncDockChromeState()
Connections { Connections {
target: SettingsData target: SettingsData
@@ -680,7 +609,7 @@ Variants {
return 0; return 0;
if (dock.reveal) if (dock.reveal)
return 0; return 0;
if (Theme.isConnectedEffect) { if (dock.usesConnectedFrameChrome) {
const retractDist = dockBackground.width + SettingsData.dockSpacing + 10; const retractDist = dockBackground.width + SettingsData.dockSpacing + 10;
return SettingsData.dockPosition === SettingsData.Position.Right ? retractDist : -retractDist; return SettingsData.dockPosition === SettingsData.Position.Right ? retractDist : -retractDist;
} }
@@ -696,7 +625,7 @@ Variants {
return 0; return 0;
if (dock.reveal) if (dock.reveal)
return 0; return 0;
if (Theme.isConnectedEffect) { if (dock.usesConnectedFrameChrome) {
const retractDist = dockBackground.height + SettingsData.dockSpacing + 10; const retractDist = dockBackground.height + SettingsData.dockSpacing + 10;
return SettingsData.dockPosition === SettingsData.Position.Bottom ? retractDist : -retractDist; return SettingsData.dockPosition === SettingsData.Position.Bottom ? retractDist : -retractDist;
} }
@@ -711,9 +640,9 @@ Variants {
Behavior on x { Behavior on x {
NumberAnimation { NumberAnimation {
id: slideXAnimation id: slideXAnimation
duration: Theme.isConnectedEffect ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration duration: dock.usesConnectedFrameChrome ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration
easing.type: Theme.isConnectedEffect ? Easing.BezierSpline : Easing.OutCubic easing.type: dock.usesConnectedFrameChrome ? Easing.BezierSpline : Easing.OutCubic
easing.bezierCurve: Theme.isConnectedEffect ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : [] easing.bezierCurve: dock.usesConnectedFrameChrome ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : []
onRunningChanged: if (!running) onRunningChanged: if (!running)
dock._syncDockChromeState() dock._syncDockChromeState()
} }
@@ -722,9 +651,9 @@ Variants {
Behavior on y { Behavior on y {
NumberAnimation { NumberAnimation {
id: slideYAnimation id: slideYAnimation
duration: Theme.isConnectedEffect ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration duration: dock.usesConnectedFrameChrome ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration
easing.type: Theme.isConnectedEffect ? Easing.BezierSpline : Easing.OutCubic easing.type: dock.usesConnectedFrameChrome ? Easing.BezierSpline : Easing.OutCubic
easing.bezierCurve: Theme.isConnectedEffect ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : [] easing.bezierCurve: dock.usesConnectedFrameChrome ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : []
onRunningChanged: if (!running) onRunningChanged: if (!running)
dock._syncDockChromeState() dock._syncDockChromeState()
} }
@@ -756,12 +685,12 @@ Variants {
height: implicitHeight height: implicitHeight
// Avoid an offscreen texture seam where the connected dock meets the frame. // Avoid an offscreen texture seam where the connected dock meets the frame.
layer.enabled: !Theme.isConnectedEffect layer.enabled: !usesConnectedFrameChrome
clip: false clip: false
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
visible: !SettingsData.connectedFrameModeActive && !(Theme.isConnectedEffect && dock.reveal) visible: !usesConnectedFrameChrome && (!SettingsData.connectedFrameModeActive || dock.reveal)
color: dock.surfaceColor color: dock.surfaceColor
topLeftRadius: dock.surfaceTopLeftRadius topLeftRadius: dock.surfaceTopLeftRadius
topRightRadius: dock.surfaceTopRightRadius topRightRadius: dock.surfaceTopRightRadius
@@ -771,7 +700,7 @@ Variants {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
visible: !SettingsData.connectedFrameModeActive && !(Theme.isConnectedEffect && dock.reveal) visible: !usesConnectedFrameChrome && (!SettingsData.connectedFrameModeActive || dock.reveal)
color: "transparent" color: "transparent"
topLeftRadius: dock.surfaceTopLeftRadius topLeftRadius: dock.surfaceTopLeftRadius
topRightRadius: dock.surfaceTopRightRadius topRightRadius: dock.surfaceTopRightRadius
@@ -807,7 +736,7 @@ Variants {
y: dockBackground.y - borderThickness y: dockBackground.y - borderThickness
width: dockBackground.width + borderThickness * 2 width: dockBackground.width + borderThickness * 2
height: dockBackground.height + borderThickness * 2 height: dockBackground.height + borderThickness * 2
visible: SettingsData.dockBorderEnabled && dock.hasApps && !Theme.isConnectedEffect visible: SettingsData.dockBorderEnabled && dock.hasApps && !usesConnectedFrameChrome
preferredRendererType: Shape.CurveRenderer preferredRendererType: Shape.CurveRenderer
readonly property real borderThickness: Math.max(1, dock.borderThickness) readonly property real borderThickness: Math.max(1, dock.borderThickness)
@@ -883,6 +812,7 @@ Variants {
isVertical: dock.isVertical isVertical: dock.isVertical
dockScreen: dock.screen dockScreen: dock.screen
iconSize: dock.widgetHeight iconSize: dock.widgetHeight
usesOverlayLayer: dock.usesOverlayLayer
} }
} }
} }
+1
View File
@@ -15,6 +15,7 @@ Item {
property bool isVertical: false property bool isVertical: false
property var dockScreen: null property var dockScreen: null
property real iconSize: 40 property real iconSize: 40
property bool usesOverlayLayer: false
property int draggedIndex: -1 property int draggedIndex: -1
property int dropTargetIndex: -1 property int dropTargetIndex: -1
property bool suppressShiftAnimation: false property bool suppressShiftAnimation: false
+11 -11
View File
@@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import qs.Common import qs.Common
import qs.Services
QtObject { QtObject {
id: root id: root
@@ -10,7 +11,6 @@ QtObject {
property string edge: "bottom" property string edge: "bottom"
property bool dockVisible: false property bool dockVisible: false
property bool autoHide: false property bool autoHide: false
property bool hasFullscreenToplevel: false
property real iconSize: 40 property real iconSize: 40
property real spacing: 4 property real spacing: 4
property real borderThickness: 0 property real borderThickness: 0
@@ -23,14 +23,14 @@ QtObject {
return Math.round(value * dpr) / dpr; return Math.round(value * dpr) / dpr;
} }
readonly property bool frameExclusionActive: SettingsData.frameEnabled && !!screen && SettingsData.isScreenInPreferences(screen, SettingsData.frameScreenPreferences) readonly property bool frameExclusionActive: CompositorService.frameWindowVisibleForScreen(screen)
readonly property bool connectedMode: Theme.isConnectedEffect readonly property bool usesConnectedFrameChrome: CompositorService.usesConnectedFrameChromeForScreen(screen)
readonly property bool connectedBarActiveOnEdge: connectedMode && !!screen && SettingsData.getActiveBarEdgesForScreen(screen).includes(edge) readonly property bool connectedBarActiveOnEdge: usesConnectedFrameChrome && !!screen && SettingsData.getActiveBarEdgesForScreen(screen).includes(edge)
readonly property real connectedJoinInset: { readonly property real connectedJoinInset: {
if (connectedMode) if (usesConnectedFrameChrome)
return connectedBarActiveOnEdge ? SettingsData.frameBarSize : SettingsData.frameThickness; return connectedBarActiveOnEdge ? SettingsData.frameBarSize : SettingsData.frameThickness;
if (SettingsData.frameEnabled) if (frameExclusionActive)
return SettingsData.frameEdgeInsetForSide(screen, edge); return SettingsData.frameEdgeInsetForSide(screen, edge);
return 0; return 0;
} }
@@ -38,15 +38,15 @@ QtObject {
readonly property real frameInset: { readonly property real frameInset: {
if (!frameExclusionActive) if (!frameExclusionActive)
return 0; return 0;
if (connectedMode) if (usesConnectedFrameChrome)
return connectedJoinInset; return connectedJoinInset;
return SettingsData.frameThickness; return SettingsData.frameThickness;
} }
readonly property real effectiveMargin: connectedMode ? 0 : margin readonly property real effectiveMargin: usesConnectedFrameChrome ? 0 : margin
readonly property real visualOffset: connectedMode ? 0 : offset readonly property real visualOffset: usesConnectedFrameChrome ? 0 : offset
readonly property real reserveOffset: offset readonly property real reserveOffset: offset
readonly property real joinedEdgeMargin: connectedMode ? 0 : (barSpacing + effectiveMargin + 1 + borderThickness) readonly property real joinedEdgeMargin: usesConnectedFrameChrome ? 0 : (barSpacing + effectiveMargin + 1 + borderThickness)
readonly property real bodyEdgeMargin: frameInset + joinedEdgeMargin readonly property real bodyEdgeMargin: frameInset + joinedEdgeMargin
readonly property real bodyThickness: iconSize + spacing * 2 + borderThickness * 2 readonly property real bodyThickness: iconSize + spacing * 2 + borderThickness * 2
@@ -57,5 +57,5 @@ QtObject {
// Frame/bar edge exclusions already reserve the edge itself, so the dock // Frame/bar edge exclusions already reserve the edge itself, so the dock
// reservation covers only the dock body and user offset beyond that edge. // reservation covers only the dock body and user offset beyond that edge.
readonly property real reserveZone: px(bodyThickness + reserveOffset + effectiveMargin) readonly property real reserveZone: px(bodyThickness + reserveOffset + effectiveMargin)
readonly property bool shouldReserveSpace: dockVisible && !hasFullscreenToplevel && !autoHide && barSpacing <= 0 readonly property bool shouldReserveSpace: dockVisible && !autoHide && barSpacing <= 0
} }
@@ -148,7 +148,7 @@ Item {
if (wasDragging || mouse.button !== Qt.LeftButton) if (wasDragging || mouse.button !== Qt.LeftButton)
return; return;
PopoutService.toggleDankLauncherV2(); PopoutService.toggleDankLauncherV2(dockApps?.usesOverlayLayer ?? false);
} }
onPositionChanged: mouse => { onPositionChanged: mouse => {
if (longPressing && !dragging) { if (longPressing && !dragging) {
+2 -1
View File
@@ -4,6 +4,7 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Services
Scope { Scope {
id: root id: root
@@ -18,7 +19,7 @@ Scope {
// One thin invisible PanelWindow per edge. // One thin invisible PanelWindow per edge.
// Skips any edge where a bar already provides its own exclusiveZone. // Skips any edge where a bar already provides its own exclusiveZone.
readonly property bool screenEnabled: SettingsData.frameEnabled && SettingsData.isScreenInPreferences(root.screen, SettingsData.frameScreenPreferences) readonly property bool screenEnabled: CompositorService.frameWindowVisibleForScreen(root.screen)
Loader { Loader {
active: root.screenEnabled && !root.barEdges.includes("top") active: root.screenEnabled && !root.barEdges.includes("top")
+4 -3
View File
@@ -17,8 +17,9 @@ PanelWindow {
required property var targetScreen required property var targetScreen
screen: targetScreen screen: targetScreen
visible: _frameActive readonly property bool _frameVisible: CompositorService.frameWindowVisibleForScreen(win.targetScreen)
updatesEnabled: _frameActive visible: win._frameVisible
updatesEnabled: win._frameVisible
WlrLayershell.namespace: "dms:frame" WlrLayershell.namespace: "dms:frame"
WlrLayershell.layer: WlrLayer.Top WlrLayershell.layer: WlrLayer.Top
@@ -52,7 +53,7 @@ PanelWindow {
readonly property var _notifState: ConnectedModeState.notificationStates[win._screenName] || ConnectedModeState.emptyNotificationState readonly property var _notifState: ConnectedModeState.notificationStates[win._screenName] || ConnectedModeState.emptyNotificationState
readonly property var _modalState: ConnectedModeState.modalStates[win._screenName] || ConnectedModeState.emptyModalState readonly property var _modalState: ConnectedModeState.modalStates[win._screenName] || ConnectedModeState.emptyModalState
readonly property bool _connectedActive: win._frameActive && SettingsData.connectedFrameModeActive readonly property bool _connectedActive: CompositorService.usesConnectedFrameChromeForScreen(win.targetScreen)
readonly property string _barSide: { readonly property string _barSide: {
const edges = win.barEdges; const edges = win.barEdges;
if (edges.includes("top")) if (edges.includes("top"))
+2 -1
View File
@@ -97,7 +97,8 @@ sudo rpm -ivh x86_64/dms-greeter-*.rpm
``` ```
The package automatically: The package automatically:
- Creates the greeter user
- Creates the greeter user (via `systemd-sysusers` from `/usr/lib/sysusers.d/dms-greeter.conf` for atomic/immutable compatibility, with package script fallback)
- Sets up directories and permissions - Sets up directories and permissions
- Configures greetd with auto-detected compositor - Configures greetd with auto-detected compositor
- Applies SELinux contexts - Applies SELinux contexts
+13
View File
@@ -36,6 +36,8 @@ Rectangle {
signal closed signal closed
signal switchUserRequested
function updateVisibleActions() { function updateVisibleActions() {
const allActions = powerMenuActionsOverride !== undefined ? powerMenuActionsOverride : ((typeof SettingsData !== "undefined" && SettingsData.powerMenuActions) ? SettingsData.powerMenuActions : ["logout", "suspend", "hibernate", "reboot", "poweroff"]); const allActions = powerMenuActionsOverride !== undefined ? powerMenuActionsOverride : ((typeof SettingsData !== "undefined" && SettingsData.powerMenuActions) ? SettingsData.powerMenuActions : ["logout", "suspend", "hibernate", "reboot", "poweroff"]);
const hibernateSupported = (typeof SessionService !== "undefined" && SessionService.hibernateSupported) || false; const hibernateSupported = (typeof SessionService !== "undefined" && SessionService.hibernateSupported) || false;
@@ -128,6 +130,12 @@ Rectangle {
"label": I18n.tr("Hibernate"), "label": I18n.tr("Hibernate"),
"key": "H" "key": "H"
}; };
case "switchuser":
return {
"icon": "switch_account",
"label": I18n.tr("Switch User"),
"key": "U"
};
default: default:
return { return {
"icon": "help", "icon": "help",
@@ -183,6 +191,11 @@ Rectangle {
function executeAction(action) { function executeAction(action) {
if (!action) if (!action)
return; return;
if (action === "switchuser") {
hide();
switchUserRequested();
return;
}
if (typeof SessionService === "undefined") if (typeof SessionService === "undefined")
return; return;
hide(); hide();
+49 -6
View File
@@ -9,6 +9,7 @@ import Quickshell.Hyprland
import Quickshell.Io import Quickshell.Io
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import qs.Common import qs.Common
import qs.Modals
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -73,6 +74,10 @@ Item {
return pam && (pam.u2fState === "waiting" || pam.u2fState === "insert") && !pam.u2fPending; return pam && (pam.u2fState === "waiting" || pam.u2fState === "insert") && !pam.u2fPending;
} }
function canStartSecurityKeyUnlock() {
return !demoMode && pam && pam.u2f && pam.u2f.available && SettingsData.enableU2f && SettingsData.u2fMode === "or" && !pam.passwd.active && !pam.u2f.active && !pam.u2fPending && !root.unlocking;
}
Component.onCompleted: { Component.onCompleted: {
WeatherService.addRef(); WeatherService.addRef();
UserInfoService.getUserInfo(); UserInfoService.getUserInfo();
@@ -761,6 +766,9 @@ Item {
if (enterButton.visible) { if (enterButton.visible) {
margin += enterButton.width + 2; margin += enterButton.width + 2;
} }
if (securityKeyButton.visible) {
margin += securityKeyButton.width;
}
if (virtualKeyboardButton.visible) { if (virtualKeyboardButton.visible) {
margin += virtualKeyboardButton.width; margin += virtualKeyboardButton.width;
} }
@@ -854,7 +862,7 @@ Item {
anchors.left: lockIconContainer.right anchors.left: lockIconContainer.right
anchors.leftMargin: Theme.spacingM anchors.leftMargin: Theme.spacingM
anchors.right: (revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right)))) anchors.right: (revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (securityKeyButton.visible ? securityKeyButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right)))))
anchors.rightMargin: 2 anchors.rightMargin: 2
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: { text: {
@@ -896,7 +904,7 @@ Item {
StyledText { StyledText {
anchors.left: lockIconContainer.right anchors.left: lockIconContainer.right
anchors.leftMargin: Theme.spacingM anchors.leftMargin: Theme.spacingM
anchors.right: (revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right)))) anchors.right: (revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (securityKeyButton.visible ? securityKeyButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right)))))
anchors.rightMargin: 2 anchors.rightMargin: 2
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: { text: {
@@ -926,7 +934,7 @@ Item {
DankActionButton { DankActionButton {
id: revealButton id: revealButton
anchors.right: virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right)) anchors.right: virtualKeyboardButton.visible ? virtualKeyboardButton.left : (securityKeyButton.visible ? securityKeyButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right)))
anchors.rightMargin: 0 anchors.rightMargin: 0
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
iconName: parent.showPassword ? "visibility_off" : "visibility" iconName: parent.showPassword ? "visibility_off" : "visibility"
@@ -936,10 +944,26 @@ Item {
onClicked: parent.showPassword = !parent.showPassword onClicked: parent.showPassword = !parent.showPassword
} }
DankActionButton { DankActionButton {
id: virtualKeyboardButton id: securityKeyButton
anchors.right: enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right) anchors.right: enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right)
anchors.rightMargin: enterButton.visible ? 0 : Theme.spacingS anchors.rightMargin: 0
anchors.verticalCenter: parent.verticalCenter
iconName: "passkey"
buttonSize: 32
visible: root.canStartSecurityKeyUnlock()
enabled: visible
onClicked: {
passwordField.text = "";
root.passwordBuffer = "";
pam.u2f.startForAlternativeAuth();
}
}
DankActionButton {
id: virtualKeyboardButton
anchors.right: securityKeyButton.visible ? securityKeyButton.left : (enterButton.visible ? enterButton.left : (loadingSpinner.visible ? loadingSpinner.left : parent.right))
anchors.rightMargin: securityKeyButton.visible || enterButton.visible ? 0 : Theme.spacingS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
iconName: "keyboard" iconName: "keyboard"
buttonSize: 32 buttonSize: 32
@@ -1438,6 +1462,7 @@ Item {
} }
DankIcon { DankIcon {
id: lockNetworkIcon
name: { name: {
if (NetworkService.wifiToggling) if (NetworkService.wifiToggling)
return "sync"; return "sync";
@@ -1451,9 +1476,14 @@ Item {
} }
} }
size: Theme.iconSize - 2 size: Theme.iconSize - 2
color: NetworkService.networkStatus !== "disconnected" ? "white" : Qt.rgba(255, 255, 255, 0.5) color: (NetworkService.networkStatus !== "disconnected" || NetworkService.isConnecting) ? "white" : Qt.rgba(255, 255, 255, 0.5)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: NetworkService.networkAvailable visible: NetworkService.networkAvailable
DankBlink {
target: lockNetworkIcon
running: NetworkService.isWifiConnecting
}
} }
DankIcon { DankIcon {
@@ -1465,11 +1495,17 @@ Item {
} }
DankIcon { DankIcon {
id: lockBluetoothIcon
name: "bluetooth" name: "bluetooth"
size: Theme.iconSize - 2 size: Theme.iconSize - 2
color: "white" color: "white"
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: BluetoothService.available && BluetoothService.enabled visible: BluetoothService.available && BluetoothService.enabled
DankBlink {
target: lockBluetoothIcon
running: BluetoothService.connecting
}
} }
DankIcon { DankIcon {
@@ -1693,5 +1729,12 @@ Item {
Qt.callLater(() => passwordField.forceActiveFocus()); Qt.callLater(() => passwordField.forceActiveFocus());
} }
} }
onSwitchUserRequested: {
switchUserPicker.showFromLockScreen();
}
}
SwitchUserModal {
id: switchUserPicker
} }
} }
+31 -5
View File
@@ -20,6 +20,7 @@ Scope {
property string fprintState property string fprintState
property string u2fState property string u2fState
property bool u2fPending: false property bool u2fPending: false
property string u2fPendingMode
property string buffer property string buffer
signal flashMsg signal flashMsg
@@ -35,6 +36,7 @@ Scope {
passwdActiveTimeout.running = false; passwdActiveTimeout.running = false;
unlockRequestTimeout.running = false; unlockRequestTimeout.running = false;
root.u2fPending = false; root.u2fPending = false;
root.u2fPendingMode = "";
root.u2fState = ""; root.u2fState = "";
root.unlockInProgress = false; root.unlockInProgress = false;
} }
@@ -58,6 +60,7 @@ Scope {
u2fErrorRetry.running = false; u2fErrorRetry.running = false;
u2fPendingTimeout.running = false; u2fPendingTimeout.running = false;
root.u2fPending = false; root.u2fPending = false;
root.u2fPendingMode = "";
root.u2fState = ""; root.u2fState = "";
unlockRequestTimeout.restart(); unlockRequestTimeout.restart();
unlockRequested(); unlockRequested();
@@ -79,6 +82,7 @@ Scope {
u2fErrorRetry.running = false; u2fErrorRetry.running = false;
u2fPendingTimeout.running = false; u2fPendingTimeout.running = false;
root.u2fPending = false; root.u2fPending = false;
root.u2fPendingMode = "";
root.u2fState = ""; root.u2fState = "";
fprint.checkAvail(); fprint.checkAvail();
} }
@@ -142,6 +146,7 @@ Scope {
unlockRequestTimeout.running = false; unlockRequestTimeout.running = false;
root.unlockInProgress = false; root.unlockInProgress = false;
root.u2fPending = false; root.u2fPending = false;
root.u2fPendingMode = "";
root.u2fState = ""; root.u2fState = "";
u2fPendingTimeout.running = false; u2fPendingTimeout.running = false;
u2f.abort(); u2f.abort();
@@ -243,9 +248,8 @@ Scope {
return; return;
} }
if (SettingsData.u2fMode === "or") { if (SettingsData.u2fMode === "or")
start(); abort();
}
} }
function startForSecondFactor(): void { function startForSecondFactor(): void {
@@ -255,6 +259,18 @@ Scope {
} }
abort(); abort();
root.u2fPending = true; root.u2fPending = true;
root.u2fPendingMode = "and";
root.u2fState = "";
u2fPendingTimeout.restart();
start();
}
function startForAlternativeAuth(): void {
if (!available || !SettingsData.enableU2f || SettingsData.u2fMode !== "or" || root.unlockInProgress || passwd.active || active)
return;
abort();
root.u2fPending = true;
root.u2fPendingMode = "or";
root.u2fState = ""; root.u2fState = "";
u2fPendingTimeout.restart(); u2fPendingTimeout.restart();
start(); start();
@@ -281,9 +297,19 @@ Scope {
abort(); abort();
if (root.u2fPending) { if (root.u2fPending) {
if (root.u2fPendingMode === "or") {
root.u2fPending = false;
root.u2fPendingMode = "";
root.u2fState = root.u2fState === "waiting" ? "" : "insert";
u2fPendingTimeout.running = false;
fprint.checkAvail();
return;
}
if (root.u2fState === "waiting") { if (root.u2fState === "waiting") {
// AND mode: device was found but auth failed back to password // AND mode: device was found but auth failed back to password
root.u2fPending = false; root.u2fPending = false;
root.u2fPendingMode = "";
root.u2fState = ""; root.u2fState = "";
fprint.checkAvail(); fprint.checkAvail();
} else { } else {
@@ -292,9 +318,7 @@ Scope {
u2fErrorRetry.restart(); u2fErrorRetry.restart();
} }
} else { } else {
// OR mode: prompt to insert key, silently retry
root.u2fState = "insert"; root.u2fState = "insert";
u2fErrorRetry.restart();
} }
} }
} }
@@ -367,6 +391,7 @@ Scope {
root.fprintState = ""; root.fprintState = "";
root.u2fState = ""; root.u2fState = "";
root.u2fPending = false; root.u2fPending = false;
root.u2fPendingMode = "";
root.lockMessage = ""; root.lockMessage = "";
root.resetAuthFlows(); root.resetAuthFlows();
fprint.checkAvail(); fprint.checkAvail();
@@ -399,6 +424,7 @@ Scope {
u2fPendingTimeout.running = false; u2fPendingTimeout.running = false;
unlockRequestTimeout.running = false; unlockRequestTimeout.running = false;
root.u2fPending = false; root.u2fPending = false;
root.u2fPendingMode = "";
root.u2fState = ""; root.u2fState = "";
u2f.checkAvail(); u2f.checkAvail();
} }
@@ -182,26 +182,30 @@ Rectangle {
Row { Row {
width: parent.width width: parent.width
spacing: Theme.spacingXS spacing: Theme.spacingXS
readonly property real reservedTrailingWidth: historySeparator.implicitWidth + Math.max(historyTimeText.implicitWidth, 72) + spacing
StyledText { Item {
id: historyTitleText width: Math.max(0, parent.width - historySeparator.implicitWidth - Math.max(historyTimeText.implicitWidth, 72) - parent.spacing * 2)
width: Math.min(implicitWidth, Math.max(0, parent.width - parent.reservedTrailingWidth)) height: historyTitleText.implicitHeight
text: { visible: historyTitleText.text.length > 0
let title = historyItem.summary || "";
const appName = historyItem.appName || ""; StyledText {
const prefix = appName + " • "; id: historyTitleText
if (appName && title.toLowerCase().startsWith(prefix.toLowerCase())) { anchors.fill: parent
title = title.substring(prefix.length); text: {
let title = historyItem.summary || "";
const appName = historyItem.appName || "";
const prefix = appName + " • ";
if (appName && title.toLowerCase().startsWith(prefix.toLowerCase())) {
title = title.substring(prefix.length);
}
return title;
} }
return title; color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
} }
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
visible: text.length > 0
} }
StyledText { StyledText {
id: historySeparator id: historySeparator
@@ -10,7 +10,7 @@ import qs.Widgets
PanelWindow { PanelWindow {
id: win id: win
readonly property bool connectedFrameMode: SettingsData.frameEnabled && Theme.isConnectedEffect && SettingsData.isScreenInPreferences(win.screen, SettingsData.frameScreenPreferences) readonly property bool connectedFrameMode: CompositorService.usesConnectedFrameChromeForScreen(win.screen)
readonly property string notifBarSide: { readonly property string notifBarSide: {
const pos = SettingsData.notificationPopupPosition; const pos = SettingsData.notificationPopupPosition;
if (pos === -1) if (pos === -1)
@@ -370,9 +370,9 @@ PanelWindow {
return Math.max(0, Math.round(Theme.px(raw, dpr))); return Math.max(0, Math.round(Theme.px(raw, dpr)));
} }
readonly property bool frameOnlyNoConnected: SettingsData.frameEnabled && !connectedFrameMode && !!screen && SettingsData.isScreenInPreferences(screen, SettingsData.frameScreenPreferences) readonly property bool frameVisibleWithoutConnectedChrome: CompositorService.frameWindowVisibleForScreen(screen) && !connectedFrameMode
// Frame ON + Connected OFF. frameEdgeInset is the full bar/frame inset // Frame visible without connected chrome. frameEdgeInset is the full bar/frame inset.
function _frameGapMargin(side) { function _frameGapMargin(side) {
return _frameEdgeInset(side) + Theme.popupDistance; return _frameEdgeInset(side) + Theme.popupDistance;
} }
@@ -387,7 +387,7 @@ PanelWindow {
const cornerClear = (isCenterPosition || SettingsData.frameCloseGaps) ? 0 : (Theme.px(SettingsData.frameRounding, dpr) + Theme.px(Theme.connectedCornerRadius, dpr)); const cornerClear = (isCenterPosition || SettingsData.frameCloseGaps) ? 0 : (Theme.px(SettingsData.frameRounding, dpr) + Theme.px(Theme.connectedCornerRadius, dpr));
return _frameEdgeInset("top") + cornerClear + screenY; return _frameEdgeInset("top") + cornerClear + screenY;
} }
if (frameOnlyNoConnected) if (frameVisibleWithoutConnectedChrome)
return _frameGapMargin("top") + screenY; return _frameGapMargin("top") + screenY;
const barInfo = getBarInfo(); const barInfo = getBarInfo();
const base = barInfo.topBar > 0 ? barInfo.topBar : Theme.popupDistance; const base = barInfo.topBar > 0 ? barInfo.topBar : Theme.popupDistance;
@@ -404,7 +404,7 @@ PanelWindow {
const cornerClear = (isCenterPosition || SettingsData.frameCloseGaps) ? 0 : (Theme.px(SettingsData.frameRounding, dpr) + Theme.px(Theme.connectedCornerRadius, dpr)); const cornerClear = (isCenterPosition || SettingsData.frameCloseGaps) ? 0 : (Theme.px(SettingsData.frameRounding, dpr) + Theme.px(Theme.connectedCornerRadius, dpr));
return _frameEdgeInset("bottom") + cornerClear + screenY; return _frameEdgeInset("bottom") + cornerClear + screenY;
} }
if (frameOnlyNoConnected) if (frameVisibleWithoutConnectedChrome)
return _frameGapMargin("bottom") + screenY; return _frameGapMargin("bottom") + screenY;
const barInfo = getBarInfo(); const barInfo = getBarInfo();
const base = barInfo.bottomBar > 0 ? barInfo.bottomBar : Theme.popupDistance; const base = barInfo.bottomBar > 0 ? barInfo.bottomBar : Theme.popupDistance;
@@ -422,7 +422,7 @@ PanelWindow {
if (connectedFrameMode) if (connectedFrameMode)
return _frameEdgeInset("left"); return _frameEdgeInset("left");
if (frameOnlyNoConnected) if (frameVisibleWithoutConnectedChrome)
return _frameGapMargin("left"); return _frameGapMargin("left");
const barInfo = getBarInfo(); const barInfo = getBarInfo();
return barInfo.leftBar > 0 ? barInfo.leftBar : Theme.popupDistance; return barInfo.leftBar > 0 ? barInfo.leftBar : Theme.popupDistance;
@@ -439,7 +439,7 @@ PanelWindow {
if (connectedFrameMode) if (connectedFrameMode)
return _frameEdgeInset("right"); return _frameEdgeInset("right");
if (frameOnlyNoConnected) if (frameVisibleWithoutConnectedChrome)
return _frameGapMargin("right"); return _frameGapMargin("right");
const barInfo = getBarInfo(); const barInfo = getBarInfo();
return barInfo.rightBar > 0 ? barInfo.rightBar : Theme.popupDistance; return barInfo.rightBar > 0 ? barInfo.rightBar : Theme.popupDistance;
@@ -10,7 +10,7 @@ QtObject {
property var modelData property var modelData
property int topMargin: 0 property int topMargin: 0
readonly property bool compactMode: SettingsData.notificationCompactMode readonly property bool compactMode: SettingsData.notificationCompactMode
readonly property bool notificationConnectedMode: SettingsData.frameEnabled && Theme.isConnectedEffect && SettingsData.isScreenInPreferences(manager.modelData, SettingsData.frameScreenPreferences) readonly property bool notificationConnectedMode: CompositorService.usesConnectedFrameChromeForScreen(manager.modelData)
readonly property bool closeGapNotifications: notificationConnectedMode && SettingsData.frameCloseGaps readonly property bool closeGapNotifications: notificationConnectedMode && SettingsData.frameCloseGaps
readonly property string notifBarSide: { readonly property string notifBarSide: {
const pos = SettingsData.notificationPopupPosition; const pos = SettingsData.notificationPopupPosition;
-29
View File
@@ -1,29 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
DankOSD {
id: root
osdWidth: Theme.iconSize + Theme.spacingS * 2
osdHeight: Theme.iconSize + Theme.spacingS * 2
autoHideInterval: 2000
enableMouseInteraction: false
Connections {
target: AudioService
function onMicMuteChanged() {
if (SettingsData.osdMicMuteEnabled) {
root.show()
}
}
}
content: DankIcon {
anchors.centerIn: parent
name: AudioService.source && AudioService.source.audio && AudioService.source.audio.muted ? "mic_off" : "mic"
size: Theme.iconSize
color: AudioService.source && AudioService.source.audio && AudioService.source.audio.muted ? Theme.error : Theme.primary
}
}
+253
View File
@@ -0,0 +1,253 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
DankOSD {
id: root
readonly property bool useVertical: isVerticalLayout
property int _displayVolume: 0
function _syncVolume() {
if (!AudioService.source?.audio)
return;
_displayVolume = Math.round(AudioService.source.audio.volume * 100);
}
osdWidth: useVertical ? (40 + Theme.spacingS * 2) : Math.min(260, Screen.width - Theme.spacingM * 2)
osdHeight: useVertical ? Math.min(260, Screen.height - Theme.spacingM * 2) : (40 + Theme.spacingS * 2)
autoHideInterval: 3000
enableMouseInteraction: true
Connections {
target: AudioService.source?.audio ?? null
function onVolumeChanged() {
root._syncVolume();
if (SettingsData.osdMicVolumeEnabled)
root.show();
}
function onMutedChanged() {
if (SettingsData.osdMicMuteEnabled)
root.show();
}
}
Connections {
target: AudioService
function onSourceChanged() {
root._syncVolume();
if (root.shouldBeVisible && SettingsData.osdMicVolumeEnabled)
root.show();
}
}
content: Loader {
anchors.fill: parent
sourceComponent: useVertical ? verticalContent : horizontalContent
}
Component {
id: horizontalContent
Item {
property int gap: Theme.spacingS
anchors.centerIn: parent
width: parent.width - Theme.spacingS * 2
height: 40
Rectangle {
width: Theme.iconSize
height: Theme.iconSize
radius: Theme.iconSize / 2
color: "transparent"
x: parent.gap
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: AudioService.source?.audio?.muted ? "mic_off" : "mic"
size: Theme.iconSize
color: muteButton.containsMouse ? Theme.primary : (AudioService.source?.audio?.muted ? Theme.error : Theme.surfaceText)
}
MouseArea {
id: muteButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: AudioService.toggleMicMute()
onContainsMouseChanged: setChildHovered(containsMouse || volumeSlider.containsMouse)
}
}
DankSlider {
id: volumeSlider
width: parent.width - Theme.iconSize - parent.gap * 3
height: 40
x: parent.gap * 2 + Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
minimum: 0
maximum: 100
enabled: AudioService.source?.audio ?? false
showValue: true
unit: "%"
thumbOutlineColor: Theme.surfaceContainer
valueOverride: root._displayVolume
alwaysShowValue: SettingsData.osdAlwaysShowValue
Component.onCompleted: {
root._syncVolume();
value = root._displayVolume;
}
onSliderValueChanged: newValue => {
if (!AudioService.source?.audio)
return;
SessionData.suppressOSDTemporarily();
AudioService.source.audio.volume = newValue / 100;
resetHideTimer();
}
onContainsMouseChanged: setChildHovered(containsMouse || muteButton.containsMouse)
Binding on value {
value: root._displayVolume
when: !volumeSlider.pressed
}
}
}
}
Component {
id: verticalContent
Item {
anchors.fill: parent
property int gap: Theme.spacingS
Rectangle {
width: Theme.iconSize
height: Theme.iconSize
radius: Theme.iconSize / 2
color: "transparent"
anchors.horizontalCenter: parent.horizontalCenter
y: gap
DankIcon {
anchors.centerIn: parent
name: AudioService.source?.audio?.muted ? "mic_off" : "mic"
size: Theme.iconSize
color: muteButtonVert.containsMouse ? Theme.primary : (AudioService.source?.audio?.muted ? Theme.error : Theme.surfaceText)
}
MouseArea {
id: muteButtonVert
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: AudioService.toggleMicMute()
onContainsMouseChanged: setChildHovered(containsMouse || vertSliderArea.containsMouse)
}
}
Item {
id: vertSlider
width: 12
height: parent.height - Theme.iconSize - gap * 3 - 24
anchors.horizontalCenter: parent.horizontalCenter
y: gap * 2 + Theme.iconSize
property bool dragging: false
property int value: root._displayVolume
Rectangle {
id: vertTrack
width: parent.width
height: parent.height
anchors.centerIn: parent
color: Theme.outline
radius: Theme.cornerRadius
}
Rectangle {
id: vertFill
width: parent.width
height: (vertSlider.value / 100) * parent.height
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
color: AudioService.source?.audio?.muted ? Theme.error : Theme.primary
radius: Theme.cornerRadius
}
Rectangle {
id: vertHandle
width: 24
height: 8
radius: Theme.cornerRadius
y: {
const ratio = vertSlider.value / 100;
const travel = parent.height - height;
return Math.max(0, Math.min(travel, travel * (1 - ratio)));
}
anchors.horizontalCenter: parent.horizontalCenter
color: AudioService.source?.audio?.muted ? Theme.error : Theme.primary
border.width: 3
border.color: Theme.surfaceContainer
}
MouseArea {
id: vertSliderArea
anchors.fill: parent
anchors.margins: -12
enabled: AudioService.source?.audio ?? false
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onContainsMouseChanged: setChildHovered(containsMouse || muteButtonVert.containsMouse)
onPressed: mouse => {
vertSlider.dragging = true;
updateVolume(mouse);
}
onReleased: vertSlider.dragging = false
onPositionChanged: mouse => {
if (pressed)
updateVolume(mouse);
}
onClicked: mouse => updateVolume(mouse)
function updateVolume(mouse) {
if (!AudioService.source?.audio)
return;
const ratio = 1.0 - (mouse.y / height);
const volume = Math.max(0, Math.min(100, Math.round(ratio * 100)));
SessionData.suppressOSDTemporarily();
AudioService.source.audio.volume = volume / 100;
resetHideTimer();
}
}
}
StyledText {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: gap
text: vertSlider.value + "%"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
visible: SettingsData.osdAlwaysShowValue
}
}
}
}
+13
View File
@@ -1,4 +1,5 @@
import QtQuick import QtQuick
import Quickshell
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -38,6 +39,18 @@ Item {
readonly property real rightMargin: !isVerticalOrientation ? (isRightBarEdge && isLast ? barEdgeExtension : (isLast ? gapExtension : gapExtension / 2)) : 0 readonly property real rightMargin: !isVerticalOrientation ? (isRightBarEdge && isLast ? barEdgeExtension : (isLast ? gapExtension : gapExtension / 2)) : 0
readonly property real topMargin: isVerticalOrientation ? (isTopBarEdge && isFirst ? barEdgeExtension : (isFirst ? gapExtension : gapExtension / 2)) : 0 readonly property real topMargin: isVerticalOrientation ? (isTopBarEdge && isFirst ? barEdgeExtension : (isFirst ? gapExtension : gapExtension / 2)) : 0
readonly property real bottomMargin: isVerticalOrientation ? (isBottomBarEdge && isLast ? barEdgeExtension : (isLast ? gapExtension : gapExtension / 2)) : 0 readonly property real bottomMargin: isVerticalOrientation ? (isBottomBarEdge && isLast ? barEdgeExtension : (isLast ? gapExtension : gapExtension / 2)) : 0
readonly property bool barUsesOverlayLayer: {
switch (Quickshell.env("DMS_DANKBAR_LAYER")) {
case "overlay":
return true;
case "bottom":
case "background":
case "top":
return false;
default:
return (barConfig?.useOverlayLayer ?? false) || CompositorService.framePeerSurfacesUseOverlayForScreen(parentScreen);
}
}
signal clicked signal clicked
signal rightClicked(real rootX, real rootY) signal rightClicked(real rootX, real rootY)
+39 -4
View File
@@ -137,7 +137,7 @@ Item {
popupGapsAuto: defaultBar.popupGapsAuto ?? true, popupGapsAuto: defaultBar.popupGapsAuto ?? true,
popupGapsManual: defaultBar.popupGapsManual ?? 4, popupGapsManual: defaultBar.popupGapsManual ?? 4,
maximizeDetection: defaultBar.maximizeDetection ?? true, maximizeDetection: defaultBar.maximizeDetection ?? true,
fullscreenDetection: defaultBar.fullscreenDetection ?? true, useOverlayLayer: defaultBar.useOverlayLayer ?? false,
scrollEnabled: defaultBar.scrollEnabled ?? true, scrollEnabled: defaultBar.scrollEnabled ?? true,
scrollXBehavior: defaultBar.scrollXBehavior ?? "column", scrollXBehavior: defaultBar.scrollXBehavior ?? "column",
scrollYBehavior: defaultBar.scrollYBehavior ?? "workspace", scrollYBehavior: defaultBar.scrollYBehavior ?? "workspace",
@@ -597,6 +597,7 @@ Item {
SettingsToggleRow { SettingsToggleRow {
text: I18n.tr("Auto-hide") text: I18n.tr("Auto-hide")
description: I18n.tr("Automatically hide the bar when the pointer moves away")
checked: selectedBarConfig?.autoHide ?? false checked: selectedBarConfig?.autoHide ?? false
onToggled: toggled => { onToggled: toggled => {
SettingsData.updateBarConfig(selectedBarId, { SettingsData.updateBarConfig(selectedBarId, {
@@ -623,6 +624,7 @@ Item {
id: hideDelaySlider id: hideDelaySlider
width: parent.width - parent.parent.leftPadding width: parent.width - parent.parent.leftPadding
text: I18n.tr("Hide Delay") text: I18n.tr("Hide Delay")
description: I18n.tr("Time to wait before hiding after the pointer leaves")
value: selectedBarConfig?.autoHideDelay ?? 250 value: selectedBarConfig?.autoHideDelay ?? 250
minimum: 0 minimum: 0
maximum: 2000 maximum: 2000
@@ -645,6 +647,7 @@ Item {
SettingsToggleRow { SettingsToggleRow {
width: parent.width - parent.leftPadding width: parent.width - parent.leftPadding
text: I18n.tr("Strict auto-hide", "Dank bar setting: hide the bar when the pointer leaves even if a menu or bar popover is still open") text: I18n.tr("Strict auto-hide", "Dank bar setting: hide the bar when the pointer leaves even if a menu or bar popover is still open")
description: I18n.tr("Hide the bar when the pointer leaves even if a popout is still open")
checked: selectedBarConfig?.autoHideStrict ?? false checked: selectedBarConfig?.autoHideStrict ?? false
onToggled: toggled => { onToggled: toggled => {
SettingsData.updateBarConfig(selectedBarId, { SettingsData.updateBarConfig(selectedBarId, {
@@ -658,6 +661,7 @@ Item {
width: parent.width - parent.leftPadding width: parent.width - parent.leftPadding
visible: CompositorService.isNiri || CompositorService.isHyprland visible: CompositorService.isNiri || CompositorService.isHyprland
text: I18n.tr("Hide When Windows Open") text: I18n.tr("Hide When Windows Open")
description: I18n.tr("Show the bar only when no windows are open")
checked: selectedBarConfig?.showOnWindowsOpen ?? false checked: selectedBarConfig?.showOnWindowsOpen ?? false
onToggled: toggled => { onToggled: toggled => {
SettingsData.updateBarConfig(selectedBarId, { SettingsData.updateBarConfig(selectedBarId, {
@@ -676,6 +680,7 @@ Item {
SettingsToggleRow { SettingsToggleRow {
text: I18n.tr("Manual Show/Hide") text: I18n.tr("Manual Show/Hide")
description: I18n.tr("Toggle bar visibility manually via IPC")
checked: selectedBarConfig?.visible ?? true checked: selectedBarConfig?.visible ?? true
onToggled: toggled => { onToggled: toggled => {
SettingsData.updateBarConfig(selectedBarId, { SettingsData.updateBarConfig(selectedBarId, {
@@ -694,6 +699,7 @@ Item {
SettingsToggleRow { SettingsToggleRow {
text: I18n.tr("Click Through") text: I18n.tr("Click Through")
description: I18n.tr("Mouse clicks pass through the bar to windows behind it")
checked: selectedBarConfig?.clickThrough ?? false checked: selectedBarConfig?.clickThrough ?? false
onToggled: toggled => SettingsData.updateBarConfig(selectedBarId, { onToggled: toggled => SettingsData.updateBarConfig(selectedBarId, {
clickThrough: toggled clickThrough: toggled
@@ -713,6 +719,7 @@ Item {
enabled: !SettingsData.frameEnabled enabled: !SettingsData.frameEnabled
opacity: SettingsData.frameEnabled ? 0.5 : 1.0 opacity: SettingsData.frameEnabled ? 0.5 : 1.0
text: I18n.tr("Show on Overview") text: I18n.tr("Show on Overview")
description: I18n.tr("Show the bar when niri overview is active")
checked: selectedBarConfig?.openOnOverview ?? false checked: selectedBarConfig?.openOnOverview ?? false
onToggled: toggled => { onToggled: toggled => {
SettingsData.updateBarConfig(selectedBarId, { SettingsData.updateBarConfig(selectedBarId, {
@@ -729,11 +736,14 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
text: I18n.tr("Hide When Fullscreen", "bar visibility toggle: hide the bar when a window is fullscreen") settingKey: "barUseOverlayLayer"
checked: selectedBarConfig?.fullscreenDetection ?? true tags: ["bar", "fullscreen", "overlay", "layer"]
text: I18n.tr("Use Overlay Layer", "bar layer toggle: use Wayland overlay layer")
description: I18n.tr("Place the bar on the Wayland overlay layer")
checked: selectedBarConfig?.useOverlayLayer ?? false
onToggled: toggled => { onToggled: toggled => {
SettingsData.updateBarConfig(selectedBarId, { SettingsData.updateBarConfig(selectedBarId, {
fullscreenDetection: toggled useOverlayLayer: toggled
}); });
notifyHorizontalBarChange(); notifyHorizontalBarChange();
} }
@@ -756,6 +766,7 @@ Item {
SettingsSliderRow { SettingsSliderRow {
id: edgeSpacingSlider id: edgeSpacingSlider
text: I18n.tr("Edge Spacing") text: I18n.tr("Edge Spacing")
description: I18n.tr("Space between the bar and screen edges")
value: selectedBarConfig?.spacing ?? 4 value: selectedBarConfig?.spacing ?? 4
minimum: 0 minimum: 0
maximum: 32 maximum: 32
@@ -777,6 +788,7 @@ Item {
SettingsSliderRow { SettingsSliderRow {
id: exclusiveZoneSlider id: exclusiveZoneSlider
text: I18n.tr("Exclusive Zone Offset") text: I18n.tr("Exclusive Zone Offset")
description: I18n.tr("Fine-tune the space reserved for the bar from the screen edge")
value: selectedBarConfig?.bottomGap ?? 0 value: selectedBarConfig?.bottomGap ?? 0
minimum: -50 minimum: -50
maximum: 50 maximum: 50
@@ -798,6 +810,7 @@ Item {
SettingsSliderRow { SettingsSliderRow {
id: sizeSlider id: sizeSlider
text: I18n.tr("Size") text: I18n.tr("Size")
description: I18n.tr("Adjust the bar height via inner padding")
value: selectedBarConfig?.innerPadding ?? 4 value: selectedBarConfig?.innerPadding ?? 4
minimum: -8 minimum: -8
maximum: 24 maximum: 24
@@ -819,6 +832,7 @@ Item {
SettingsSliderRow { SettingsSliderRow {
id: widgetPaddingSlider id: widgetPaddingSlider
text: I18n.tr("Padding") text: I18n.tr("Padding")
description: I18n.tr("Inner padding applied to each widget")
value: selectedBarConfig?.widgetPadding ?? 8 value: selectedBarConfig?.widgetPadding ?? 8
minimum: 0 minimum: 0
maximum: 32 maximum: 32
@@ -849,6 +863,7 @@ Item {
SettingsToggleRow { SettingsToggleRow {
text: I18n.tr("Auto Popup Gaps") text: I18n.tr("Auto Popup Gaps")
description: I18n.tr("Automatically calculate popup gap based on bar spacing")
checked: selectedBarConfig?.popupGapsAuto ?? true checked: selectedBarConfig?.popupGapsAuto ?? true
onToggled: checked => { onToggled: checked => {
SettingsData.updateBarConfig(selectedBarId, { SettingsData.updateBarConfig(selectedBarId, {
@@ -874,6 +889,7 @@ Item {
id: popupGapsManualSlider id: popupGapsManualSlider
width: parent.width - parent.parent.leftPadding width: parent.width - parent.parent.leftPadding
text: I18n.tr("Manual Gap Size") text: I18n.tr("Manual Gap Size")
description: I18n.tr("Override the popup gap size when auto is disabled")
value: selectedBarConfig?.popupGapsManual ?? 4 value: selectedBarConfig?.popupGapsManual ?? 4
minimum: 0 minimum: 0
maximum: 50 maximum: 50
@@ -904,6 +920,7 @@ Item {
id: barTransparencySlider id: barTransparencySlider
visible: !SettingsData.frameEnabled visible: !SettingsData.frameEnabled
text: I18n.tr("Bar Transparency") text: I18n.tr("Bar Transparency")
description: I18n.tr("Opacity of the bar background")
value: (selectedBarConfig?.transparency ?? 1.0) * 100 value: (selectedBarConfig?.transparency ?? 1.0) * 100
minimum: 0 minimum: 0
maximum: 100 maximum: 100
@@ -926,6 +943,7 @@ Item {
SettingsSliderRow { SettingsSliderRow {
id: widgetTransparencySlider id: widgetTransparencySlider
text: I18n.tr("Widget Transparency") text: I18n.tr("Widget Transparency")
description: I18n.tr("Opacity of widget backgrounds")
value: (selectedBarConfig?.widgetTransparency ?? 1.0) * 100 value: (selectedBarConfig?.widgetTransparency ?? 1.0) * 100
minimum: 0 minimum: 0
maximum: 100 maximum: 100
@@ -1020,6 +1038,7 @@ Item {
SettingsToggleRow { SettingsToggleRow {
text: I18n.tr("Square Corners") text: I18n.tr("Square Corners")
description: I18n.tr("Remove corner rounding from the bar")
visible: !SettingsData.frameEnabled visible: !SettingsData.frameEnabled
checked: selectedBarConfig?.squareCorners ?? false checked: selectedBarConfig?.squareCorners ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, { onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
@@ -1029,6 +1048,7 @@ Item {
SettingsToggleRow { SettingsToggleRow {
text: I18n.tr("No Background") text: I18n.tr("No Background")
description: I18n.tr("Make the bar background fully transparent")
visible: !SettingsData.frameEnabled visible: !SettingsData.frameEnabled
checked: selectedBarConfig?.noBackground ?? false checked: selectedBarConfig?.noBackground ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, { onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
@@ -1038,6 +1058,7 @@ Item {
SettingsToggleRow { SettingsToggleRow {
text: I18n.tr("Maximize Widget Icons") text: I18n.tr("Maximize Widget Icons")
description: I18n.tr("Stretch widget icons to fill the available bar height")
checked: selectedBarConfig?.maximizeWidgetIcons ?? false checked: selectedBarConfig?.maximizeWidgetIcons ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, { onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
maximizeWidgetIcons: checked maximizeWidgetIcons: checked
@@ -1046,6 +1067,7 @@ Item {
SettingsToggleRow { SettingsToggleRow {
text: I18n.tr("Maximize Widget Text") text: I18n.tr("Maximize Widget Text")
description: I18n.tr("Stretch widget text to fill the available bar height")
checked: selectedBarConfig?.maximizeWidgetText ?? false checked: selectedBarConfig?.maximizeWidgetText ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, { onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
maximizeWidgetText: checked maximizeWidgetText: checked
@@ -1054,6 +1076,7 @@ Item {
SettingsToggleRow { SettingsToggleRow {
text: I18n.tr("Remove Widget Padding") text: I18n.tr("Remove Widget Padding")
description: I18n.tr("Remove inner padding from all widgets")
checked: selectedBarConfig?.removeWidgetPadding ?? false checked: selectedBarConfig?.removeWidgetPadding ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, { onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
removeWidgetPadding: checked removeWidgetPadding: checked
@@ -1069,6 +1092,7 @@ Item {
SettingsToggleRow { SettingsToggleRow {
text: I18n.tr("Goth Corners") text: I18n.tr("Goth Corners")
description: I18n.tr("Apply inverse concave corner cutouts to the bar")
visible: !SettingsData.frameEnabled visible: !SettingsData.frameEnabled
checked: selectedBarConfig?.gothCornersEnabled ?? false checked: selectedBarConfig?.gothCornersEnabled ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, { onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
@@ -1078,6 +1102,7 @@ Item {
SettingsToggleRow { SettingsToggleRow {
text: I18n.tr("Corner Radius Override") text: I18n.tr("Corner Radius Override")
description: I18n.tr("Use a custom radius for goth corner cutouts")
checked: selectedBarConfig?.gothCornerRadiusOverride ?? false checked: selectedBarConfig?.gothCornerRadiusOverride ?? false
visible: selectedBarConfig?.gothCornersEnabled ?? false visible: selectedBarConfig?.gothCornersEnabled ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, { onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
@@ -1236,6 +1261,7 @@ Item {
SettingsButtonGroupRow { SettingsButtonGroupRow {
text: I18n.tr("Color") text: I18n.tr("Color")
description: I18n.tr("Theme color used for the border")
model: ["Surface", "Secondary", "Primary"] model: ["Surface", "Secondary", "Primary"]
currentIndex: { currentIndex: {
switch (selectedBarConfig?.borderColor || "surfaceText") { switch (selectedBarConfig?.borderColor || "surfaceText") {
@@ -1273,6 +1299,7 @@ Item {
SettingsSliderRow { SettingsSliderRow {
id: borderOpacitySlider id: borderOpacitySlider
text: I18n.tr("Opacity") text: I18n.tr("Opacity")
description: I18n.tr("Transparency of the border")
value: (selectedBarConfig?.borderOpacity ?? 1.0) * 100 value: (selectedBarConfig?.borderOpacity ?? 1.0) * 100
minimum: 0 minimum: 0
maximum: 100 maximum: 100
@@ -1295,6 +1322,7 @@ Item {
SettingsSliderRow { SettingsSliderRow {
id: borderThicknessSlider id: borderThicknessSlider
text: I18n.tr("Thickness") text: I18n.tr("Thickness")
description: I18n.tr("Width of the border in pixels")
value: selectedBarConfig?.borderThickness ?? 1 value: selectedBarConfig?.borderThickness ?? 1
minimum: 1 minimum: 1
maximum: 10 maximum: 10
@@ -1326,6 +1354,7 @@ Item {
SettingsButtonGroupRow { SettingsButtonGroupRow {
text: I18n.tr("Color") text: I18n.tr("Color")
description: I18n.tr("Theme color used for the widget outline")
model: ["Surface", "Secondary", "Primary"] model: ["Surface", "Secondary", "Primary"]
currentIndex: { currentIndex: {
switch (selectedBarConfig?.widgetOutlineColor || "primary") { switch (selectedBarConfig?.widgetOutlineColor || "primary") {
@@ -1363,6 +1392,7 @@ Item {
SettingsSliderRow { SettingsSliderRow {
id: widgetOutlineOpacitySlider id: widgetOutlineOpacitySlider
text: I18n.tr("Opacity") text: I18n.tr("Opacity")
description: I18n.tr("Transparency of the widget outline")
value: (selectedBarConfig?.widgetOutlineOpacity ?? 1.0) * 100 value: (selectedBarConfig?.widgetOutlineOpacity ?? 1.0) * 100
minimum: 0 minimum: 0
maximum: 100 maximum: 100
@@ -1385,6 +1415,7 @@ Item {
SettingsSliderRow { SettingsSliderRow {
id: widgetOutlineThicknessSlider id: widgetOutlineThicknessSlider
text: I18n.tr("Thickness") text: I18n.tr("Thickness")
description: I18n.tr("Width of the widget outline in pixels")
value: selectedBarConfig?.widgetOutlineThickness ?? 1 value: selectedBarConfig?.widgetOutlineThickness ?? 1
minimum: 1 minimum: 1
maximum: 10 maximum: 10
@@ -1455,6 +1486,7 @@ Item {
SettingsSliderRow { SettingsSliderRow {
visible: shadowCard.shadowActive visible: shadowCard.shadowActive
text: I18n.tr("Intensity", "shadow intensity slider") text: I18n.tr("Intensity", "shadow intensity slider")
description: I18n.tr("Shadow blur radius in pixels")
minimum: 0 minimum: 0
maximum: 100 maximum: 100
unit: "px" unit: "px"
@@ -1468,6 +1500,7 @@ Item {
SettingsSliderRow { SettingsSliderRow {
visible: shadowCard.shadowActive visible: shadowCard.shadowActive
text: I18n.tr("Opacity") text: I18n.tr("Opacity")
description: I18n.tr("Transparency of the shadow layer")
minimum: 10 minimum: 10
maximum: 100 maximum: 100
unit: "%" unit: "%"
@@ -1655,6 +1688,7 @@ Item {
SettingsButtonGroupRow { SettingsButtonGroupRow {
text: I18n.tr("Y Axis") text: I18n.tr("Y Axis")
description: I18n.tr("Action performed when scrolling vertically on the bar")
model: CompositorService.isNiri ? [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")] : [I18n.tr("None"), I18n.tr("Workspace")] model: CompositorService.isNiri ? [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")] : [I18n.tr("None"), I18n.tr("Workspace")]
currentIndex: { currentIndex: {
switch (selectedBarConfig?.scrollYBehavior || "workspace") { switch (selectedBarConfig?.scrollYBehavior || "workspace") {
@@ -1691,6 +1725,7 @@ Item {
SettingsButtonGroupRow { SettingsButtonGroupRow {
text: I18n.tr("X Axis") text: I18n.tr("X Axis")
description: I18n.tr("Action performed when scrolling horizontally on the bar")
visible: CompositorService.isNiri visible: CompositorService.isNiri
model: [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")] model: [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")]
currentIndex: { currentIndex: {
+6 -6
View File
@@ -90,13 +90,13 @@ Item {
} }
SettingsToggleRow { SettingsToggleRow {
settingKey: "dockHideOnFullscreen" settingKey: "dockUseOverlayLayer"
tags: ["dock", "fullscreen", "hide"] tags: ["dock", "fullscreen", "overlay", "layer"]
text: I18n.tr("Hide When Fullscreen", "dock visibility toggle: hide the dock when a window is fullscreen") text: I18n.tr("Use Overlay Layer", "dock layer toggle: use Wayland overlay layer")
description: I18n.tr("Hide the dock when a window is fullscreen", "dock visibility toggle description") description: I18n.tr("Place the dock on the Wayland overlay layer")
checked: SettingsData.dockHideOnFullscreen checked: SettingsData.dockUseOverlayLayer
visible: SettingsData.showDock visible: SettingsData.showDock
onToggled: checked => SettingsData.set("dockHideOnFullscreen", checked) onToggled: checked => SettingsData.set("dockUseOverlayLayer", checked)
} }
} }
-9
View File
@@ -308,15 +308,6 @@ Item {
onToggled: checked => SettingsData.set("frameCloseGaps", !checked) onToggled: checked => SettingsData.set("frameCloseGaps", !checked)
} }
SettingsToggleRow {
settingKey: "frameUseSpotlightLauncher"
tags: ["frame", "connected", "launcher", "spotlight", "search", "minimal"]
text: I18n.tr("Use Spotlight Launcher")
description: I18n.tr("Use the centered minimal launcher instead of the connected V2 launcher")
checked: SettingsData.frameUseSpotlightLauncher
onToggled: checked => SettingsData.set("frameUseSpotlightLauncher", checked)
}
SettingsButtonGroupRow { SettingsButtonGroupRow {
settingKey: "frameLauncherEmergeSide" settingKey: "frameLauncherEmergeSide"
tags: ["frame", "connected", "launcher", "modal", "emerge", "direction", "bottom", "top"] tags: ["frame", "connected", "launcher", "modal", "emerge", "direction", "bottom", "top"]
+24 -2
View File
@@ -16,6 +16,7 @@ Item {
property var parentModal: null property var parentModal: null
property string selectedCategory: "" property string selectedCategory: ""
property string searchQuery: "" property string searchQuery: ""
property string requestedSearchQuery: ""
property string expandedKey: "" property string expandedKey: ""
property bool showingNewBind: false property bool showingNewBind: false
@@ -206,13 +207,34 @@ Item {
} }
} }
Component.onCompleted: _ensureCurrentProvider() function _applyRequestedSearch() {
if (!requestedSearchQuery)
return;
const query = requestedSearchQuery;
selectedCategory = "";
searchField.text = query;
searchQuery = query;
_updateFiltered();
if (parentModal?.keybindSearchQuery === query)
parentModal.keybindSearchQuery = "";
Qt.callLater(scrollToTop);
}
Component.onCompleted: {
_ensureCurrentProvider();
Qt.callLater(_applyRequestedSearch);
}
onRequestedSearchQueryChanged: Qt.callLater(_applyRequestedSearch)
onVisibleChanged: { onVisibleChanged: {
if (!visible) if (!visible)
return; return;
Qt.callLater(scrollToTop);
_ensureCurrentProvider(); _ensureCurrentProvider();
Qt.callLater(() => {
_applyRequestedSearch();
scrollToTop();
});
} }
DankFlickable { DankFlickable {
+219 -6
View File
@@ -9,6 +9,37 @@ Item {
id: root id: root
property var parentModal: null property var parentModal: null
readonly property string defaultLauncherAction: "spawn dms ipc call spotlight toggle"
readonly property string spotlightBarAction: "spawn dms ipc call spotlight-bar toggle"
readonly property int keybindDataVersion: KeybindsService._dataVersion
readonly property bool keybindsAvailable: KeybindsService.available
readonly property string defaultLauncherKeybindSearch: "spotlight toggle"
readonly property string spotlightBarKeybindSearch: "spotlight-bar"
function openKeybindsSearch(query) {
if (!root.parentModal)
return;
if (typeof root.parentModal.showKeybindsSearch === "function") {
root.parentModal.showKeybindsSearch(query);
} else {
root.parentModal.showWithTabName("keybinds");
}
}
function keysLabel(actionId) {
void (keybindDataVersion);
if (!keybindsAvailable)
return I18n.tr("Manual config");
const keys = KeybindsService.keysForAction(actionId);
if (!keys || keys.length === 0)
return I18n.tr("Not bound");
return keys.join(", ");
}
Component.onCompleted: {
if (KeybindsService.available)
KeybindsService.loadBinds(false);
}
FileBrowserModal { FileBrowserModal {
id: logoFileBrowser id: logoFileBrowser
@@ -35,20 +66,20 @@ Item {
SettingsCard { SettingsCard {
width: parent.width width: parent.width
iconName: "search" iconName: "search"
title: I18n.tr("Launcher Style") title: I18n.tr("Default Launcher")
settingKey: "launcherStyle" settingKey: "launcherStyle"
SettingsControlledByFrame { SettingsControlledByFrame {
visible: SettingsData.connectedFrameModeActive visible: SettingsData.connectedFrameModeActive
parentModal: root.parentModal parentModal: root.parentModal
settingLabel: I18n.tr("Launcher Style") settingLabel: I18n.tr("Default Launcher")
reason: I18n.tr("Managed by Frame Mode") reason: I18n.tr("Connected Frame Mode uses the connected launcher for default launcher shortcuts.")
} }
StyledText { StyledText {
width: parent.width width: parent.width
visible: !SettingsData.connectedFrameModeActive visible: !SettingsData.connectedFrameModeActive
text: SettingsData.launcherStyle === "spotlight" ? I18n.tr("Minimal Spotlight-style bar: appears instantly at the top of the screen and expands as you type.") : I18n.tr("Full-featured launcher with mode tabs, grid view, and action panel.") text: SettingsData.launcherStyle === "spotlight" ? I18n.tr("Default launcher shortcuts open the minimal Spotlight Bar. The dedicated Spotlight Bar shortcut below stays independent.") : I18n.tr("Default launcher shortcuts open the full launcher with mode tabs, grid view, and action panel.")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
@@ -57,8 +88,8 @@ Item {
SettingsButtonGroupRow { SettingsButtonGroupRow {
visible: !SettingsData.connectedFrameModeActive visible: !SettingsData.connectedFrameModeActive
settingKey: "launcherStyleSelector" settingKey: "launcherStyleSelector"
tags: ["launcher", "style", "spotlight", "full", "minimal"] tags: ["launcher", "style", "default", "spotlight", "full", "minimal"]
text: I18n.tr("Style") text: I18n.tr("Default Opens")
model: [I18n.tr("Full"), I18n.tr("Spotlight")] model: [I18n.tr("Full"), I18n.tr("Spotlight")]
currentIndex: SettingsData.launcherStyle === "spotlight" ? 1 : 0 currentIndex: SettingsData.launcherStyle === "spotlight" ? 1 : 0
onSelectionChanged: (index, selected) => { onSelectionChanged: (index, selected) => {
@@ -67,6 +98,179 @@ Item {
SettingsData.set("launcherStyle", index === 1 ? "spotlight" : "full"); SettingsData.set("launcherStyle", index === 1 ? "spotlight" : "full");
} }
} }
StyledRect {
id: defaultShortcutCard
width: parent.width
height: defaultShortcutRow.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: defaultShortcutMouse.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, 0.48) : Theme.withAlpha(Theme.surfaceContainer, 0.35)
border.color: Theme.outlineMedium
border.width: 1
Row {
id: defaultShortcutRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
DankIcon {
name: "keyboard"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: Math.max(0, parent.width - Theme.iconSize - defaultShortcutValue.width - Theme.spacingM * 2)
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: I18n.tr("Default Launcher Shortcut")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
StyledText {
text: !root.keybindsAvailable ? I18n.tr("Bind the spotlight IPC action in your compositor config.") : SettingsData.connectedFrameModeActive ? I18n.tr("Opens the connected launcher in Connected Frame Mode.") : I18n.tr("Follows the default launcher choice selected above.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
}
}
StyledText {
id: defaultShortcutValue
text: root.keysLabel(root.defaultLauncherAction)
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignRight
width: Math.min(170, implicitWidth)
elide: Text.ElideRight
}
}
MouseArea {
id: defaultShortcutMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.openKeybindsSearch(root.defaultLauncherKeybindSearch)
}
}
SettingsToggleRow {
settingKey: "launcherUseOverlayLayer"
tags: ["launcher", "fullscreen", "overlay", "layer"]
text: I18n.tr("Use Overlay Layer", "launcher layer toggle: use Wayland overlay layer")
description: I18n.tr("Use the overlay layer when opening the launcher")
checked: SettingsData.launcherUseOverlayLayer
onToggled: checked => SettingsData.set("launcherUseOverlayLayer", checked)
}
}
SettingsCard {
width: parent.width
iconName: "search"
title: I18n.tr("Spotlight Bar")
settingKey: "spotlightBarLauncher"
StyledText {
width: parent.width
text: I18n.tr("A separate minimal launcher action that works in Standalone, Separate Frame Mode, and Connected Frame Mode.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
StyledRect {
id: spotlightShortcutCard
width: parent.width
height: spotlightShortcutRow.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: spotlightShortcutMouse.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, 0.48) : Theme.withAlpha(Theme.surfaceContainer, 0.35)
border.color: Theme.outlineMedium
border.width: 1
Row {
id: spotlightShortcutRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
DankIcon {
name: "keyboard"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: Math.max(0, parent.width - Theme.iconSize - spotlightShortcutValue.width - Theme.spacingM * 2)
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: I18n.tr("Spotlight Bar Shortcut")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
StyledText {
text: !root.keybindsAvailable ? I18n.tr("Bind the spotlight-bar IPC action in your compositor config.") : I18n.tr("Uses the spotlight-bar IPC action and always opens the minimal bar.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
}
}
StyledText {
id: spotlightShortcutValue
text: root.keysLabel(root.spotlightBarAction)
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignRight
width: Math.min(170, implicitWidth)
elide: Text.ElideRight
}
}
MouseArea {
id: spotlightShortcutMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.openKeybindsSearch(root.spotlightBarKeybindSearch)
}
}
SettingsToggleRow {
settingKey: "spotlightBarShowModeChips"
tags: ["launcher", "spotlight", "bar", "chips", "tabs", "modes"]
text: I18n.tr("Show Mode Chips")
description: I18n.tr("Show All, Apps, Files, and Plugins chips beside the Spotlight Bar input.")
checked: SettingsData.spotlightBarShowModeChips
onToggled: checked => SettingsData.set("spotlightBarShowModeChips", checked)
}
} }
SettingsCard { SettingsCard {
@@ -917,6 +1121,15 @@ Item {
onToggled: checked => SessionData.setSearchAppActions(checked) onToggled: checked => SessionData.setSearchAppActions(checked)
} }
SettingsToggleRow {
settingKey: "rememberLastMode"
tags: ["launcher", "remember", "last", "mode", "tab"]
text: I18n.tr("Remember Last Mode")
description: I18n.tr("Restore the last selected mode (tab) when the launcher is opened")
checked: SettingsData.rememberLastMode
onToggled: checked => SettingsData.set("rememberLastMode", checked)
}
SettingsToggleRow { SettingsToggleRow {
settingKey: "rememberLastQuery" settingKey: "rememberLastQuery"
tags: ["launcher", "remember", "last", "search", "query"] tags: ["launcher", "remember", "last", "search", "query"]
@@ -273,6 +273,17 @@ Item {
onToggled: checked => SettingsData.set("notificationCompactMode", checked) onToggled: checked => SettingsData.set("notificationCompactMode", checked)
} }
SettingsToggleRow {
settingKey: "notificationDedupeEnabled"
tags: ["notification", "duplicate", "dedupe", "stack", "coalesce", "repeat"]
text: I18n.tr("Suppress Duplicate Notifications")
description: SettingsData.notificationDedupeEnabled
? I18n.tr("Identical alerts show as one popup instead of stacking")
: I18n.tr("Identical alerts stack as separate notification cards")
checked: SettingsData.notificationDedupeEnabled
onToggled: checked => SettingsData.set("notificationDedupeEnabled", checked)
}
SettingsToggleRow { SettingsToggleRow {
settingKey: "notificationPopupShadowEnabled" settingKey: "notificationPopupShadowEnabled"
tags: ["notification", "popup", "shadow", "radius", "rounded"] tags: ["notification", "popup", "shadow", "radius", "rounded"]
@@ -455,6 +455,11 @@ Item {
label: I18n.tr("Show Restart DMS"), label: I18n.tr("Show Restart DMS"),
desc: I18n.tr("Restart the DankMaterialShell") desc: I18n.tr("Restart the DankMaterialShell")
}, },
{
key: "switchuser",
label: I18n.tr("Show Switch User"),
desc: I18n.tr("Opens a picker of other active sessions on this seat")
},
{ {
key: "hibernate", key: "hibernate",
label: I18n.tr("Show Hibernate"), label: I18n.tr("Show Hibernate"),
+414
View File
@@ -0,0 +1,414 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Modals.Common
import qs.Services
import qs.Widgets
import qs.Modules.Settings.Widgets
Item {
id: root
property string statusText: ""
property bool statusIsError: false
property bool operationPending: false
property string pendingUsername: ""
property string pendingPassword: ""
property string pendingConfirm: ""
property bool pendingAdmin: false
function _resetForm() {
pendingUsername = "";
pendingPassword = "";
pendingConfirm = "";
pendingAdmin = false;
usernameField.text = "";
passwordField.text = "";
confirmField.text = "";
}
function _passwordsMatch() {
return pendingPassword.length > 0 && pendingPassword === pendingConfirm;
}
function _createCanProceed() {
return !operationPending && UsersService.isValidUsername(pendingUsername) && !UsersService.userExists(pendingUsername) && _passwordsMatch();
}
Connections {
target: UsersService
function onOperationCompleted(op, username, success, message) {
root.operationPending = false;
root.statusIsError = !success;
if (success) {
root.statusText = message + (username ? (" — " + username) : "");
if (op === "create")
root._resetForm();
} else {
root.statusText = (username ? (username + ": ") : "") + message;
}
}
}
ConfirmModal {
id: deleteUserConfirm
}
ConfirmModal {
id: adminToggleConfirm
}
DankFlickable {
anchors.fill: parent
clip: true
contentHeight: mainColumn.height + Theme.spacingXL
contentWidth: width
Column {
id: mainColumn
topPadding: 4
width: Math.min(600, parent.width - Theme.spacingL * 2)
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXL
StyledText {
width: parent.width
visible: !PolkitService.polkitAvailable
text: I18n.tr("Polkit integration is disabled. User management requires Polkit to elevate privileges.")
font.pixelSize: Theme.fontSizeMedium
color: Theme.error
wrapMode: Text.WordWrap
}
SettingsCard {
width: parent.width
iconName: "group"
title: I18n.tr("Existing Users")
settingKey: "usersList"
visible: PolkitService.polkitAvailable
Row {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Administrator group:")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: UsersService.adminGroup
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: Theme.spacingM
height: 1
}
StyledText {
text: UsersService.refreshing ? I18n.tr("Refreshing…") : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
}
Repeater {
model: UsersService.users
Rectangle {
id: userRow
required property var modelData
width: parent.width
height: Math.max(48, rowContent.implicitHeight + Theme.spacingS * 2)
radius: Theme.cornerRadius
color: Theme.surfaceContainerHighest
readonly property bool isLastAdmin: modelData.isAdmin && UsersService.adminMembers.length <= 1
Row {
id: rowContent
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
DankIcon {
name: "account_circle"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - actionButtons.width - Theme.spacingM * 3
spacing: 2
anchors.verticalCenter: parent.verticalCenter
Row {
spacing: Theme.spacingS
StyledText {
text: userRow.modelData.username
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
visible: userRow.modelData.isAdmin
width: adminChipText.implicitWidth + Theme.spacingS * 2
height: adminChipText.implicitHeight + Theme.spacingXS * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.primary, 0.15)
anchors.verticalCenter: parent.verticalCenter
StyledText {
id: adminChipText
anchors.centerIn: parent
text: I18n.tr("admin")
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
}
}
}
StyledText {
text: userRow.modelData.gecos && userRow.modelData.gecos.length > 0 ? userRow.modelData.gecos + " · UID " + userRow.modelData.uid : "UID " + userRow.modelData.uid
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
}
}
Row {
id: actionButtons
spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
DankActionButton {
id: adminToggleBtn
readonly property bool actionBlocked: root.operationPending || (userRow.isLastAdmin && userRow.modelData.isAdmin)
buttonSize: 36
iconSize: 20
iconName: userRow.modelData.isAdmin ? "shield_person" : "shield"
iconColor: userRow.modelData.isAdmin ? Theme.primary : Theme.surfaceVariantText
opacity: actionBlocked ? 0.4 : 1.0
tooltipText: (userRow.isLastAdmin && userRow.modelData.isAdmin) ? I18n.tr("Cannot remove the only administrator") : (userRow.modelData.isAdmin ? I18n.tr("Remove admin") : I18n.tr("Make admin"))
tooltipSide: "left"
onClicked: {
if (actionBlocked)
return;
const makeAdmin = !userRow.modelData.isAdmin;
adminToggleConfirm.showWithOptions({
title: makeAdmin ? I18n.tr("Grant admin?") : I18n.tr("Remove admin?"),
message: makeAdmin ? I18n.tr("Add \"%1\" to the %2 group?").arg(userRow.modelData.username).arg(UsersService.adminGroup) : I18n.tr("Remove \"%1\" from the %2 group?").arg(userRow.modelData.username).arg(UsersService.adminGroup),
confirmText: makeAdmin ? I18n.tr("Grant") : I18n.tr("Remove"),
confirmColor: Theme.primary,
onConfirm: () => {
root.operationPending = true;
root.statusText = "";
UsersService.setAdmin(userRow.modelData.username, makeAdmin, null);
}
});
}
}
DankActionButton {
id: deleteBtn
readonly property bool actionBlocked: root.operationPending || !UsersService.canDelete(userRow.modelData.username)
buttonSize: 36
iconSize: 20
iconName: "delete"
iconColor: Theme.error
opacity: actionBlocked ? 0.4 : 1.0
tooltipText: userRow.isLastAdmin ? I18n.tr("Cannot delete the only administrator") : I18n.tr("Delete user")
tooltipSide: "left"
onClicked: {
if (actionBlocked)
return;
deleteUserConfirm.showWithOptions({
title: I18n.tr("Delete user?"),
message: I18n.tr("Delete \"%1\" and remove the home directory? This cannot be undone.").arg(userRow.modelData.username),
confirmText: I18n.tr("Delete"),
confirmColor: Theme.primary,
onConfirm: () => {
root.operationPending = true;
root.statusText = "";
UsersService.deleteUser(userRow.modelData.username, null);
}
});
}
}
}
}
}
}
StyledText {
width: parent.width
visible: UsersService.users.length === 0 && !UsersService.refreshing
text: I18n.tr("No human user accounts found.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
SettingsCard {
width: parent.width
iconName: "person_add"
title: I18n.tr("Create User")
settingKey: "createUser"
visible: PolkitService.polkitAvailable
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Username")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: usernameField
width: parent.width
placeholderText: I18n.tr("e.g. alice")
backgroundColor: Theme.surfaceContainerHighest
normalBorderColor: usernameInvalid ? Theme.error : Theme.outlineMedium
focusedBorderColor: usernameInvalid ? Theme.error : Theme.primary
readonly property bool usernameInvalid: text.length > 0 && (!UsersService.isValidUsername(text) || UsersService.userExists(text))
onTextEdited: {
root.pendingUsername = text.trim();
}
}
StyledText {
width: parent.width
visible: usernameField.text.length > 0 && !UsersService.isValidUsername(usernameField.text)
text: I18n.tr("Username must start with a lowercase letter or underscore and contain only lowercase letters, digits, hyphens, or underscores.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
wrapMode: Text.WordWrap
}
StyledText {
width: parent.width
visible: usernameField.text.length > 0 && UsersService.isValidUsername(usernameField.text) && UsersService.userExists(usernameField.text)
text: I18n.tr("A user with that name already exists.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
wrapMode: Text.WordWrap
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Password")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: passwordField
width: parent.width
placeholderText: I18n.tr("Set initial password")
echoMode: TextInput.Password
showPasswordToggle: true
backgroundColor: Theme.surfaceContainerHighest
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
onTextEdited: root.pendingPassword = text
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Confirm password")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: confirmField
width: parent.width
placeholderText: I18n.tr("Re-enter password")
echoMode: TextInput.Password
showPasswordToggle: true
backgroundColor: Theme.surfaceContainerHighest
normalBorderColor: confirmMismatch ? Theme.error : Theme.outlineMedium
focusedBorderColor: confirmMismatch ? Theme.error : Theme.primary
readonly property bool confirmMismatch: text.length > 0 && text !== passwordField.text
onTextEdited: root.pendingConfirm = text
}
StyledText {
width: parent.width
visible: confirmField.text.length > 0 && confirmField.text !== passwordField.text
text: I18n.tr("Passwords do not match.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
}
}
SettingsToggleRow {
settingKey: "createUserAdmin"
tags: ["user", "admin", "sudo", "wheel"]
text: I18n.tr("Grant administrator privileges")
description: I18n.tr("Add the new user to the %1 group so they can use sudo.").arg(UsersService.adminGroup)
checked: root.pendingAdmin
onToggled: checked => root.pendingAdmin = checked
}
Row {
width: parent.width
spacing: Theme.spacingM
DankButton {
text: root.operationPending ? I18n.tr("Working…") : I18n.tr("Create User")
iconName: "person_add"
backgroundColor: Theme.primary
textColor: Theme.primaryText
enabled: root._createCanProceed()
onClicked: {
if (!root._createCanProceed())
return;
root.operationPending = true;
root.statusText = "";
UsersService.createUser(root.pendingUsername, root.pendingPassword, root.pendingAdmin, null);
}
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: root.statusText
color: root.statusIsError ? Theme.error : Theme.primary
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.WordWrap
width: parent.width - parent.children[0].width - Theme.spacingM
}
}
}
}
}
}
+35 -4
View File
@@ -431,7 +431,7 @@ Item {
"id": widget.id, "id": widget.id,
"enabled": widget.enabled "enabled": widget.enabled
}; };
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion", "hideWhenIdle"]; var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowSize", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion", "hideWhenIdle"];
for (var i = 0; i < keys.length; i++) { for (var i = 0; i < keys.length; i++) {
if (widget[keys[i]] !== undefined) if (widget[keys[i]] !== undefined)
result[keys[i]] = widget[keys[i]]; result[keys[i]] = widget[keys[i]];
@@ -625,9 +625,6 @@ Item {
var newWidget = cloneWidgetData(widget); var newWidget = cloneWidgetData(widget);
switch (widgetId) { switch (widgetId) {
case "music":
newWidget.mediaSize = value;
break;
case "clock": case "clock":
newWidget.clockCompactMode = value; newWidget.clockCompactMode = value;
break; break;
@@ -647,6 +644,29 @@ Item {
setWidgetsForSection(sectionId, widgets); setWidgetsForSection(sectionId, widgets);
} }
function handleWidgetSizeChanged(sectionId, widgetId, value) {
var widgets = getWidgetsForSection(sectionId).slice();
for (var i = 0; i < widgets.length; i++) {
var widget = widgets[i];
var currentId = typeof widget === "string" ? widget : widget.id;
if (currentId !== widgetId)
continue;
var newWidget = cloneWidgetData(widget);
switch (widgetId) {
case "music":
newWidget.mediaSize = value;
break;
case "focusedWindow":
newWidget.focusedWindowSize = value;
break;
}
widgets[i] = newWidget;
break;
}
setWidgetsForSection(sectionId, widgets);
}
function getItemsForSection(sectionId) { function getItemsForSection(sectionId) {
var widgets = []; var widgets = [];
var widgetData = getWidgetsForSection(sectionId); var widgetData = getWidgetsForSection(sectionId);
@@ -708,6 +728,8 @@ Item {
item.clockCompactMode = widget.clockCompactMode; item.clockCompactMode = widget.clockCompactMode;
if (widget.focusedWindowCompactMode !== undefined) if (widget.focusedWindowCompactMode !== undefined)
item.focusedWindowCompactMode = widget.focusedWindowCompactMode; item.focusedWindowCompactMode = widget.focusedWindowCompactMode;
if (widget.focusedWindowSize !== undefined)
item.focusedWindowSize = widget.focusedWindowSize;
if (widget.runningAppsCompactMode !== undefined) if (widget.runningAppsCompactMode !== undefined)
item.runningAppsCompactMode = widget.runningAppsCompactMode; item.runningAppsCompactMode = widget.runningAppsCompactMode;
if (widget.runningAppsGroupByApp !== undefined) if (widget.runningAppsGroupByApp !== undefined)
@@ -1014,6 +1036,9 @@ Item {
onCompactModeChanged: (widgetId, value) => { onCompactModeChanged: (widgetId, value) => {
widgetsTab.handleCompactModeChanged(sectionId, widgetId, value); widgetsTab.handleCompactModeChanged(sectionId, widgetId, value);
} }
onWidgetSizeChanged: (widgetId, value) => {
widgetsTab.handleWidgetSizeChanged(sectionId, widgetId, value);
}
onOverflowSettingChanged: (sectionId, widgetIndex, settingName, value) => { onOverflowSettingChanged: (sectionId, widgetIndex, settingName, value) => {
widgetsTab.handleOverflowSettingChanged(sectionId, widgetIndex, settingName, value); widgetsTab.handleOverflowSettingChanged(sectionId, widgetIndex, settingName, value);
} }
@@ -1084,6 +1109,9 @@ Item {
onCompactModeChanged: (widgetId, value) => { onCompactModeChanged: (widgetId, value) => {
widgetsTab.handleCompactModeChanged(sectionId, widgetId, value); widgetsTab.handleCompactModeChanged(sectionId, widgetId, value);
} }
onWidgetSizeChanged: (widgetId, value) => {
widgetsTab.handleWidgetSizeChanged(sectionId, widgetId, value);
}
onOverflowSettingChanged: (sectionId, widgetIndex, settingName, value) => { onOverflowSettingChanged: (sectionId, widgetIndex, settingName, value) => {
widgetsTab.handleOverflowSettingChanged(sectionId, widgetIndex, settingName, value); widgetsTab.handleOverflowSettingChanged(sectionId, widgetIndex, settingName, value);
} }
@@ -1154,6 +1182,9 @@ Item {
onCompactModeChanged: (widgetId, value) => { onCompactModeChanged: (widgetId, value) => {
widgetsTab.handleCompactModeChanged(sectionId, widgetId, value); widgetsTab.handleCompactModeChanged(sectionId, widgetId, value);
} }
onWidgetSizeChanged: (widgetId, value) => {
widgetsTab.handleWidgetSizeChanged(sectionId, widgetId, value);
}
onOverflowSettingChanged: (sectionId, widgetIndex, settingName, value) => { onOverflowSettingChanged: (sectionId, widgetIndex, settingName, value) => {
widgetsTab.handleOverflowSettingChanged(sectionId, widgetIndex, settingName, value); widgetsTab.handleOverflowSettingChanged(sectionId, widgetIndex, settingName, value);
} }
+206 -12
View File
@@ -24,6 +24,7 @@ Column {
signal removeWidget(string sectionId, int widgetIndex) signal removeWidget(string sectionId, int widgetIndex)
signal spacerSizeChanged(string sectionId, int widgetIndex, int newSize) signal spacerSizeChanged(string sectionId, int widgetIndex, int newSize)
signal compactModeChanged(string widgetId, var value) signal compactModeChanged(string widgetId, var value)
signal widgetSizeChanged(string widgetId, var value)
signal gpuSelectionChanged(string sectionId, int widgetIndex, int selectedIndex) signal gpuSelectionChanged(string sectionId, int widgetIndex, int selectedIndex)
signal diskMountSelectionChanged(string sectionId, int widgetIndex, string mountPath) signal diskMountSelectionChanged(string sectionId, int widgetIndex, string mountPath)
signal controlCenterSettingChanged(string sectionId, int widgetIndex, string settingName, bool value) signal controlCenterSettingChanged(string sectionId, int widgetIndex, string settingName, bool value)
@@ -41,7 +42,7 @@ Column {
"id": widget.id, "id": widget.id,
"enabled": widget.enabled "enabled": widget.enabled
}; };
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion"]; var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowSize", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion"];
for (var i = 0; i < keys.length; i++) { for (var i = 0; i < keys.length; i++) {
if (widget[keys[i]] !== undefined) if (widget[keys[i]] !== undefined)
result[keys[i]] = widget[keys[i]]; result[keys[i]] = widget[keys[i]];
@@ -390,6 +391,39 @@ Column {
} }
} }
DankActionButton {
id: focusedWindowMenuButton
buttonSize: 32
visible: modelData.id === "focusedWindow"
iconName: "more_vert"
iconSize: 18
iconColor: Theme.outline
onClicked: {
focusedWindowContextMenu.widgetData = modelData;
focusedWindowContextMenu.sectionId = root.sectionId;
focusedWindowContextMenu.widgetIndex = index;
var buttonPos = focusedWindowMenuButton.mapToItem(root, 0, 0);
var popupWidth = focusedWindowContextMenu.width;
var popupHeight = focusedWindowContextMenu.height;
var xPos = buttonPos.x - popupWidth - Theme.spacingS;
if (xPos < 0)
xPos = buttonPos.x + focusedWindowMenuButton.width + Theme.spacingS;
var yPos = buttonPos.y - popupHeight / 2 + focusedWindowMenuButton.height / 2;
if (yPos < 0) {
yPos = Theme.spacingS;
} else if (yPos + popupHeight > root.height) {
yPos = root.height - popupHeight - Theme.spacingS;
}
focusedWindowContextMenu.x = xPos;
focusedWindowContextMenu.y = yPos;
focusedWindowContextMenu.open();
}
}
DankActionButton { DankActionButton {
id: musicMenuButton id: musicMenuButton
visible: modelData.id === "music" visible: modelData.id === "music"
@@ -458,19 +492,17 @@ Column {
Row { Row {
spacing: Theme.spacingXS spacing: Theme.spacingXS
visible: modelData.id === "clock" || modelData.id === "focusedWindow" || modelData.id === "keyboard_layout_name" || modelData.id === "appsDock" || modelData.id === "systemTray" visible: modelData.id === "clock" || modelData.id === "keyboard_layout_name" || modelData.id === "appsDock" || modelData.id === "systemTray"
DankActionButton { DankActionButton {
id: compactModeButton id: compactModeButton
buttonSize: 28 buttonSize: 28
visible: modelData.id === "clock" || modelData.id === "focusedWindow" || modelData.id === "keyboard_layout_name" visible: modelData.id === "clock" || modelData.id === "keyboard_layout_name"
iconName: { iconName: {
const isCompact = (() => { const isCompact = (() => {
switch (modelData.id) { switch (modelData.id) {
case "clock": case "clock":
return modelData.clockCompactMode !== undefined ? modelData.clockCompactMode : SettingsData.clockCompactMode; return modelData.clockCompactMode !== undefined ? modelData.clockCompactMode : SettingsData.clockCompactMode;
case "focusedWindow":
return modelData.focusedWindowCompactMode !== undefined ? modelData.focusedWindowCompactMode : SettingsData.focusedWindowCompactMode;
case "keyboard_layout_name": case "keyboard_layout_name":
return modelData.keyboardLayoutNameCompactMode !== undefined ? modelData.keyboardLayoutNameCompactMode : SettingsData.keyboardLayoutNameCompactMode; return modelData.keyboardLayoutNameCompactMode !== undefined ? modelData.keyboardLayoutNameCompactMode : SettingsData.keyboardLayoutNameCompactMode;
default: default:
@@ -485,8 +517,6 @@ Column {
switch (modelData.id) { switch (modelData.id) {
case "clock": case "clock":
return modelData.clockCompactMode !== undefined ? modelData.clockCompactMode : SettingsData.clockCompactMode; return modelData.clockCompactMode !== undefined ? modelData.clockCompactMode : SettingsData.clockCompactMode;
case "focusedWindow":
return modelData.focusedWindowCompactMode !== undefined ? modelData.focusedWindowCompactMode : SettingsData.focusedWindowCompactMode;
case "keyboard_layout_name": case "keyboard_layout_name":
return modelData.keyboardLayoutNameCompactMode !== undefined ? modelData.keyboardLayoutNameCompactMode : SettingsData.keyboardLayoutNameCompactMode; return modelData.keyboardLayoutNameCompactMode !== undefined ? modelData.keyboardLayoutNameCompactMode : SettingsData.keyboardLayoutNameCompactMode;
default: default:
@@ -500,8 +530,6 @@ Column {
switch (modelData.id) { switch (modelData.id) {
case "clock": case "clock":
return modelData.clockCompactMode !== undefined ? modelData.clockCompactMode : SettingsData.clockCompactMode; return modelData.clockCompactMode !== undefined ? modelData.clockCompactMode : SettingsData.clockCompactMode;
case "focusedWindow":
return modelData.focusedWindowCompactMode !== undefined ? modelData.focusedWindowCompactMode : SettingsData.focusedWindowCompactMode;
case "keyboard_layout_name": case "keyboard_layout_name":
return modelData.keyboardLayoutNameCompactMode !== undefined ? modelData.keyboardLayoutNameCompactMode : SettingsData.keyboardLayoutNameCompactMode; return modelData.keyboardLayoutNameCompactMode !== undefined ? modelData.keyboardLayoutNameCompactMode : SettingsData.keyboardLayoutNameCompactMode;
default: default:
@@ -515,8 +543,6 @@ Column {
switch (modelData.id) { switch (modelData.id) {
case "clock": case "clock":
return modelData.clockCompactMode !== undefined ? modelData.clockCompactMode : SettingsData.clockCompactMode; return modelData.clockCompactMode !== undefined ? modelData.clockCompactMode : SettingsData.clockCompactMode;
case "focusedWindow":
return modelData.focusedWindowCompactMode !== undefined ? modelData.focusedWindowCompactMode : SettingsData.focusedWindowCompactMode;
case "keyboard_layout_name": case "keyboard_layout_name":
return modelData.keyboardLayoutNameCompactMode !== undefined ? modelData.keyboardLayoutNameCompactMode : SettingsData.keyboardLayoutNameCompactMode; return modelData.keyboardLayoutNameCompactMode !== undefined ? modelData.keyboardLayoutNameCompactMode : SettingsData.keyboardLayoutNameCompactMode;
default: default:
@@ -1067,6 +1093,174 @@ Column {
} }
} }
Popup {
id: focusedWindowContextMenu
property var widgetData: null
property string sectionId: ""
property int widgetIndex: -1
width: 180
height: focusedWindowMenuColumn.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: focusedWindowMenuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 2
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: fwCompactArea.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: "zoom_in"
size: 16
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Compact")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
id: fwCompactToggle
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
width: 40
height: 20
checked: focusedWindowContextMenu.currentWidgetData?.focusedWindowCompactMode ?? SettingsData.focusedWindowCompactMode
onToggled: {
root.overflowSettingChanged(focusedWindowContextMenu.sectionId, focusedWindowContextMenu.widgetIndex, "focuswedWindowCompactMode", toggled);
}
}
MouseArea {
id: fwCompactArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
fwCompactToggle.checked = !fwCompactToggle.checked;
root.overflowSettingChanged(focusedWindowContextMenu.sectionId, focusedWindowContextMenu.widgetIndex, "focusedWindowCompactMode", fwCompactToggle.checked);
}
}
}
Repeater {
model: [
{
icon: "photo_size_select_small",
label: I18n.tr("Small"),
sizeValue: 0
},
{
icon: "photo_size_select_actual",
label: I18n.tr("Medium"),
sizeValue: 1
},
{
icon: "photo_size_select_large",
label: I18n.tr("Large"),
sizeValue: 2
},
{
icon: "fit_screen",
label: I18n.tr("Largest"),
sizeValue: 3
}
]
delegate: Rectangle {
required property var modelData
required property int index
function isSelected() {
var wd = focusedWindowContextMenu.widgetData;
var currentSize = wd?.focusedWindowSize ?? SettingsData.focusedWindowSize;
return currentSize === modelData.sizeValue;
}
width: focusedWindowMenuColumn.width
height: Math.max(18, Theme.fontSizeSmall) + Theme.spacingM * 2
radius: Theme.cornerRadius
color: focusedWindowOptionArea.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: modelData.icon
size: 18
color: isSelected() ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: modelData.label
font.pixelSize: Theme.fontSizeSmall
font.weight: isSelected() ? Font.Medium : Font.Normal
color: isSelected() ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankIcon {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
name: "check"
size: 16
color: Theme.primary
visible: isSelected()
}
MouseArea {
id: focusedWindowOptionArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.widgetSizeChanged("focusedWindow", modelData.sizeValue);
focusedWindowContextMenu.close();
}
}
}
}
}
}
}
Popup { Popup {
id: diskUsageContextMenu id: diskUsageContextMenu
@@ -2144,7 +2338,7 @@ Column {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
root.compactModeChanged("music", modelData.sizeValue); root.widgetSizeChanged("music", modelData.sizeValue);
musicContextMenu.close(); musicContextMenu.close();
} }
} }
@@ -338,45 +338,61 @@ Scope {
border.width: 1 border.width: 1
} }
LauncherContent { FocusScope {
id: launcherContent
anchors.fill: parent anchors.fill: parent
anchors.margins: 0 focus: true
property var fakeParentModal: QtObject { Keys.onPressed: event => launcherContent.activeContextMenu?.handleKey(event)
property bool spotlightOpen: spotlightContainer.visible
property bool isClosing: niriOverviewScope.isClosing Keys.onEscapePressed: event => {
function hide() { launcherContent.activeContextMenu?.handleKey(event);
if (niriOverviewScope.searchActive) { if (!event.accepted)
niriOverviewScope.hideSpotlight(); launcherContent.parentModal?.hide();
return; event.accepted = true;
}
LauncherContent {
id: launcherContent
anchors.fill: parent
anchors.margins: 0
property var fakeParentModal: QtObject {
property bool spotlightOpen: spotlightContainer.visible
property bool isClosing: niriOverviewScope.isClosing
property real alignedX: spotlightContainer.x
property real alignedY: spotlightContainer.y
function hide() {
if (niriOverviewScope.searchActive) {
niriOverviewScope.hideSpotlight();
return;
}
NiriService.toggleOverview();
} }
NiriService.toggleOverview();
} }
}
Connections { Connections {
target: launcherContent.searchField target: launcherContent.searchField
function onTextChanged() { function onTextChanged() {
if (launcherContent.searchField.text.length > 0 || !niriOverviewScope.searchActive) if (launcherContent.searchField.text.length > 0 || !niriOverviewScope.searchActive)
return; return;
niriOverviewScope.hideSpotlight(); niriOverviewScope.hideSpotlight();
}
} }
}
Component.onCompleted: { Component.onCompleted: {
parentModal = fakeParentModal; parentModal = fakeParentModal;
}
Connections {
target: launcherContent.controller
function onItemExecuted() {
niriOverviewScope.releaseKeyboard = true;
} }
function onModeChanged(mode) {
if (launcherContent.controller.autoSwitchedToFiles) Connections {
return; target: launcherContent.controller
SessionData.setNiriOverviewLastMode(mode); function onItemExecuted() {
niriOverviewScope.releaseKeyboard = true;
}
function onModeChanged(mode) {
if (launcherContent.controller.autoSwitchedToFiles)
return;
SessionData.setNiriOverviewLastMode(mode);
}
} }
} }
} }
+6 -7
View File
@@ -9,6 +9,7 @@ import qs.Services
Singleton { Singleton {
id: root id: root
readonly property var log: Log.scoped("AppSearchService") readonly property var log: Log.scoped("AppSearchService")
property int refCount: 0
property var applications: [] property var applications: []
property var _cachedCategories: null property var _cachedCategories: null
@@ -296,20 +297,18 @@ Singleton {
function getBuiltInLauncherItems(pluginId, query) { function getBuiltInLauncherItems(pluginId, query) {
if (pluginId === "dms_clipboard_search") { if (pluginId === "dms_clipboard_search") {
ClipboardService.ensureLauncherHistory();
const trimmed = (query || "").toString().trim(); const trimmed = (query || "").toString().trim();
const entries = trimmed.length === 0 ? ClipboardService.getRecentLauncherEntries(20) : ClipboardService.getLauncherEntries(trimmed, 20, 1); const entries = ClipboardService.internalEntries.length > 0 ? ClipboardService.getLauncherEntries(trimmed, 20, 0) : ClipboardService.getCachedLauncherSearchEntries(trimmed, 20);
return entries.map(entry => ({ return entries.map(entry => ({
type: "clipboard", type: "clipboard",
data: entry data: entry
})); }));
} }
if (pluginId !== "dms_settings_search") if (pluginId !== "dms_settings_search")
return []; return [];
SettingsSearchService.search(query); const results = SettingsSearchService.searchForLauncher(query);
const results = SettingsSearchService.results;
const items = []; const items = [];
for (let i = 0; i < results.length; i++) { for (let i = 0; i < results.length; i++) {
const r = results[i]; const r = results[i];
+39 -4
View File
@@ -397,6 +397,14 @@ EOFCONFIG
} }
} }
Connections {
target: root.source?.audio ?? null
function onMutedChanged() {
root.micMuteChanged();
}
}
function checkGsettings() { function checkGsettings() {
Proc.runCommand("checkGsettings", ["sh", "-c", "gsettings get org.gnome.desktop.sound theme-name 2>/dev/null"], (output, exitCode) => { Proc.runCommand("checkGsettings", ["sh", "-c", "gsettings get org.gnome.desktop.sound theme-name 2>/dev/null"], (output, exitCode) => {
gsettingsAvailable = (exitCode === 0); gsettingsAvailable = (exitCode === 0);
@@ -844,6 +852,36 @@ EOFCONFIG
return root.source.audio.muted ? "Microphone muted" : "Microphone unmuted"; return root.source.audio.muted ? "Microphone muted" : "Microphone unmuted";
} }
function incrementMicVolume(step) {
if (!root.source?.audio)
return "No audio source available";
if (root.source.audio.muted)
root.source.audio.muted = false;
const currentVolume = Math.round(root.source.audio.volume * 100);
const stepValue = parseInt(step || "5");
const newVolume = Math.max(0, Math.min(100, currentVolume + stepValue));
root.source.audio.volume = newVolume / 100;
return `Microphone volume increased to ${newVolume}%`;
}
function decrementMicVolume(step) {
if (!root.source?.audio)
return "No audio source available";
if (root.source.audio.muted)
root.source.audio.muted = false;
const currentVolume = Math.round(root.source.audio.volume * 100);
const stepValue = parseInt(step || "5");
const newVolume = Math.max(0, Math.min(100, currentVolume - stepValue));
root.source.audio.volume = newVolume / 100;
return `Microphone volume decreased to ${newVolume}%`;
}
IpcHandler { IpcHandler {
target: "audio" target: "audio"
@@ -892,9 +930,7 @@ EOFCONFIG
} }
function micmute(): string { function micmute(): string {
const result = root.toggleMicMute(); return root.toggleMicMute();
root.micMuteChanged();
return result;
} }
function status(): string { function status(): string {
@@ -957,7 +993,6 @@ EOFCONFIG
return `Switched to: ${result}`; return `Switched to: ${result}`;
} }
} }
Connections { Connections {
target: SettingsData target: SettingsData
function onUseSystemSoundThemeChanged() { function onUseSystemSoundThemeChanged() {
+14
View File
@@ -28,6 +28,20 @@ Singleton {
}); });
return isConnected; return isConnected;
} }
readonly property bool connecting: {
if (!adapter || !adapter.devices) {
return false;
}
let busy = false;
adapter.devices.values.forEach(dev => {
if (!dev)
return;
if (dev.pairing || dev.state === BluetoothDeviceState.Connecting)
busy = true;
});
return busy;
}
readonly property var pairedDevices: { readonly property var pairedDevices: {
if (!adapter || !adapter.devices) { if (!adapter || !adapter.devices) {
return []; return [];
+73
View File
@@ -27,9 +27,14 @@ Singleton {
property bool keyboardNavigationActive: false property bool keyboardNavigationActive: false
property int refCount: 0 property int refCount: 0
property real _launcherLastRefresh: 0 property real _launcherLastRefresh: 0
property bool _launcherCacheValid: false
property string _launcherCachedQuery: ""
property var _launcherCachedEntries: []
property int _launcherSearchSeq: 0
signal historyCopied signal historyCopied
signal historyCleared signal historyCleared
signal launcherSearchReady(string query)
Process { Process {
id: wtypeProcess id: wtypeProcess
@@ -103,6 +108,63 @@ Singleton {
} }
} }
function requestLauncherSearch(query, limit) {
if (!clipboardAvailable) {
return;
}
const trimmed = (query || "").toString().trim();
const maxItems = limit > 0 ? limit : 20;
if (_launcherCacheValid && _launcherCachedQuery === trimmed) {
return;
}
_launcherSearchSeq++;
const seq = _launcherSearchSeq;
DMSService.sendRequest("clipboard.search", {
"query": trimmed,
"limit": maxItems
}, function (response) {
if (seq !== _launcherSearchSeq) {
return;
}
if (response.error) {
log.warn("Launcher clipboard search failed:", response.error);
_launcherCacheValid = true;
_launcherCachedQuery = trimmed;
_launcherCachedEntries = [];
launcherSearchReady(trimmed);
return;
}
const result = response.result || {};
_launcherCacheValid = true;
_launcherCachedQuery = trimmed;
_launcherCachedEntries = result.entries || [];
launcherSearchReady(trimmed);
});
}
function getCachedLauncherSearchEntries(query, limit) {
if (!clipboardAvailable) {
return [];
}
const trimmed = (query || "").toString().trim();
const maxItems = limit > 0 ? limit : 20;
if (!_launcherCacheValid || _launcherCachedQuery !== trimmed) {
requestLauncherSearch(trimmed, maxItems);
return [];
}
return _launcherCachedEntries.slice(0, maxItems);
}
function invalidateLauncherSearchCache() {
_launcherCacheValid = false;
_launcherCachedQuery = "";
_launcherCachedEntries = [];
_launcherSearchSeq++;
}
function getLauncherEntries(query, limit, minLength) { function getLauncherEntries(query, limit, minLength) {
if (!clipboardAvailable) { if (!clipboardAvailable) {
return []; return [];
@@ -178,6 +240,17 @@ Singleton {
}); });
} }
function pasteClipboard(closeCallback) {
if (!wtypeAvailable) {
ToastService.showError(I18n.tr("wtype not available - install wtype for paste support"));
return;
}
if (closeCallback) {
closeCallback();
}
pasteTimer.start();
}
function pasteEntry(entry, closeCallback) { function pasteEntry(entry, closeCallback) {
if (!wtypeAvailable) { if (!wtypeAvailable) {
ToastService.showError(I18n.tr("wtype not available - install wtype for paste support")); ToastService.showError(I18n.tr("wtype not available - install wtype for paste support"));
+247 -1
View File
@@ -36,6 +36,7 @@ Singleton {
signal randrDataReady signal randrDataReady
property var sortedToplevels: [] property var sortedToplevels: []
property var hyprlandVisibleSpecialWorkspaces: ({})
property bool _sortScheduled: false property bool _sortScheduled: false
signal toplevelsChanged signal toplevelsChanged
@@ -153,10 +154,14 @@ Singleton {
enabled: isHyprland enabled: isHyprland
function onRawEvent(event) { function onRawEvent(event) {
if (event.name === "openwindow" || event.name === "closewindow" || event.name === "movewindow" || event.name === "movewindowv2" || event.name === "workspace" || event.name === "workspacev2" || event.name === "focusedmon" || event.name === "focusedmonv2" || event.name === "activewindow" || event.name === "activewindowv2" || event.name === "changefloatingmode" || event.name === "fullscreen" || event.name === "moveintogroup" || event.name === "moveoutofgroup") { if (event.name === "openwindow" || event.name === "closewindow" || event.name === "movewindow" || event.name === "movewindowv2" || event.name === "workspace" || event.name === "workspacev2" || event.name === "focusedmon" || event.name === "focusedmonv2" || event.name === "activewindow" || event.name === "activewindowv2" || event.name === "changefloatingmode" || event.name === "fullscreen" || event.name === "moveintogroup" || event.name === "moveoutofgroup" || event.name === "activespecial") {
try { try {
Hyprland.refreshToplevels(); Hyprland.refreshToplevels();
if (event.name === "workspace" || event.name === "workspacev2" || event.name === "focusedmon" || event.name === "focusedmonv2" || event.name === "activespecial")
Hyprland.refreshMonitors();
} catch (e) {} } catch (e) {}
if (event.name === "activespecial")
root.updateHyprlandVisibleSpecialWorkspaces(event);
root.scheduleSort(); root.scheduleSort();
} }
} }
@@ -171,6 +176,7 @@ Singleton {
Component.onCompleted: { Component.onCompleted: {
fetchRandrData(); fetchRandrData();
detectCompositor(); detectCompositor();
updateHyprlandVisibleSpecialWorkspaces(null);
scheduleSort(); scheduleSort();
Qt.callLater(() => { Qt.callLater(() => {
NiriService.generateNiriLayoutConfig(); NiriService.generateNiriLayoutConfig();
@@ -215,6 +221,81 @@ Singleton {
} }
} }
function _normalizeSpecialWorkspaceName(name) {
const raw = String(name ?? "").trim();
if (raw.length === 0)
return "";
if (raw === "special")
return "special:special";
return raw.startsWith("special:") ? raw : `special:${raw}`;
}
function _hyprlandRawEventParts(event, argumentCount) {
if (!event)
return [];
try {
const parsed = event.parse(argumentCount);
if (parsed && parsed.length !== undefined)
return parsed;
} catch (e) {}
const data = String(event.data ?? "");
return data.length > 0 ? data.split(",") : [];
}
function _specialWorkspaceNameFromMonitor(monitor) {
if (!monitor)
return "";
const candidates = [
monitor.activeSpecialWorkspace?.name,
monitor.specialWorkspace?.name,
monitor.lastIpcObject?.specialWorkspace?.name,
monitor.lastIpcObject?.specialWorkspace,
monitor.lastIpcObject?.activeSpecialWorkspace?.name
];
for (let i = 0; i < candidates.length; i++) {
const normalized = _normalizeSpecialWorkspaceName(candidates[i]);
if (normalized)
return normalized;
}
return "";
}
function updateHyprlandVisibleSpecialWorkspaces(event) {
if (!isHyprland) {
hyprlandVisibleSpecialWorkspaces = ({});
return;
}
const next = {};
try {
const monitors = Hyprland.monitors?.values || [];
for (const monitor of monitors) {
const monitorName = monitor?.name ?? monitor?.lastIpcObject?.name ?? "";
if (!monitorName)
continue;
const specialName = _specialWorkspaceNameFromMonitor(monitor);
if (specialName)
next[monitorName] = specialName;
}
} catch (e) {
log.warn("updateHyprlandVisibleSpecialWorkspaces monitor snapshot failed:", e);
}
if (event?.name === "activespecial") {
const parts = _hyprlandRawEventParts(event, 2);
const specialName = _normalizeSpecialWorkspaceName(parts[0]);
const monitorName = String(parts[1] ?? Hyprland.focusedMonitor?.name ?? Hyprland.focusedWorkspace?.monitor?.name ?? "");
if (monitorName) {
if (specialName)
next[monitorName] = specialName;
else
delete next[monitorName];
}
}
hyprlandVisibleSpecialWorkspaces = next;
}
function sortHyprlandToplevelsSafe() { function sortHyprlandToplevelsSafe() {
if (!Hyprland.toplevels || !Hyprland.toplevels.values) if (!Hyprland.toplevels || !Hyprland.toplevels.values)
return []; return [];
@@ -451,6 +532,171 @@ Singleton {
return false; return false;
} }
function _hyprlandToplevelMapped(hyprToplevel) {
if (!hyprToplevel)
return false;
if (hyprToplevel.mapped === false)
return false;
const ipcMapped = hyprToplevel.lastIpcObject?.mapped;
if (ipcMapped === false)
return false;
if (hyprToplevel.hidden === true)
return false;
const ipcHidden = hyprToplevel.lastIpcObject?.hidden;
if (ipcHidden === true)
return false;
return true;
}
function hyprlandVisibleSpecialWorkspaceOnScreen(screenOrName) {
const screenName = _screenName(screenOrName);
if (!isHyprland || !screenName)
return "";
hyprlandVisibleSpecialWorkspaces;
const trackedName = hyprlandVisibleSpecialWorkspaces[screenName] ?? "";
if (trackedName)
return trackedName;
try {
const monitor = Hyprland.monitors?.values?.find(m => m.name === screenName);
return _specialWorkspaceNameFromMonitor(monitor);
} catch (e) {
return "";
}
}
function hyprlandSpecialWorkspaceBlocksConnectedFrame(screenOrName) {
const screenName = _screenName(screenOrName);
if (!isHyprland || !screenName || !Hyprland.toplevels?.values)
return false;
const visibleSpecialWorkspace = hyprlandVisibleSpecialWorkspaceOnScreen(screenName);
if (!visibleSpecialWorkspace)
return false;
try {
for (const t of Hyprland.toplevels.values) {
const monName = t.monitor?.name ?? t.lastIpcObject?.monitor ?? "";
if (monName !== screenName)
continue;
const wsName = _normalizeSpecialWorkspaceName(t.workspace?.name ?? t.lastIpcObject?.workspace?.name ?? "");
if (!wsName || wsName !== visibleSpecialWorkspace)
continue;
if (_hyprlandToplevelMapped(t))
return true;
}
} catch (e) {
log.warn("hyprlandSpecialWorkspaceBlocksConnectedFrame failed:", e);
}
return false;
}
function connectedFrameBlockedOnScreen(screenOrName) {
if (hasFullscreenToplevelOnScreen(screenOrName))
return true;
return hyprlandSpecialWorkspaceBlocksConnectedFrame(screenOrName);
}
function _screenForName(screenOrName) {
if (screenOrName && typeof screenOrName !== "string")
return screenOrName;
const screenName = _screenName(screenOrName);
if (!screenName)
return null;
const screens = Quickshell.screens || [];
for (let i = 0; i < screens.length; i++) {
if (screens[i]?.name === screenName)
return screens[i];
}
return null;
}
function frameConfiguredForScreen(screenOrName) {
if (!SettingsData.frameEnabled)
return false;
const screen = _screenForName(screenOrName);
if (!screen || !SettingsData.isScreenInPreferences(screen, SettingsData.frameScreenPreferences))
return false;
return true;
}
function frameWindowVisibleForScreen(screenOrName) {
if (!frameConfiguredForScreen(screenOrName))
return false;
return !connectedFrameBlockedOnScreen(screenOrName);
}
function usesConnectedFrameChromeForScreen(screenOrName) {
return SettingsData.connectedFrameModeActive && frameWindowVisibleForScreen(screenOrName);
}
function framePeerSurfacesUseOverlayForScreen(screenOrName) {
return frameWindowVisibleForScreen(screenOrName);
}
function hyprlandToplevelOverlapsDockEdge(hyprToplevel, screenName, dockPosition, dockThickness, screenWidth, screenHeight) {
if (!hyprToplevel?.lastIpcObject || !screenName)
return false;
const monName = hyprToplevel.monitor?.name ?? hyprToplevel.lastIpcObject?.monitor ?? "";
if (monName && monName !== screenName)
return false;
const ipc = hyprToplevel.lastIpcObject;
const at = ipc.at;
const size = ipc.size;
if (!at || !size)
return false;
const monX = hyprToplevel.monitor?.x ?? 0;
const monY = hyprToplevel.monitor?.y ?? 0;
const winX = at[0] - monX;
const winY = at[1] - monY;
const winW = size[0];
const winH = size[1];
switch (dockPosition) {
case SettingsData.Position.Top:
return winY < dockThickness;
case SettingsData.Position.Bottom:
return winY + winH > screenHeight - dockThickness;
case SettingsData.Position.Left:
return winX < dockThickness;
case SettingsData.Position.Right:
return winX + winW > screenWidth - dockThickness;
default:
return false;
}
}
function hyprlandDockOverlapForSmartAutoHide(screenName, dockPosition, dockThickness, screenWidth, screenHeight) {
if (!isHyprland || !screenName || !Hyprland.toplevels?.values)
return false;
const filtered = filterCurrentWorkspace(sortedToplevels, screenName);
for (let i = 0; i < filtered.length; i++) {
const toplevel = filtered[i];
let hyprToplevel = null;
for (const t of Hyprland.toplevels.values) {
if (t.wayland === toplevel) {
hyprToplevel = t;
break;
}
}
if (hyprlandToplevelOverlapsDockEdge(hyprToplevel, screenName, dockPosition, dockThickness, screenWidth, screenHeight))
return true;
}
const visibleSpecialWorkspace = hyprlandVisibleSpecialWorkspaceOnScreen(screenName);
if (!visibleSpecialWorkspace)
return false;
for (const hyprToplevel of Hyprland.toplevels.values) {
const wsName = _normalizeSpecialWorkspaceName(hyprToplevel.workspace?.name ?? hyprToplevel.lastIpcObject?.workspace?.name ?? "");
if (wsName !== visibleSpecialWorkspace)
continue;
if (!_hyprlandToplevelMapped(hyprToplevel))
continue;
if (hyprlandToplevelOverlapsDockEdge(hyprToplevel, screenName, dockPosition, dockThickness, screenWidth, screenHeight))
return true;
}
return false;
}
function filterHyprlandCurrentDisplaySafe(toplevels, screenName) { function filterHyprlandCurrentDisplaySafe(toplevels, screenName) {
if (!toplevels || toplevels.length === 0 || !Hyprland.toplevels) if (!toplevels || toplevels.length === 0 || !Hyprland.toplevels)
return toplevels; return toplevels;
@@ -41,6 +41,9 @@ Singleton {
property var savedConnections: [] property var savedConnections: []
property var ssidToConnectionName: ({}) property var ssidToConnectionName: ({})
property var wifiSignalIcon: { property var wifiSignalIcon: {
if (isConnecting) {
return "wifi";
}
if (!wifiConnected) { if (!wifiConnected) {
return "wifi_off"; return "wifi_off";
} }
+18
View File
@@ -463,6 +463,24 @@ Singleton {
return _flatCache; return _flatCache;
} }
function keysForAction(actionId) {
if (!actionId)
return [];
for (let i = 0; i < _flatCache.length; i++) {
const group = _flatCache[i];
if (!group || group.action !== actionId || !Array.isArray(group.keys))
continue;
const keys = [];
for (let k = 0; k < group.keys.length; k++) {
const key = group.keys[k]?.key || "";
if (key)
keys.push(key);
}
return keys;
}
return [];
}
function saveBind(originalKey, bindData) { function saveBind(originalKey, bindData) {
if (!bindData.key || !Actions.isValidAction(bindData.action)) if (!bindData.key || !Actions.isValidAction(bindData.action))
return; return;
@@ -99,6 +99,9 @@ Singleton {
} }
readonly property string wifiSignalIcon: { readonly property string wifiSignalIcon: {
if (isConnecting) {
return "wifi";
}
if (!wifiConnected || networkStatus !== "wifi") { if (!wifiConnected || networkStatus !== "wifi") {
return "wifi_off"; return "wifi_off";
} }
+1
View File
@@ -42,6 +42,7 @@ Singleton {
property string userPreference: activeService?.userPreference ?? "auto" property string userPreference: activeService?.userPreference ?? "auto"
property bool isConnecting: activeService?.isConnecting ?? false property bool isConnecting: activeService?.isConnecting ?? false
readonly property bool isWifiConnecting: isConnecting && !ethernetConnected && !wifiToggling
property string connectingSSID: activeService?.connectingSSID ?? "" property string connectingSSID: activeService?.connectingSSID ?? ""
property string connectionError: activeService?.connectionError ?? "" property string connectionError: activeService?.connectionError ?? ""
+66 -21
View File
@@ -35,6 +35,8 @@ Singleton {
property int maxIngressPerSecond: 20 property int maxIngressPerSecond: 20
property double _lastIngressSec: 0 property double _lastIngressSec: 0
property int _ingressCountThisSec: 0 property int _ingressCountThisSec: 0
readonly property int notificationDedupBurstMs: 5000
property var _recentDedupKeys: []
property var _dismissQueue: [] property var _dismissQueue: []
property int _dismissBatchSize: 8 property int _dismissBatchSize: 8
@@ -291,18 +293,58 @@ Singleton {
return Date.now() / 1000.0; return Date.now() / 1000.0;
} }
function _normalizeDedupText(text) {
if (!text)
return "";
let normalized = text.toString();
normalized = normalized.replace(/<img\b[^>]*>/gi, "");
normalized = normalized.replace(/<[^>]+>/g, "");
normalized = normalized.replace(/\s+/g, " ").trim();
return normalized.toLowerCase();
}
function _dedupAppId(source) {
if (!source)
return "";
const desktopEntry = (source.desktopEntry || "").toString().trim().toLowerCase();
if (desktopEntry)
return desktopEntry;
return (source.appName || "").toString().trim().toLowerCase();
}
function _notificationDedupKey(source) { function _notificationDedupKey(source) {
if (!source) if (!source)
return ""; return "";
const app = (source.appName || source.desktopEntry || "").toString(); const app = _dedupAppId(source);
const summary = (source.summary || "").toString(); const summary = _normalizeDedupText(source.summary);
const body = (source.body || "").toString(); const body = _normalizeDedupText(source.body);
const urgency = typeof source.urgency === "number" ? source.urgency : NotificationUrgency.Normal; const urgency = typeof source.urgency === "number" ? source.urgency : NotificationUrgency.Normal;
const icon = (source.appIcon || "").toString();
if (!app && !summary && !body) if (!app && !summary && !body)
return ""; return "";
const sep = ""; const sep = "";
return app + sep + summary + sep + body + sep + urgency + sep + icon; return app + sep + summary + sep + body + sep + urgency;
}
function _pruneRecentDedupKeys() {
const cutoff = Date.now() - notificationDedupBurstMs;
_recentDedupKeys = _recentDedupKeys.filter(entry => entry && entry.atMs >= cutoff);
}
function _hasRecentDuplicate(key) {
if (!key)
return false;
_pruneRecentDedupKeys();
return _recentDedupKeys.some(entry => entry && entry.key === key);
}
function _recordDedupKey(key) {
if (!key)
return;
_pruneRecentDedupKeys();
_recentDedupKeys.push({
"key": key,
"atMs": Date.now()
});
} }
function _findActiveDuplicate(notif) { function _findActiveDuplicate(notif) {
@@ -310,17 +352,14 @@ Singleton {
if (!key) if (!key)
return null; return null;
for (const w of visibleNotifications) { for (const w of allWrappers) {
if (!w || !w.notification || !w.popup) if (!w || !w.notification || !w.popup)
continue; continue;
if (_notificationDedupKey(w.notification) === key) if (_notificationDedupKey(w.notification) !== key)
return w;
}
for (const w of notificationQueue) {
if (!w || !w.notification)
continue; continue;
if (_notificationDedupKey(w.notification) === key) if (visibleNotifications.indexOf(w) !== -1 || notificationQueue.indexOf(w) !== -1)
return w;
if (w.timer && w.timer.running)
return w; return w;
} }
@@ -637,14 +676,17 @@ Singleton {
return; return;
} }
const duplicate = _findActiveDuplicate(notif); if (SettingsData.notificationDedupeEnabled) {
if (duplicate) { const dedupKey = _notificationDedupKey(notif);
if (duplicate.timer && duplicate.timer.running) const duplicate = _findActiveDuplicate(notif);
duplicate.timer.restart(); if (duplicate || _hasRecentDuplicate(dedupKey)) {
try { if (duplicate && duplicate.timer && duplicate.timer.running)
notif.dismiss(); duplicate.timer.restart();
} catch (e) {} try {
return; notif.dismiss();
} catch (e) {}
return;
}
} }
if (!_ingressAllowed(policy.urgency)) { if (!_ingressAllowed(policy.urgency)) {
@@ -686,6 +728,9 @@ Singleton {
}); });
if (wrapper) { if (wrapper) {
if (SettingsData.notificationDedupeEnabled)
_recordDedupKey(_notificationDedupKey(notif));
root.allWrappers.push(wrapper); root.allWrappers.push(wrapper);
if (shouldKeepInCenter) { if (shouldKeepInCenter) {
root.notifications.push(wrapper); root.notifications.push(wrapper);
+62 -6
View File
@@ -34,6 +34,8 @@ Singleton {
property var clipboardHistoryModal: null property var clipboardHistoryModal: null
property var dankLauncherV2Modal: null property var dankLauncherV2Modal: null
property var dankLauncherV2ModalLoader: null property var dankLauncherV2ModalLoader: null
property var spotlightBarModal: null
property var spotlightBarModalLoader: null
property var powerMenuModal: null property var powerMenuModal: null
property var processListModal: null property var processListModal: null
property var processListModalLoader: null property var processListModalLoader: null
@@ -500,8 +502,16 @@ Singleton {
property bool _dankLauncherV2WantsToggle: false property bool _dankLauncherV2WantsToggle: false
property string _dankLauncherV2PendingQuery: "" property string _dankLauncherV2PendingQuery: ""
property string _dankLauncherV2PendingMode: "" property string _dankLauncherV2PendingMode: ""
property bool _dankLauncherV2TriggerUsesOverlayLayer: false
function openDankLauncherV2() { function _setDankLauncherV2TriggerUsesOverlayLayer(value) {
_dankLauncherV2TriggerUsesOverlayLayer = value === true;
if (dankLauncherV2Modal)
dankLauncherV2Modal.triggerUsesOverlayLayer = _dankLauncherV2TriggerUsesOverlayLayer;
}
function openDankLauncherV2(triggerUsesOverlayLayer) {
_setDankLauncherV2TriggerUsesOverlayLayer(triggerUsesOverlayLayer);
if (dankLauncherV2Modal) { if (dankLauncherV2Modal) {
dankLauncherV2Modal.show(); dankLauncherV2Modal.show();
} else if (dankLauncherV2ModalLoader) { } else if (dankLauncherV2ModalLoader) {
@@ -511,7 +521,8 @@ Singleton {
} }
} }
function openDankLauncherV2WithQuery(query: string) { function openDankLauncherV2WithQuery(query: string, triggerUsesOverlayLayer) {
_setDankLauncherV2TriggerUsesOverlayLayer(triggerUsesOverlayLayer);
if (dankLauncherV2Modal) { if (dankLauncherV2Modal) {
dankLauncherV2Modal.showWithQuery(query); dankLauncherV2Modal.showWithQuery(query);
} else if (dankLauncherV2ModalLoader) { } else if (dankLauncherV2ModalLoader) {
@@ -522,7 +533,8 @@ Singleton {
} }
} }
function openDankLauncherV2WithMode(mode: string) { function openDankLauncherV2WithMode(mode: string, triggerUsesOverlayLayer) {
_setDankLauncherV2TriggerUsesOverlayLayer(triggerUsesOverlayLayer);
if (dankLauncherV2Modal) { if (dankLauncherV2Modal) {
dankLauncherV2Modal.showWithMode(mode); dankLauncherV2Modal.showWithMode(mode);
} else if (dankLauncherV2ModalLoader) { } else if (dankLauncherV2ModalLoader) {
@@ -544,7 +556,8 @@ Singleton {
} }
} }
function toggleDankLauncherV2() { function toggleDankLauncherV2(triggerUsesOverlayLayer) {
_setDankLauncherV2TriggerUsesOverlayLayer(triggerUsesOverlayLayer);
if (dankLauncherV2Modal) { if (dankLauncherV2Modal) {
dankLauncherV2Modal.toggle(); dankLauncherV2Modal.toggle();
} else if (dankLauncherV2ModalLoader) { } else if (dankLauncherV2ModalLoader) {
@@ -554,7 +567,8 @@ Singleton {
} }
} }
function toggleDankLauncherV2WithMode(mode: string) { function toggleDankLauncherV2WithMode(mode: string, triggerUsesOverlayLayer) {
_setDankLauncherV2TriggerUsesOverlayLayer(triggerUsesOverlayLayer);
if (dankLauncherV2Modal) { if (dankLauncherV2Modal) {
dankLauncherV2Modal.toggleWithMode(mode); dankLauncherV2Modal.toggleWithMode(mode);
} else if (dankLauncherV2ModalLoader) { } else if (dankLauncherV2ModalLoader) {
@@ -565,7 +579,8 @@ Singleton {
} }
} }
function toggleDankLauncherV2WithQuery(query: string) { function toggleDankLauncherV2WithQuery(query: string, triggerUsesOverlayLayer) {
_setDankLauncherV2TriggerUsesOverlayLayer(triggerUsesOverlayLayer);
if (dankLauncherV2Modal) { if (dankLauncherV2Modal) {
dankLauncherV2Modal.toggleWithQuery(query); dankLauncherV2Modal.toggleWithQuery(query);
} else if (dankLauncherV2ModalLoader) { } else if (dankLauncherV2ModalLoader) {
@@ -577,6 +592,8 @@ Singleton {
} }
function _onDankLauncherV2ModalLoaded() { function _onDankLauncherV2ModalLoaded() {
if (dankLauncherV2Modal)
dankLauncherV2Modal.triggerUsesOverlayLayer = _dankLauncherV2TriggerUsesOverlayLayer;
if (_dankLauncherV2WantsOpen) { if (_dankLauncherV2WantsOpen) {
_dankLauncherV2WantsOpen = false; _dankLauncherV2WantsOpen = false;
if (_dankLauncherV2PendingQuery) { if (_dankLauncherV2PendingQuery) {
@@ -601,6 +618,45 @@ Singleton {
} }
} }
property bool _spotlightBarWantsOpen: false
property bool _spotlightBarWantsToggle: false
function openSpotlightBar() {
if (spotlightBarModal) {
spotlightBarModal.show();
} else if (spotlightBarModalLoader) {
_spotlightBarWantsOpen = true;
_spotlightBarWantsToggle = false;
spotlightBarModalLoader.active = true;
}
}
function closeSpotlightBar() {
spotlightBarModal?.hide();
}
function toggleSpotlightBar() {
if (spotlightBarModal) {
spotlightBarModal.toggle();
} else if (spotlightBarModalLoader) {
_spotlightBarWantsToggle = true;
_spotlightBarWantsOpen = false;
spotlightBarModalLoader.active = true;
}
}
function _onSpotlightBarModalLoaded() {
if (_spotlightBarWantsOpen) {
_spotlightBarWantsOpen = false;
spotlightBarModal?.show();
return;
}
if (_spotlightBarWantsToggle) {
_spotlightBarWantsToggle = false;
spotlightBarModal?.toggle();
}
}
function openPowerMenu() { function openPowerMenu() {
powerMenuModal?.openCentered(); powerMenuModal?.openCentered();
} }
+4
View File
@@ -205,6 +205,8 @@ Singleton {
} }
function launchDesktopEntry(desktopEntry, useNvidia) { function launchDesktopEntry(desktopEntry, useNvidia) {
if (!desktopEntry || !desktopEntry.command)
return;
let cmd = desktopEntry.command; let cmd = desktopEntry.command;
const appId = desktopEntry.id || desktopEntry.execString || desktopEntry.exec || ""; const appId = desktopEntry.id || desktopEntry.execString || desktopEntry.exec || "";
@@ -261,6 +263,8 @@ Singleton {
} }
function launchDesktopAction(desktopEntry, action, useNvidia) { function launchDesktopAction(desktopEntry, action, useNvidia) {
if (!desktopEntry || !action || !action.command)
return;
let cmd = action.command; let cmd = action.command;
const appId = desktopEntry.id || desktopEntry.execString || desktopEntry.exec || ""; const appId = desktopEntry.id || desktopEntry.execString || desktopEntry.exec || "";
+236
View File
@@ -0,0 +1,236 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
Singleton {
id: root
readonly property var log: Log.scoped("SessionsService")
property var sessions: []
property string currentSessionId: ""
property string currentSeat: ""
property bool refreshing: false
signal switchFailed(string sessionId, string username, string message)
signal switchRequested
function isCurrent(sessionId) {
return sessionId === currentSessionId;
}
function findByUsername(username) {
for (let i = 0; i < sessions.length; i++) {
const s = sessions[i];
if (s.username === username && !s.current)
return s;
}
return null;
}
function findById(sessionId) {
for (let i = 0; i < sessions.length; i++) {
if (sessions[i].sessionId === sessionId)
return sessions[i];
}
return null;
}
function otherSessions() {
return sessions.filter(s => !s.current);
}
function refresh() {
if (refreshing)
return;
refreshing = true;
Proc.runCommand("sessionsService-current", ["sh", "-c", "echo \"${XDG_SESSION_ID}:$(loginctl show-session \"${XDG_SESSION_ID}\" -p Seat --value 2>/dev/null)\""], (output, exitCode) => {
const trimmed = (output || "").trim();
const parts = trimmed.split(":");
root.currentSessionId = parts[0] || "";
root.currentSeat = parts[1] || "";
_loadSessions();
}, 0);
}
function _loadSessions() {
const script = "loginctl list-sessions --no-legend 2>/dev/null | awk '{print $1}' | while read id; do loginctl show-session \"$id\" -p Id -p User -p Name -p Seat -p TTY -p Type -p Class -p Active -p State -p Remote 2>/dev/null | tr '\\n' '|'; echo; done";
Proc.runCommand("sessionsService-list", ["sh", "-c", script], (output, exitCode) => {
const lines = (output || "").trim().split("\n").filter(l => l.length > 0);
const list = [];
for (let i = 0; i < lines.length; i++) {
const fields = {};
const pairs = lines[i].split("|");
for (let j = 0; j < pairs.length; j++) {
const eq = pairs[j].indexOf("=");
if (eq <= 0)
continue;
fields[pairs[j].substring(0, eq)] = pairs[j].substring(eq + 1);
}
if (!fields.Id)
continue;
if (fields.Class !== "user")
continue;
if (fields.State === "closing")
continue;
const sessionId = fields.Id;
list.push({
sessionId: sessionId,
uid: parseInt(fields.User || "0", 10),
username: fields.Name || "",
seat: fields.Seat || "",
tty: fields.TTY || "",
type: fields.Type || "",
sessionClass: fields.Class || "",
active: fields.Active === "yes",
state: fields.State || "",
remote: fields.Remote === "yes",
current: sessionId === root.currentSessionId
});
}
list.sort((a, b) => {
if (a.current !== b.current)
return a.current ? -1 : 1;
if (a.username !== b.username)
return a.username.localeCompare(b.username);
return parseInt(a.sessionId, 10) - parseInt(b.sessionId, 10);
});
root.sessions = list;
root.refreshing = false;
}, 0);
}
function activate(sessionId, callback) {
if (!sessionId) {
_fail("", "", I18n.tr("No session selected"), callback);
return;
}
if (sessionId === root.currentSessionId) {
_fail(sessionId, "", I18n.tr("Already on that session"), callback);
return;
}
const session = findById(sessionId);
const username = session ? session.username : "";
_spawnActivate(sessionId, username, callback);
}
function switchToUser(target, callback) {
if (!target) {
_fail("", "", I18n.tr("No user specified"), callback);
return;
}
let session = findById(target);
if (!session)
session = findByUsername(target);
if (!session) {
_fail("", target, I18n.tr("No active session found for %1").arg(target), callback);
return;
}
if (session.current) {
_fail(session.sessionId, session.username, I18n.tr("Already on that session"), callback);
return;
}
_spawnActivate(session.sessionId, session.username, callback);
}
function _fail(sessionId, username, message, callback) {
log.warn("switch failed:", sessionId, username, message);
root.switchFailed(sessionId, username, message);
if (typeof callback === "function") {
try {
callback(false, message);
} catch (e) {
log.warn("SessionsService callback error:", e);
}
}
}
Component {
id: activateComp
Process {
id: activateProc
property string targetSession: ""
property string targetUsername: ""
property var cb: null
property string capturedErr: ""
running: false
stdout: StdioCollector {}
stderr: StdioCollector {
onStreamFinished: activateProc.capturedErr = text || ""
}
onExited: exitCode => {
const svc = root;
const sessionId = activateProc.targetSession;
const username = activateProc.targetUsername;
const cb = activateProc.cb;
const err = (activateProc.capturedErr || "").trim();
Qt.callLater(() => activateProc.destroy());
if (exitCode !== 0) {
svc._fail(sessionId, username, err || I18n.tr("loginctl activate failed (exit %1)").arg(exitCode), cb);
return;
}
if (typeof cb === "function") {
try {
cb(true, "");
} catch (e) {
svc.log.warn("activate cb error:", e);
}
}
}
}
}
function _spawnActivate(sessionId, username, callback) {
const proc = activateComp.createObject(root, {
command: ["loginctl", "activate", sessionId],
targetSession: sessionId,
targetUsername: username,
cb: callback
});
proc.running = true;
}
IpcHandler {
target: "sessions"
function list(): string {
const lines = [];
for (let i = 0; i < root.sessions.length; i++) {
const s = root.sessions[i];
lines.push([s.sessionId, s.username, s.seat || "-", s.tty || "-", s.type || "-", s.current ? "*current*" : ""].join("\t"));
}
return lines.join("\n");
}
function refresh(): string {
root.refresh();
return "ok";
}
function open(): string {
root.refresh();
root.switchRequested();
return "ok";
}
function activate(sessionId: string): string {
if (!sessionId)
return "ERROR: missing session id";
root.activate(sessionId, null);
return "ok";
}
function switchTo(target: string): string {
if (!target)
return "ERROR: missing target (username or session id)";
root.switchToUser(target, null);
return "ok";
}
}
Component.onCompleted: refresh()
}

Some files were not shown because too many files have changed in this diff Show More