1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-24 03:55:23 -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,
trashCmd,
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 ===
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("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 + M", hl.dsp.exec_cmd("dms ipc call processlist 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" {
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" {
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 conflictKb, ok := conflicts[keyStr]; ok {
if conflictKb, ok := conflicts[normalizeNiriBindKey(keyStr)]; ok {
bind.Conflict = &keybinds.Keybind{
Key: keyStr,
Description: conflictKb.Description,
@@ -249,7 +249,7 @@ func (n *NiriProvider) SetBind(key, action, description string, options map[stri
existingBinds = make(map[string]*overrideBind)
}
existingBinds[key] = &overrideBind{
existingBinds[normalizeNiriBindKey(key)] = &overrideBind{
Key: key,
Action: action,
Description: description,
@@ -265,7 +265,7 @@ func (n *NiriProvider) RemoveBind(key string) error {
return nil
}
delete(existingBinds, key)
delete(existingBinds, normalizeNiriBindKey(key))
return n.writeOverrideBinds(existingBinds)
}
@@ -316,7 +316,7 @@ func (n *NiriProvider) loadOverrideBinds() (map[string]*overrideBind, error) {
action = n.formatRawAction(kb.Action, kb.Args)
}
binds[keyStr] = &overrideBind{
binds[normalizeNiriBindKey(keyStr)] = &overrideBind{
Key: keyStr,
Action: action,
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) {
dmsBindsPath := filepath.Join(p.configDir, "dms", "binds.kdl")
if _, err := os.Stat(dmsBindsPath); err == nil {
@@ -213,24 +221,25 @@ func (p *NiriParser) finalizeBinds() []NiriKeyBinding {
func (p *NiriParser) addBind(kb *NiriKeyBinding) {
key := p.formatBindKey(kb)
normalizedKey := normalizeNiriBindKey(key)
isDMSBind := strings.Contains(kb.Source, "dms/binds.kdl")
if isDMSBind {
p.dmsBindKeys[key] = true
p.dmsBindMap[key] = kb
} else if p.dmsBindKeys[key] {
p.dmsBindKeys[normalizedKey] = true
p.dmsBindMap[normalizedKey] = kb
} else if p.dmsBindKeys[normalizedKey] {
p.bindsAfterDMS++
p.conflictingConfigs[key] = kb
p.configBindKeys[key] = true
p.conflictingConfigs[normalizedKey] = kb
p.configBindKeys[normalizedKey] = true
return
} else {
p.configBindKeys[key] = true
p.configBindKeys[normalizedKey] = true
}
if _, exists := p.bindMap[key]; !exists {
p.bindOrder = append(p.bindOrder, key)
if _, exists := p.bindMap[normalizedKey]; !exists {
p.bindOrder = append(p.bindOrder, normalizedKey)
}
p.bindMap[key] = kb
p.bindMap[normalizedKey] = kb
}
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) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
@@ -367,7 +367,7 @@ func TestNiriEmptyArgsPreservation(t *testing.T) {
}
for key, expected := range binds {
loaded, ok := loadedBinds[key]
loaded, ok := loadedBinds[normalizeNiriBindKey(key)]
if !ok {
t.Errorf("Missing bind for key %s", key)
continue
@@ -1,6 +1,7 @@
package network
import (
"encoding/json"
"fmt"
"net"
"strings"
@@ -18,10 +19,41 @@ const (
)
type linkInfo struct {
ifindex int32
name string
path dbus.ObjectPath
opState string
ifindex int32
name string
path dbus.ObjectPath
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 {
@@ -95,17 +127,50 @@ func (b *SystemdNetworkdBackend) enumerateLinks() error {
defer b.linksMutex.Unlock()
for _, l := range links {
b.links[l.Name] = &linkInfo{
ifindex: l.Ifindex,
name: l.Name,
path: l.Path,
if existing, ok := b.links[l.Name]; ok && existing.path == l.Path {
existing.ifindex = l.Ifindex
continue
}
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
}
// 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 {
b.linksMutex.RLock()
defer b.linksMutex.RUnlock()
@@ -113,8 +178,8 @@ func (b *SystemdNetworkdBackend) updateState() error {
var wiredIface *linkInfo
var wifiIface *linkInfo
for name, link := range b.links {
if b.isVirtualInterface(name) {
for _, link := range b.links {
if !link.isWired() && !link.isWireless() {
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" {
wifiIface = link
}
} else if !b.isVirtualInterface(name) {
} else if link.isWired() {
if wiredIface == nil || link.opState == "routable" || link.opState == "carrier" {
wiredIface = link
}
@@ -140,7 +205,7 @@ func (b *SystemdNetworkdBackend) updateState() error {
var wiredConns []WiredConnection
var ethernetDevices []EthernetDevice
for name, link := range b.links {
if b.isVirtualInterface(name) || strings.HasPrefix(name, "wlan") || strings.HasPrefix(name, "wlp") {
if !link.isWired() {
continue
}
@@ -229,19 +294,6 @@ func (b *SystemdNetworkdBackend) updateState() error {
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 {
iface, err := net.InterfaceByName(ifname)
if err != nil {
@@ -12,7 +12,7 @@ func (b *SystemdNetworkdBackend) GetWiredConnections() ([]WiredConnection, error
var conns []WiredConnection
for name, link := range b.links {
if b.isVirtualInterface(name) || strings.HasPrefix(name, "wlan") || strings.HasPrefix(name, "wlp") {
if !link.isWired() {
continue
}
@@ -73,8 +73,8 @@ func (b *SystemdNetworkdBackend) GetWiredNetworkDetails(id string) (*WiredNetwor
func (b *SystemdNetworkdBackend) ConnectEthernet() error {
b.linksMutex.RLock()
var primaryWired *linkInfo
for name, l := range b.links {
if strings.HasPrefix(name, "lo") || strings.HasPrefix(name, "wlan") || strings.HasPrefix(name, "wlp") {
for _, l := range b.links {
if !l.isWired() {
continue
}
primaryWired = l
@@ -145,3 +145,73 @@ func TestSystemdNetworkdBackend_DisconnectEthernetDevice(t *testing.T) {
assert.Error(t, err)
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 \
debian/dms-greeter/usr/share/doc/dms-greeter/LICENSE && \
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 \
echo "ERROR: No upstream source (dms-qml or Modules/Greetd/assets/dms-greeter)!" && \
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 -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 -dm755 %{buildroot}%{_sharedstatedir}/greeter
@@ -78,6 +80,7 @@ fi
%{_bindir}/dms-greeter
%{_datadir}/quickshell/dms-greeter/
%{_tmpfilesdir}/%{name}.conf
%{_sysusersdir}/dms-greeter.conf
%pre
# 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 -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 -dm755 %{buildroot}%{_sharedstatedir}/greeter
@@ -78,6 +80,7 @@ fi
%dir %{_datadir}/quickshell
%{_datadir}/quickshell/dms-greeter/
%{_tmpfilesdir}/%{name}.conf
%{_sysusersdir}/dms-greeter.conf
%pre
# 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 \
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)
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
```
## 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`
Idle inhibitor control to prevent automatic sleep/lock.
+7 -4
View File
@@ -8,9 +8,12 @@ const ACTION_TYPES = [
];
const DMS_ACTIONS = [
{ id: "spawn dms ipc call spotlight toggle", label: "App Launcher: Toggle" },
{ id: "spawn dms ipc call spotlight open", label: "App Launcher: Open" },
{ id: "spawn dms ipc call spotlight close", label: "App Launcher: Close" },
{ id: "spawn dms ipc call spotlight toggle", label: "Default Launcher: Toggle" },
{ id: "spawn dms ipc call spotlight open", label: "Default Launcher: Open" },
{ 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 open", label: "Clipboard: Open" },
{ 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 decrement 5", label: "Player Volume Down (5%)" },
{ 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 brightness increment 5 \"\"", label: "Brightness Up" },
{ id: "spawn dms ipc call brightness increment 1 \"\"", label: "Brightness Up (1%)" },
+11 -3
View File
@@ -9,9 +9,11 @@ Singleton {
property var currentOSDsByScreen: ({})
Connections {
target: Quickshell
function onScreensChanged() {
Timer {
id: screensChangedDelayTimer
interval: 3000 // 3 seconds
repeat: false
onTriggered: {
const activeNames = {};
for (let i = 0; i < Quickshell.screens.length; i++)
activeNames[Quickshell.screens[i].name] = true;
@@ -22,6 +24,12 @@ Singleton {
}
}
}
Connections {
target: Quickshell
function onScreensChanged() {
screensChangedDelayTimer.restart();
}
}
function showOSD(osd) {
if (!osd || !osd.screen)
+12
View File
@@ -187,6 +187,7 @@ Singleton {
property string timeLocale: ""
property string launcherLastMode: "all"
property string launcherLastFileSearchType: "all"
property string launcherLastQuery: ""
property var launcherQueryHistory: []
property string appDrawerLastMode: "apps"
@@ -1178,6 +1179,17 @@ Singleton {
saveSettings();
}
function getLauncherRestoreMode() {
if (!SettingsData.rememberLastMode)
return "all";
return launcherLastMode || "all";
}
function setLauncherLastFileSearchType(type) {
launcherLastFileSearchType = type;
saveSettings();
}
function setLauncherLastQuery(query) {
launcherLastQuery = query;
saveSettings();
+8 -3
View File
@@ -258,8 +258,6 @@ Singleton {
onFrameLauncherEmergeSideChanged: saveSettings()
property bool frameLauncherArcExtender: false
onFrameLauncherArcExtenderChanged: saveSettings()
property bool frameUseSpotlightLauncher: false
onFrameUseSpotlightLauncherChanged: saveSettings()
readonly property string frameModalEmergeSide: frameLauncherEmergeSide === "top" ? "bottom" : "top"
property string frameMode: "connected"
onFrameModeChanged: saveSettings()
@@ -394,6 +392,7 @@ Singleton {
property string audioScrollMode: "volume"
property int audioWheelScrollAmount: 5
property bool clockCompactMode: false
property int focusedWindowSize: 1
property bool focusedWindowCompactMode: false
property bool runningAppsCompactMode: true
property int barMaxVisibleApps: 0
@@ -436,6 +435,7 @@ Singleton {
property int appLauncherGridColumns: 4
property bool spotlightCloseNiriOverview: true
property bool rememberLastQuery: false
property bool rememberLastMode: true
property var spotlightSectionViewModes: ({})
onSpotlightSectionViewModesChanged: saveSettings()
property var appDrawerSectionViewModes: ({})
@@ -449,7 +449,9 @@ Singleton {
property bool dankLauncherV2UnloadOnClose: false
property bool dankLauncherV2IncludeFilesInAll: false
property bool dankLauncherV2IncludeFoldersInAll: false
property bool launcherUseOverlayLayer: false
property string launcherStyle: "full"
property bool spotlightBarShowModeChips: false
property string _legacyWeatherLocation: "New York, NY"
property string _legacyWeatherCoordinates: "40.7128,-74.0060"
@@ -606,7 +608,7 @@ Singleton {
property bool showDock: false
property bool dockAutoHide: false
property bool dockSmartAutoHide: false
property bool dockHideOnFullscreen: true
property bool dockUseOverlayLayer: false
property bool dockGroupByApp: false
property bool dockRestoreSpecialWorkspaceOnClick: false
property bool dockOpenOnOverview: false
@@ -686,6 +688,7 @@ Singleton {
property int notificationTimeoutNormal: 5000
property int notificationTimeoutCritical: 0
property bool notificationCompactMode: false
property bool notificationDedupeEnabled: true
property int notificationPopupPosition: SettingsData.Position.Top
property int notificationAnimationSpeed: SettingsData.AnimationSpeed.Short
property int notificationCustomAnimationDuration: 400
@@ -706,6 +709,7 @@ Singleton {
property bool osdBrightnessEnabled: true
property bool osdIdleInhibitorEnabled: true
property bool osdMicMuteEnabled: true
property bool osdMicVolumeEnabled: true
property bool osdCapsLockEnabled: true
property bool osdPowerProfileEnabled: true
property bool osdAudioOutputEnabled: true
@@ -787,6 +791,7 @@ Singleton {
"popupGapsAuto": true,
"popupGapsManual": 4,
"maximizeDetection": true,
"useOverlayLayer": false,
"scrollEnabled": true,
"scrollXBehavior": "column",
"scrollYBehavior": "workspace",
@@ -87,6 +87,7 @@ var SPEC = {
timeLocale: { def: "" },
launcherLastMode: { def: "all" },
launcherLastFileSearchType: { def: "all" },
launcherLastQuery: { def: "" },
launcherQueryHistory: { def: [] },
appDrawerLastMode: { def: "apps" },
+7 -3
View File
@@ -153,6 +153,7 @@ var SPEC = {
audioWheelScrollAmount: { def: 5 },
clockCompactMode: { def: false },
focusedWindowCompactMode: { def: false },
focusedWindowSize: { def: 1 },
runningAppsCompactMode: { def: true },
barMaxVisibleApps: { def: 0 },
barMaxVisibleRunningApps: { def: 0 },
@@ -202,6 +203,7 @@ var SPEC = {
appLauncherGridColumns: { def: 4 },
spotlightCloseNiriOverview: { def: true },
rememberLastQuery: { def: false },
rememberLastMode: { def: true },
spotlightSectionViewModes: { def: {} },
appDrawerSectionViewModes: { def: {} },
niriOverviewOverlayEnabled: { def: true },
@@ -213,7 +215,9 @@ var SPEC = {
dankLauncherV2UnloadOnClose: { def: false },
dankLauncherV2IncludeFilesInAll: { def: false },
dankLauncherV2IncludeFoldersInAll: { def: false },
launcherUseOverlayLayer: { def: false },
launcherStyle: { def: "full" },
spotlightBarShowModeChips: { def: false },
useAutoLocation: { def: false },
weatherEnabled: { def: true },
@@ -332,7 +336,7 @@ var SPEC = {
showDock: { def: false },
dockAutoHide: { def: false },
dockSmartAutoHide: { def: false },
dockHideOnFullscreen: { def: true },
dockUseOverlayLayer: { def: false },
dockGroupByApp: { def: false },
dockRestoreSpecialWorkspaceOnClick: { def: false },
dockOpenOnOverview: { def: false },
@@ -395,6 +399,7 @@ var SPEC = {
notificationTimeoutNormal: { def: 5000 },
notificationTimeoutCritical: { def: 0 },
notificationCompactMode: { def: false },
notificationDedupeEnabled: { def: true },
notificationPopupPosition: { def: 0 },
notificationAnimationSpeed: { def: 1 },
notificationCustomAnimationDuration: { def: 400 },
@@ -496,7 +501,7 @@ var SPEC = {
popupGapsAuto: true,
popupGapsManual: 4,
maximizeDetection: true,
fullscreenDetection: true,
useOverlayLayer: false,
scrollEnabled: true,
scrollXBehavior: "column",
scrollYBehavior: "workspace",
@@ -573,7 +578,6 @@ var SPEC = {
frameCloseGaps: { def: true },
frameLauncherEmergeSide: { def: "bottom" },
frameLauncherArcExtender: { def: false },
frameUseSpotlightLauncher: { def: false },
frameMode: { def: "connected" }
};
+150 -4
View File
@@ -30,6 +30,7 @@ import qs.Services
Item {
id: root
readonly property var log: Log.scoped("DMSShell")
readonly property var _sessionsServiceRef: SessionsService
property bool osdSurfacesLoaded: true
property int pendingOsdResumeReloads: 0
@@ -63,15 +64,27 @@ Item {
}
}
property bool wallpaperSurfacesLoaded: true
Loader {
id: blurredWallpaperBackgroundLoader
active: SettingsData.blurredWallpaperLayer && CompositorService.isNiri
active: root.wallpaperSurfacesLoaded && SettingsData.blurredWallpaperLayer && CompositorService.isNiri
asynchronous: false
sourceComponent: BlurredWallpaperBackground {}
}
WallpaperBackground {}
DeferredAction {
id: wallpaperSurfaceReloadAction
onTriggered: root.wallpaperSurfacesLoaded = true
}
Loader {
id: wallpaperBackgroundLoader
active: root.wallpaperSurfacesLoaded
asynchronous: false
sourceComponent: WallpaperBackground {}
}
DesktopWidgetLayer {}
@@ -168,6 +181,8 @@ Item {
property bool barSurfacesLoaded: true
function recreateBarSurfaces() {
log.info("Recreating bar surfaces, screens:", Quickshell.screens.length,
Quickshell.screens.map(s => s.name).join(","));
if (barSurfacesLoaded)
barSurfacesLoaded = false;
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 {
id: dankBarRepeater
@@ -301,6 +327,81 @@ Item {
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: {
dockRecreateDebounce.start();
// 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 {
id: clipboardHistoryPopoutLoader
@@ -868,9 +988,17 @@ Item {
target: SessionService
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;
osdResumeRecreateTimer.interval = 400;
osdResumeRecreateTimer.restart();
root.triggerSurfaceRecovery("sessionResumed");
}
}
@@ -1019,12 +1147,30 @@ Item {
lock.activate();
}
onSwitchUserRequested: {
switchUserModalLoader.active = true;
Qt.callLater(() => {
if (switchUserModalLoader.item)
switchUserModalLoader.item.showFromPowerMenu();
});
}
Component.onCompleted: {
PopoutService.powerMenuModal = powerMenuModal;
}
}
}
LazyLoader {
id: switchUserModalLoader
active: false
SwitchUserModal {
id: switchUserModal
}
}
LazyLoader {
id: hyprKeybindsModalLoader
@@ -1095,7 +1241,7 @@ Item {
Variants {
model: SettingsData.getFilteredScreens("osd")
delegate: MicMuteOSD {
delegate: MicVolumeOSD {
modelData: item
}
}
+49
View File
@@ -1340,6 +1340,25 @@ Item {
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 {
function info(message: string): string {
if (!message)
@@ -1775,6 +1794,36 @@ Item {
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 {
function findTrayItem(itemId: string): var {
if (!itemId)
@@ -145,6 +145,7 @@ Item {
onDeleteRequested: clipboardContent.modal.deleteEntry(modelData)
onPinRequested: clipboardContent.modal.pinEntry(modelData)
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
onEditRequested: clipboardContent.modal.editEntry(modelData)
}
}
@@ -204,6 +205,7 @@ Item {
onDeleteRequested: clipboardContent.modal.deletePinnedEntry(modelData)
onPinRequested: clipboardContent.modal.pinEntry(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 pinRequested
signal unpinRequested
signal editRequested
readonly property string entryType: modal ? modal.getEntryType(entry) : "text"
readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : ""
@@ -70,6 +71,20 @@ Rectangle {
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 {
iconName: "close"
iconSize: Theme.iconSize - 6
@@ -142,8 +157,11 @@ Rectangle {
MouseArea {
id: mouseArea
anchors.fill: parent
anchors.rightMargin: 80
anchors.left: parent.left
anchors.right: actionButtons.left
anchors.rightMargin: Theme.spacingS
anchors.top: parent.top
anchors.bottom: parent.bottom
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: mouse => {
@@ -43,6 +43,18 @@ DankModal {
service: ClipboardService
}
property string mode: "history"
onModeChanged: {
if (mode !== "history") {
return;
}
Qt.callLater(function () {
if (contentLoader.item?.searchField) {
contentLoader.item.searchField.forceActiveFocus();
}
});
}
function updateFilteredModel() {
ClipboardService.updateFilteredModel();
}
@@ -61,6 +73,7 @@ DankModal {
function show() {
open();
mode = "history";
activeImageLoads = 0;
shouldHaveFocus = true;
ClipboardService.reset();
@@ -130,6 +143,21 @@ DankModal {
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
modalWidth: ClipboardConstants.modalWidth
modalHeight: ClipboardConstants.modalHeight
@@ -138,6 +166,7 @@ DankModal {
borderColor: Theme.outlineMedium
borderWidth: 1
enableShadow: true
closeOnEscapeKey: mode !== "editor"
onBackgroundClicked: hide()
modalFocusScope.Keys.onPressed: function (event) {
keyboardController.handleKey(event);
@@ -174,9 +203,109 @@ DankModal {
property var confirmDialog: clearConfirmDialog
clipboardContent: Component {
ClipboardContent {
modal: clipboardHistoryModal
clearConfirmDialog: clipboardHistoryModal.confirmDialog
Item {
id: viewContainer
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) {
if (modal.mode === "editor") {
if (event.key === Qt.Key_Escape) {
modal.mode = "history";
event.accepted = true;
}
return;
}
switch (event.key) {
case Qt.Key_Escape:
if (ClipboardService.keyboardNavigationActive) {
@@ -152,6 +169,10 @@ QtObject {
event.accepted = true;
}
return;
case Qt.Key_E:
editSelected();
event.accepted = true;
return;
}
}
@@ -10,7 +10,7 @@ Rectangle {
readonly property string hintsText: {
if (!wtypeAvailable)
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
@@ -22,13 +22,17 @@ Rectangle {
z: 100
Column {
width: parent.width - Theme.spacingL * 2
anchors.centerIn: parent
spacing: 2
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
color: Theme.surfaceText
width: parent.width
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
anchors.horizontalCenter: parent.horizontalCenter
}
@@ -36,6 +40,9 @@ Rectangle {
text: keyboardHints.hintsText
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
anchors.horizontalCenter: parent.horizontalCenter
}
}
@@ -38,7 +38,7 @@ Item {
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) {
if (!SettingsData.showDock)
@@ -58,7 +58,7 @@ Item {
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 real animationScaleCollapsed: Theme.effectScaleCollapsed
property real animationOffset: Theme.effectAnimOffset
@@ -68,7 +68,7 @@ Item {
property color borderColor: Theme.outlineMedium
property real borderWidth: 0
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 effectiveBorderColor: connectedSurfaceOverride ? "transparent" : borderColor
readonly property real effectiveBorderWidth: connectedSurfaceOverride ? 0 : borderWidth
@@ -346,7 +346,7 @@ Item {
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 shadowMotionPadding: {
if (Theme.isConnectedEffect)
if (frameOwnsConnectedChrome)
return 0;
if (animationType === "slide")
return 30;
@@ -10,10 +10,11 @@ Rectangle {
property var entry: null
property string cachedImageData: ""
property string cachedMimeType: ""
property var _requestedEntryId: null
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)
clip: true
@@ -24,8 +25,24 @@ Rectangle {
onEntryChanged: 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() {
cachedImageData = "";
cachedMimeType = "";
if (!canLoadImage || !entry?.id) {
_requestedEntryId = null;
return;
@@ -40,9 +57,13 @@ Rectangle {
return;
if (response.error)
return;
const data = response.result?.data ?? "";
if (data.length > 0)
cachedImageData = data;
const result = response.result ?? {};
const mimeType = (result.mimeType ?? entry?.mimeType ?? "").toString();
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 viewModeVersion: 0
property string viewModeContext: "spotlight"
property bool forceLinearNavigation: false
signal itemExecuted
signal searchCompleted
signal modeChanged(string mode)
signal modeChanged(string mode, bool userInitiated)
signal queryChanged(string query)
signal viewModeChanged(string sectionId, string mode)
signal searchQueryRequested(string query)
Ref {
service: AppSearchService
}
onActiveChanged: {
if (active) {
if (clipboardSearchEnabledInAll())
ClipboardService.ensureLauncherHistory();
} else {
if (!active) {
SessionData.addLauncherHistory(searchQuery);
sections = [];
flatModel = [];
selectedItem = null;
_clearModeCache();
ClipboardService.invalidateLauncherSearchCache();
}
}
@@ -88,11 +91,25 @@ Item {
Connections {
target: ClipboardService
function onInternalEntriesChanged() {
if (!active || !clipboardSearchEnabledInAll())
function onLauncherSearchReady(query) {
if (!active)
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;
searchDebounce.restart();
if (searchMode === "all" && clipboardSearchEnabledInAll() && query.length >= 2)
ClipboardService.ensureLauncherHistory();
if (searchMode !== "plugins" && query.startsWith("/")) {
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);
if (searchMode !== "plugins" && (searchMode === "files" || query.startsWith("/") || filesInAll) && query.length > 0) {
@@ -412,9 +440,14 @@ Item {
}
}
function setMode(mode, isAutoSwitch) {
if (searchMode === mode)
function setMode(mode, isAutoSwitch, fileTypeOverride, notPersist) {
if (searchMode === mode) {
if (mode === "files" && fileTypeOverride !== undefined && fileSearchType !== fileTypeOverride) {
fileSearchType = fileTypeOverride;
performFileSearch();
}
return;
}
if (isAutoSwitch) {
previousSearchMode = searchMode;
autoSwitchedToFiles = true;
@@ -422,10 +455,11 @@ Item {
autoSwitchedToFiles = false;
}
searchMode = mode;
modeChanged(mode);
if (mode === "files") {
fileSearchType = fileTypeOverride !== undefined ? fileTypeOverride : (SessionData.launcherLastFileSearchType || "all");
}
modeChanged(mode, !isAutoSwitch && notPersist !== true);
performSearch();
if (mode === "all" && clipboardSearchEnabledInAll() && searchQuery.length >= 2)
ClipboardService.ensureLauncherHistory();
var filesInAll = mode === "all" && (SettingsData.dankLauncherV2IncludeFilesInAll || SettingsData.dankLauncherV2IncludeFoldersInAll) && searchQuery.length > 0;
if (mode === "files" || filesInAll) {
fileSearchDebounce.restart();
@@ -437,7 +471,7 @@ Item {
return;
autoSwitchedToFiles = false;
searchMode = previousSearchMode;
modeChanged(previousSearchMode);
modeChanged(previousSearchMode, false);
performSearch();
}
@@ -533,6 +567,7 @@ Item {
if (fileSearchType === type)
return;
fileSearchType = type;
SessionData.setLauncherLastFileSearchType(type);
performFileSearch();
}
@@ -703,7 +738,8 @@ Item {
clearActivePluginViewPreference();
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;
sections = [];
flatModel = [];
@@ -993,7 +1029,8 @@ Item {
var includeFolders = SettingsData.dankLauncherV2IncludeFoldersInAll;
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") {
fileQuery = searchQuery.trim();
} else if (searchMode === "all" && (includeFiles || includeFolders)) {
@@ -1209,7 +1246,6 @@ Item {
}
if (clipboardSearchEnabledInAll()) {
ClipboardService.ensureLauncherHistory();
var clipboardItems = AppSearchService.getBuiltInLauncherItems("dms_clipboard_search", query);
var clipboardLimit = Math.min(clipboardItems.length, 8);
for (var j = 0; j < clipboardLimit; j++) {
@@ -1713,7 +1749,9 @@ Item {
function selectNext() {
keyboardNavigationActive = true;
_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) {
selectedFlatIndex = newIndex;
updateSelectedItem();
@@ -1723,7 +1761,9 @@ Item {
function selectPrevious() {
keyboardNavigationActive = true;
_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) {
selectedFlatIndex = newIndex;
updateSelectedItem();
@@ -1857,7 +1897,7 @@ Item {
if (browseTrigger && browseTrigger.length > 0) {
searchQueryRequested(browseTrigger);
} else {
setMode("plugins");
setMode("plugins", false, undefined, true);
pluginFilter = browsePluginId;
performSearch();
}
@@ -159,3 +159,14 @@ function sortPluginsOrdered(plugins, order) {
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 string resolvedConnectedBarSide: impl.item ? (impl.item.resolvedConnectedBarSide ?? "") : ""
readonly property bool launcherArcExtenderActive: impl.item ? (impl.item.launcherArcExtenderActive ?? false) : false
property bool triggerUsesOverlayLayer: false
signal dialogClosed
@@ -61,7 +62,7 @@ Item {
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)
property var _resolvedBackend: null
@@ -72,9 +73,6 @@ Item {
function onConnectedFrameModeActiveChanged() {
root._maybeResolveBackend();
}
function onFrameUseSpotlightLauncherChanged() {
root._maybeResolveBackend();
}
function onLauncherStyleChanged() {
root._maybeResolveBackend();
}
@@ -116,6 +114,7 @@ Item {
if (!it)
return;
it.modalHandle = root;
it.triggerUsesOverlayLayer = Qt.binding(() => root.triggerUsesOverlayLayer);
}
Connections {
@@ -13,13 +13,14 @@ Item {
readonly property var log: Log.scoped("DankLauncherV2ModalConnected")
property var modalHandle: root
property bool triggerUsesOverlayLayer: false
visible: false
property bool spotlightOpen: false
property bool keyboardActive: 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 bool openedFromOverview: false
property bool isClosing: false
@@ -40,6 +41,21 @@ Item {
readonly property real screenWidth: effectiveScreen?.width ?? 1920
readonly property real screenHeight: effectiveScreen?.height ?? 1080
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: {
switch (SettingsData.dankLauncherV2Size) {
@@ -74,7 +90,7 @@ Item {
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")
function _dockOccupiesSide(side) {
@@ -140,10 +156,10 @@ Item {
readonly property real modalX: frameOwnsConnectedChrome ? _connectedModalPos.x : ((screenWidth - modalWidth) / 2)
readonly property real modalY: frameOwnsConnectedChrome ? _connectedModalPos.y : ((screenHeight - modalHeight) / 2)
readonly property bool connectedSurfaceOverride: Theme.isConnectedEffect
readonly property int launcherAnimationDuration: Theme.isConnectedEffect ? Theme.popoutAnimationDuration : Theme.modalAnimationDuration
readonly property list<real> launcherEnterCurve: Theme.isConnectedEffect ? Theme.variantPopoutEnterCurve : Theme.variantModalEnterCurve
readonly property list<real> launcherExitCurve: Theme.isConnectedEffect ? Theme.variantPopoutExitCurve : Theme.variantModalExitCurve
readonly property bool connectedSurfaceOverride: frameOwnsConnectedChrome
readonly property int launcherAnimationDuration: frameOwnsConnectedChrome ? Theme.popoutAnimationDuration : Theme.modalAnimationDuration
readonly property list<real> launcherEnterCurve: frameOwnsConnectedChrome ? Theme.variantPopoutEnterCurve : Theme.variantModalEnterCurve
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 real cornerRadius: connectedSurfaceOverride ? Theme.connectedSurfaceRadius : Theme.cornerRadius
readonly property color borderColor: {
@@ -372,6 +388,7 @@ Item {
if (!spotlightContent)
return;
contentVisible = true;
spotlightContent.closeTransientUi?.();
// NOTE: forceActiveFocus() is deliberately NOT called here.
// It is deferred to after animation starts to avoid compositor IPC stalls.
@@ -379,12 +396,12 @@ Item {
spotlightContent.searchField.text = query;
}
if (spotlightContent.controller) {
var targetMode = mode || SessionData.launcherLastMode || "all";
var targetMode = mode || SessionData.getLauncherRestoreMode();
spotlightContent.controller.searchMode = targetMode;
spotlightContent.controller.activePluginId = "";
spotlightContent.controller.activePluginName = "";
spotlightContent.controller.pluginFilter = "";
spotlightContent.controller.fileSearchType = "all";
spotlightContent.controller.fileSearchType = SessionData.launcherLastFileSearchType || "all";
spotlightContent.controller.fileSearchExt = "";
spotlightContent.controller.fileSearchFolder = "";
spotlightContent.controller.fileSearchSort = "score";
@@ -464,6 +481,7 @@ Item {
function hide() {
if (!spotlightOpen)
return;
spotlightContent?.closeTransientUi?.();
openedFromOverview = false;
isClosing = true;
// For directional effects, defer contentVisible=false so content stays rendered during exit slide
@@ -521,8 +539,8 @@ Item {
Connections {
target: spotlightContent?.controller ?? null
function onModeChanged(mode) {
if (spotlightContent.controller.autoSwitchedToFiles)
function onModeChanged(mode, userInitiated) {
if (!userInitiated || !SettingsData.rememberLastMode)
return;
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)
WlrLayershell.namespace: "dms:spotlight:bg"
WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.layer: root.effectiveLauncherLayer
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
@@ -669,20 +687,7 @@ Item {
}
WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: {
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.layer: root.effectiveLauncherLayer
WlrLayershell.exclusiveZone: -1
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 => {
root.hide();
root.spotlightContent?.activeContextMenu?.handleKey(event);
if (!event.accepted)
root.hide();
event.accepted = true;
}
}
@@ -11,6 +11,7 @@ Item {
readonly property var log: Log.scoped("DankLauncherV2ModalSpotlight")
property var modalHandle: root
property bool triggerUsesOverlayLayer: false
visible: false
@@ -29,13 +30,29 @@ Item {
readonly property real screenWidth: effectiveScreen?.width ?? 1920
readonly property real screenHeight: effectiveScreen?.height ?? 1080
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 _closeDuration: 70
readonly property int _motionDuration: 90
readonly property int _openDuration: 50
readonly property int _closeDuration: 40
readonly property int _motionDuration: 60
// 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) {
if (!effectiveScreen || !frameConnected)
@@ -58,7 +75,7 @@ Item {
const searchBarH = 56;
const usableH = Math.max(searchBarH, screenHeight - insetT - insetB);
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));
}
@@ -125,9 +142,10 @@ Item {
if (!spotlightContent)
return;
contentVisible = true;
spotlightContent.closeTransientUi?.();
const targetQuery = query || (SettingsData.rememberLastQuery ? (SessionData.launcherLastQuery || "") : "");
const targetMode = mode || SessionData.launcherLastMode || "all";
const targetMode = mode || SessionData.getLauncherRestoreMode();
if (spotlightContent.searchField) {
spotlightContent.searchField.text = targetQuery;
@@ -185,6 +203,7 @@ Item {
function hide() {
if (!spotlightOpen)
return;
spotlightContent?.closeTransientUi?.();
openedFromOverview = false;
isClosing = true;
contentVisible = false;
@@ -259,11 +278,11 @@ Item {
PanelWindow {
id: clickCatcher
screen: launcherWindow.screen
visible: spotlightOpen || isClosing
visible: (spotlightOpen || isClosing) && !root.useBackgroundDarken
color: "transparent"
WlrLayershell.namespace: "dms:spotlight:clickcatcher"
WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.layer: root.effectiveLauncherLayer
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
@@ -324,31 +343,26 @@ Item {
}
WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "overlay":
return WlrLayershell.Overlay;
default:
return WlrLayershell.Top;
}
}
WlrLayershell.layer: root.effectiveLauncherLayer
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
anchors {
top: true
left: true
right: root.useBackgroundDarken
bottom: root.useBackgroundDarken
}
WlrLayershell.margins {
left: root.windowX
top: root.windowY
left: root.useBackgroundDarken ? 0 : root.windowX
top: root.useBackgroundDarken ? 0 : root.windowY
right: 0
bottom: 0
}
implicitWidth: root.windowWidth
implicitHeight: root.windowHeight
implicitWidth: root.useBackgroundDarken ? 0 : root.windowWidth
implicitHeight: root.useBackgroundDarken ? 0 : root.windowHeight
mask: Region {
item: inputMask
@@ -358,19 +372,44 @@ Item {
id: inputMask
visible: false
color: "transparent"
x: modalContainer.x
y: modalContainer.y + modalContainer.slideOffset
width: root.alignedWidth
height: root._contentImplicitH
x: root.useBackgroundDarken ? 0 : modalContainer.x
y: root.useBackgroundDarken ? 0 : modalContainer.y + modalContainer.slideOffset
width: root.useBackgroundDarken ? launcherWindow.width : root.alignedWidth
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 {
id: modalContainer
x: root.contentX
y: root.contentY
x: root.useBackgroundDarken ? root.alignedX : root.contentX
y: root.useBackgroundDarken ? root.alignedY : root.contentY
width: root.alignedWidth
height: root._animatedContentH
visible: _renderActive
z: 0
property bool _renderActive: contentVisible
property real slideOffset: contentVisible ? 0 : -root._animHeadroom
@@ -450,8 +489,12 @@ Item {
}
}
Keys.onPressed: event => root.spotlightContent?.activeContextMenu?.handleKey(event)
Keys.onEscapePressed: event => {
root.hide();
root.spotlightContent?.activeContextMenu?.handleKey(event);
if (!event.accepted)
root.hide();
event.accepted = true;
}
}
@@ -11,6 +11,7 @@ Item {
readonly property var log: Log.scoped("DankLauncherV2ModalStandalone")
property var modalHandle: root
property bool triggerUsesOverlayLayer: false
visible: false
@@ -31,7 +32,7 @@ Item {
readonly property real screenHeight: effectiveScreen?.height ?? 1080
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 int baseWidth: {
@@ -79,6 +80,21 @@ Item {
readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
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 color borderColor: {
if (!SettingsData.dankLauncherV2BorderEnabled)
@@ -117,6 +133,7 @@ Item {
if (!spotlightContent)
return;
contentVisible = true;
spotlightContent.closeTransientUi?.();
spotlightContent.searchField.forceActiveFocus();
var targetQuery = "";
@@ -131,12 +148,12 @@ Item {
spotlightContent.searchField.text = targetQuery;
}
if (spotlightContent.controller) {
var targetMode = mode || SessionData.launcherLastMode || "all";
var targetMode = mode || SessionData.getLauncherRestoreMode();
spotlightContent.controller.searchMode = targetMode;
spotlightContent.controller.activePluginId = "";
spotlightContent.controller.activePluginName = "";
spotlightContent.controller.pluginFilter = "";
spotlightContent.controller.fileSearchType = "all";
spotlightContent.controller.fileSearchType = SessionData.launcherLastFileSearchType || "all";
spotlightContent.controller.fileSearchExt = "";
spotlightContent.controller.fileSearchFolder = "";
spotlightContent.controller.fileSearchSort = "score";
@@ -195,6 +212,7 @@ Item {
function hide() {
if (!spotlightOpen)
return;
spotlightContent?.closeTransientUi?.();
openedFromOverview = false;
isClosing = true;
contentVisible = false;
@@ -242,8 +260,8 @@ Item {
Connections {
target: spotlightContent?.controller ?? null
function onModeChanged(mode) {
if (spotlightContent.controller.autoSwitchedToFiles)
function onModeChanged(mode, userInitiated) {
if (!userInitiated || !SettingsData.rememberLastMode || (mode !== "all" && mode !== "apps"))
return;
SessionData.setLauncherLastMode(mode);
}
@@ -296,12 +314,11 @@ Item {
PanelWindow {
id: clickCatcher
screen: launcherWindow.screen
visible: spotlightOpen || isClosing
visible: (spotlightOpen || isClosing) && !root.useBackgroundDarken
color: "transparent"
updatesEnabled: root.useBackgroundDarken && (spotlightOpen || isClosing)
WlrLayershell.namespace: "dms:spotlight:clickcatcher"
WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.layer: root.effectiveLauncherLayer
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
@@ -342,22 +359,6 @@ Item {
enabled: spotlightOpen
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 {
@@ -369,7 +370,7 @@ Item {
WindowBlur {
targetWindow: launcherWindow
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
blurY: modalContainer.y + modalContainer.height * (1 - s * op) * 0.5
blurWidth: contentVisible ? modalContainer.width * s * op : 0
@@ -378,39 +379,26 @@ Item {
}
WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: {
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.layer: root.effectiveLauncherLayer
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
anchors {
top: true
left: true
right: root.useBackgroundDarken
bottom: root.useBackgroundDarken
}
WlrLayershell.margins {
left: root.windowX
top: root.windowY
left: root.useBackgroundDarken ? 0 : root.windowX
top: root.useBackgroundDarken ? 0 : root.windowY
right: 0
bottom: 0
}
implicitWidth: root.windowWidth
implicitHeight: root.windowHeight
implicitWidth: root.useBackgroundDarken ? 0 : root.windowWidth
implicitHeight: root.useBackgroundDarken ? 0 : root.windowHeight
mask: Region {
item: launcherInputMask
@@ -420,22 +408,48 @@ Item {
id: launcherInputMask
visible: false
color: "transparent"
x: modalContainer.x
y: modalContainer.y
width: modalContainer.width
height: modalContainer.height
x: root.useBackgroundDarken ? 0 : modalContainer.x
y: root.useBackgroundDarken ? 0 : modalContainer.y
width: root.useBackgroundDarken ? launcherWindow.width : modalContainer.width
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 {
id: modalContainer
x: root.contentX
y: root.contentY
x: root.useBackgroundDarken ? root.alignedX : root.contentX
y: root.useBackgroundDarken ? root.alignedY : root.contentY
width: root.alignedWidth
height: root.alignedHeight
visible: _renderActive
z: 0
property bool _renderActive: contentVisible
property real publishedScale: contentVisible ? 1 : 0.96
property real publishedOpacity: contentVisible ? 1 : 0
opacity: contentVisible ? 1 : 0
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 {
target: root
function onContentVisibleChanged() {
@@ -514,8 +536,12 @@ Item {
}
}
Keys.onPressed: event => root.spotlightContent?.activeContextMenu?.handleKey(event)
Keys.onEscapePressed: event => {
root.hide();
root.spotlightContent?.activeContextMenu?.handleKey(event);
if (!event.accepted)
root.hide();
event.accepted = true;
}
}
@@ -17,10 +17,22 @@ FocusScope {
property alias controller: controller
property alias resultsList: resultsList
property alias actionPanel: actionPanel
readonly property alias activeContextMenu: contextMenu
property bool editMode: false
property var editingApp: null
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() {
resultsList.resetScroll();
@@ -30,6 +42,12 @@ FocusScope {
searchField.forceActiveFocus();
}
function closeTransientUi() {
contextMenu.hide();
actionPanel.hide();
root.enabled = true;
}
function openEditMode(app) {
if (!app)
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 => {
if (editMode) {
if (event.key === Qt.Key_Escape) {
@@ -257,13 +290,6 @@ FocusScope {
}
event.accepted = false;
return;
case Qt.Key_Slash:
if (event.modifiers === Qt.NoModifier && searchField.text.length === 0) {
controller.setMode("files", true);
return;
}
event.accepted = false;
return;
default:
event.accepted = false;
}
@@ -284,7 +310,7 @@ FocusScope {
anchors.bottom: parent.bottom
anchors.leftMargin: 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
visible: showFooter
clip: true
@@ -293,7 +319,7 @@ FocusScope {
anchors.fill: parent
anchors.topMargin: -Theme.cornerRadius
// 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)
radius: Theme.cornerRadius
}
@@ -458,9 +484,11 @@ FocusScope {
id: searchField
width: parent.width - (pluginBadge.visible ? pluginBadge.width + Theme.spacingS : 0)
cornerRadius: Theme.cornerRadius
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
backgroundColor: root._launcherSearchFieldColor
normalBorderColor: root._launcherSearchBorderColor
focusedBorderColor: root._launcherSearchFocusedBorderColor
borderWidth: 1
focusedBorderWidth: 2
leftIconName: controller.activePluginId ? "extension" : controller.searchQuery.startsWith("/") ? "folder" : "search"
leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText
@@ -1,35 +1,72 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
Popup {
Item {
id: root
visible: false
width: 0
height: 0
property var item: null
property var controller: null
property var searchField: null
property var parentHandler: null
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 editAppRequested(var app)
TextMetrics {
id: menuTextMetrics
text: root.longestMenuText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Normal
}
function hasContextMenuActions(spotlightItem) {
if (!spotlightItem)
return false;
if (spotlightItem.type === "app")
return true;
if (spotlightItem.type === "plugin" && spotlightItem.pluginId) {
var instance = PluginService.pluginInstances[spotlightItem.pluginId];
const instance = PluginService.pluginInstances[spotlightItem.pluginId];
if (!instance)
return false;
if (typeof instance.getContextMenuActions !== "function")
return false;
var actions = instance.getContextMenuActions(spotlightItem.data);
const actions = instance.getContextMenuActions(spotlightItem.data);
return Array.isArray(actions) && actions.length > 0;
}
if (spotlightItem.actions && spotlightItem.actions.length > 0)
@@ -54,13 +91,13 @@ Popup {
if (!isPluginItem || !item?.pluginId)
return [];
var instance = PluginService.pluginInstances[item.pluginId];
const instance = PluginService.pluginInstances[item.pluginId];
if (!instance)
return [];
if (typeof instance.getContextMenuActions !== "function")
return [];
var actions = instance.getContextMenuActions(item.data);
const actions = instance.getContextMenuActions(item.data);
if (!Array.isArray(actions))
return [];
@@ -68,8 +105,8 @@ Popup {
}
function executePluginAction(actionOrObj) {
var actionFunc = typeof actionOrObj === "function" ? actionOrObj : actionOrObj?.action;
var closeLauncher = typeof actionOrObj === "object" && actionOrObj?.closeLauncher;
const actionFunc = typeof actionOrObj === "function" ? actionOrObj : actionOrObj?.action;
const closeLauncher = typeof actionOrObj === "object" && actionOrObj?.closeLauncher;
if (typeof actionFunc === "function")
actionFunc();
@@ -90,12 +127,12 @@ Popup {
}
readonly property var menuItems: {
var items = [];
const items = [];
if (isPluginItem) {
var pluginActions = getPluginContextMenuActions();
for (var i = 0; i < pluginActions.length; i++) {
var act = pluginActions[i];
const pluginActions = getPluginContextMenuActions();
for (let i = 0; i < pluginActions.length; i++) {
const act = pluginActions[i];
items.push({
type: "item",
icon: act.icon || "play_arrow",
@@ -107,8 +144,8 @@ Popup {
}
if (item?.type !== "app" && item?.actions && item.actions.length > 0) {
for (var i = 0; i < item.actions.length; i++) {
var genericAct = item.actions[i];
for (let i = 0; i < item.actions.length; i++) {
const genericAct = item.actions[i];
items.push({
type: "item",
icon: genericAct.icon || "play_arrow",
@@ -149,8 +186,8 @@ Popup {
items.push({
type: "separator"
});
for (var i = 0; i < item.actions.length; i++) {
var act = item.actions[i];
for (let i = 0; i < item.actions.length; i++) {
const act = item.actions[i];
items.push({
type: "item",
icon: act.icon || "play_arrow",
@@ -183,43 +220,52 @@ Popup {
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) {
if (!spotlightItem?.data)
return;
item = spotlightItem;
selectedMenuIndex = fromKeyboard ? 0 : -1;
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)
parentHandler.enabled = false;
Qt.callLater(() => {
var parentW = parent?.width ?? 500;
var parentH = parent?.height ?? 600;
var menuW = width > 0 ? width : 200;
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();
menuFlickable.contentY = 0;
keyboardHandler.forceActiveFocus();
ensureSelectedVisible();
});
}
function hide() {
if (parentHandler)
parentHandler.enabled = true;
close();
if (!renderActive)
return;
openState = false;
hideRequested();
}
function togglePin() {
@@ -286,31 +332,96 @@ Popup {
property bool keyboardNavigation: false
readonly property int visibleItemCount: {
var count = 0;
for (var i = 0; i < menuItems.length; i++) {
let count = 0;
for (let i = 0; i < menuItems.length; i++) {
if (menuItems[i].type === "item")
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() {
if (visibleItemCount > 0)
if (visibleItemCount > 0) {
keyboardNavigation = true;
selectedMenuIndex = (selectedMenuIndex + 1) % visibleItemCount;
ensureSelectedVisible();
}
}
function selectPrevious() {
if (visibleItemCount > 0)
if (visibleItemCount > 0) {
keyboardNavigation = true;
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() {
var itemIndex = 0;
for (var i = 0; i < menuItems.length; i++) {
let itemIndex = 0;
for (let i = 0; i < menuItems.length; i++) {
if (menuItems[i].type !== "item")
continue;
if (itemIndex === selectedMenuIndex) {
var menuItem = menuItems[i];
const menuItem = menuItems[i];
if (menuItem.action)
menuItem.action();
else if (menuItem.pluginAction)
@@ -325,209 +436,233 @@ Popup {
}
}
width: menuContainer.implicitWidth
height: menuContainer.implicitHeight
padding: 0
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
modal: true
dim: false
background: Item {}
PanelWindow {
id: menuWindow
onOpened: {
Qt.callLater(() => keyboardHandler.forceActiveFocus());
}
screen: root.targetScreen
visible: root.renderActive
color: "transparent"
onClosed: {
if (parentHandler)
parentHandler.enabled = true;
if (searchField?.visible) {
Qt.callLater(() => searchField.forceActiveFocus());
}
}
WlrLayershell.namespace: "dms:launcher-context-menu"
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: root.renderActive ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
enter: Transition {
NumberAnimation {
property: "opacity"
from: 0
to: 1
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;
}
anchors {
top: true
left: true
right: true
bottom: true
}
Rectangle {
id: menuContainer
WindowBlur {
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
implicitWidth: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)
implicitHeight: menuColumn.implicitHeight + Theme.spacingS * 2
color: Theme.floatingSurface
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
z: -1
enabled: root.renderActive
onClicked: root.hide()
}
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 {
anchors.fill: parent
anchors.topMargin: 4
anchors.leftMargin: 2
anchors.rightMargin: -2
anchors.bottomMargin: -4
radius: parent.radius
color: Qt.rgba(0, 0, 0, 0.15)
z: -1
}
id: menuContainer
x: Math.max(root.menuMargin, Math.min(menuWindow.width - width - root.menuMargin, root.anchorX))
y: Math.max(root.menuMargin, Math.min(menuWindow.height - height - root.menuMargin, root.anchorY))
width: root.effectiveMenuWidth
height: root.effectiveMenuHeight
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
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
opacity: root.openState ? 1 : 0
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Repeater {
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: {
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)
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
onRunningChanged: {
if (!running && !root.openState) {
root.renderActive = false;
if (root.parentHandler)
root.parentHandler.enabled = true;
if (root.searchField?.visible)
Qt.callLater(() => root.searchField.forceActiveFocus());
}
}
}
}
Rectangle {
visible: menuItemDelegate.modelData.type === "item"
width: parent.width
height: parent.height
radius: Theme.cornerRadius
color: {
if (root.keyboardNavigation && root.selectedMenuIndex === menuItemDelegate.itemIndex) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2);
Rectangle {
anchors.fill: parent
anchors.topMargin: 4
anchors.leftMargin: 2
anchors.rightMargin: -2
anchors.bottomMargin: -4
radius: parent.radius
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 {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Rectangle {
visible: menuItemDelegate.modelData.type === "separator"
width: parent.width - Theme.spacingS * 2
height: parent.height
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Item {
width: Theme.iconSize - 2
height: Theme.iconSize - 2
anchors.verticalCenter: parent.verticalCenter
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
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
StyledText {
text: menuItemDelegate.modelData.text || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
width: parent.width - (Theme.iconSize - 2) - Theme.spacingS
}
}
Rectangle {
visible: menuItemDelegate.modelData.type === "item"
width: parent.width
height: parent.height
radius: Theme.cornerRadius
color: {
if (root.keyboardNavigation && root.selectedMenuIndex === menuItemDelegate.itemIndex) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2);
}
return itemMouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
}
DankRipple {
id: menuItemRipple
rippleColor: Theme.surfaceText
cornerRadius: Theme.cornerRadius
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
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: {
var 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);
Item {
width: Theme.iconSize - 2
height: Theme.iconSize - 2
anchors.verticalCenter: parent.verticalCenter
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 {
text: menuItemDelegate.modelData.text || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
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 alias searchField: searchInput
property alias controller: searchController
readonly property alias activeContextMenu: contextMenu
readonly property bool _hasQuery: searchInput.text.length > 0
readonly property real _searchBarH: 56
readonly property real _surfaceInset: BlurService.enabled ? (_hasQuery ? Theme.spacingS : Theme.spacingXS) : 0
readonly property real _searchAreaH: _searchBarH + _surfaceInset * 2
readonly property real _searchAreaH: _searchBarH
readonly property real _statusH: 92
readonly property real _rowH: 64
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 int _fastDuration: 90
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() {
resultsList.resetScroll();
}
function closeTransientUi() {
contextMenu.hide();
root.enabled = true;
}
function _buildRows() {
const flat = searchController.flatModel || [];
const sections = searchController.sections || [];
@@ -122,13 +143,11 @@ FocusScope {
}
break;
case Qt.Key_Tab:
if (_hasQuery)
_cycleCategory(false);
_cycleCategory(false);
event.accepted = true;
return;
case Qt.Key_Backtab:
if (_hasQuery)
_cycleCategory(true);
_cycleCategory(true);
event.accepted = true;
return;
case Qt.Key_Return:
@@ -177,13 +196,6 @@ FocusScope {
return;
}
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;
@@ -193,6 +205,7 @@ FocusScope {
id: searchController
active: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
viewModeContext: "spotlight"
forceLinearNavigation: true
onItemExecuted: {
root.parentModal?.hide();
@@ -210,10 +223,25 @@ FocusScope {
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 {
target: searchController
function onModeChanged(mode) {
if (searchController.autoSwitchedToFiles)
function onModeChanged(mode, userInitiated) {
if (!userInitiated || !SettingsData.rememberLastMode)
return;
SessionData.setLauncherLastMode(mode);
}
@@ -233,11 +261,8 @@ FocusScope {
Rectangle {
id: searchBarSurface
anchors.fill: parent
anchors.margins: root._surfaceInset
radius: height / 2
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
radius: Theme.cornerRadius
color: root._searchSurfaceColor
Behavior on color {
ColorAnimation {
@@ -254,7 +279,7 @@ FocusScope {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
color: searchInput.activeFocus ? Theme.primaryContainer : Theme.surfaceContainer
color: root._searchWellColor
DankIcon {
anchors.centerIn: parent
@@ -273,8 +298,8 @@ FocusScope {
Row {
id: categoryRow
visible: SettingsData.spotlightBarShowModeChips || root._hasQuery
spacing: Theme.spacingXS
visible: root._hasQuery
anchors.verticalCenter: parent.verticalCenter
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 {
id: resultsContainer
anchors.top: searchBarItem.bottom
anchors.topMargin: 1
anchors.left: parent.left
anchors.right: parent.right
clip: true
@@ -12,6 +12,7 @@ Item {
property var controller: null
property bool hasQuery: false
property var rows: []
readonly property real bottomInset: Theme.spacingS
signal itemRightClicked(int index, var item, real mouseX, real mouseY)
@@ -53,7 +54,11 @@ Item {
DankListView {
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
visible: root.rows.length > 0
@@ -64,11 +69,6 @@ Item {
objectProp: "_rowId"
}
add: null
remove: null
displaced: null
move: null
delegate: Item {
id: delegateRoot
required property var modelData
@@ -103,7 +103,11 @@ 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
Row {
+13
View File
@@ -81,6 +81,8 @@ DankModal {
executeAction(action);
}
signal switchUserRequested
function executeAction(action) {
if (action === "lock") {
close();
@@ -92,6 +94,11 @@ DankModal {
Quickshell.execDetached(["dms", "restart"]);
return;
}
if (action === "switchuser") {
close();
switchUserRequested();
return;
}
close();
root.powerActionRequested(action, "", "");
}
@@ -216,6 +223,12 @@ DankModal {
"label": I18n.tr("Restart DMS"),
"key": "D"
};
case "switchuser":
return {
"icon": "switch_account",
"label": I18n.tr("Switch User"),
"key": "U"
};
default:
return {
"icon": "help",
@@ -64,6 +64,7 @@ FocusScope {
sourceComponent: KeybindsTab {
parentModal: root.parentModal
requestedSearchQuery: root.parentModal?.keybindSearchQuery ?? ""
}
onActiveChanged: {
@@ -554,5 +555,20 @@ FocusScope {
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 menuVisible: !isCompactMode
property bool enableAnimations: true
property string keybindSearchQuery: ""
signal closingModal
@@ -73,6 +74,11 @@ FloatingWindow {
return sidebar.resolveTabIndex(tabName);
}
function showKeybindsSearch(query: string) {
keybindSearchQuery = query || "";
showWithTabName("keybinds");
}
function toggleMenu() {
enableAnimations = true;
menuVisible = !menuVisible;
@@ -293,6 +293,12 @@ Rectangle {
"tabIndex": 20,
"updaterOnly": true
},
{
"id": "users",
"text": I18n.tr("Users"),
"icon": "manage_accounts",
"tabIndex": 35
},
{
"id": "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
function onDeviceNameChanged(newDeviceName) {
if (root.expandedWidgetData && root.expandedWidgetData.id === "brightnessSlider") {
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);
if (root.collapseCallback) {
root.collapseCallback();
}
if (!root.expandedWidgetData || root.expandedWidgetData.id !== "brightnessSlider") {
return;
}
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 || "")
width: parent.width
height: 60
iconBlinking: {
const id = widgetData.id || "";
if (id === "wifi")
return NetworkService.isWifiConnecting;
if (id === "bluetooth")
return BluetoothService.connecting;
return false;
}
iconName: {
switch (widgetData.id || "") {
case "wifi":
{
if (NetworkService.wifiToggling)
return "sync";
if (NetworkService.isConnecting && !NetworkService.ethernetConnected)
return NetworkService.wifiSignalIcon;
const status = NetworkService.networkStatus;
if (status === "ethernet")
@@ -360,6 +370,8 @@ Column {
{
if (NetworkService.wifiToggling)
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;
if (status === "ethernet")
@@ -400,6 +412,8 @@ Column {
{
if (NetworkService.wifiToggling)
return I18n.tr("Please wait...", "network status");
if (NetworkService.isConnecting && !NetworkService.ethernetConnected)
return I18n.tr("Connecting...", "network status");
const status = NetworkService.networkStatus;
if (status === "ethernet")
@@ -422,6 +436,8 @@ Column {
return I18n.tr("No adapters", "bluetooth status");
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled)
return I18n.tr("Off", "bluetooth status");
if (BluetoothService.connecting)
return I18n.tr("Connecting...", "bluetooth status");
const primaryDevice = (() => {
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
return null;
@@ -23,79 +23,103 @@ Rectangle {
if (!screenName)
return "";
const screen = Quickshell.screens.find(s => s.name === screenName);
if (screen) {
if (screen)
return SettingsData.getScreenDisplayName(screen);
}
if (SettingsData.displayNameMode === "model" && screenModel && screenModel.length > 0) {
if (SettingsData.displayNameMode === "model" && screenModel && screenModel.length > 0)
return screenModel;
}
return screenName;
}
function resolveDeviceName() {
if (!DisplayService.brightnessAvailable || !DisplayService.devices || DisplayService.devices.length === 0) {
function resolveCurrentDevice() {
const devices = DisplayService.devices || [];
if (!DisplayService.brightnessAvailable || devices.length === 0)
return "";
}
const pinKey = getScreenPinKey();
if (pinKey.length > 0) {
const pins = SettingsData.brightnessDevicePins || {};
const pinnedDevice = pins[pinKey];
if (pinnedDevice && pinnedDevice.length > 0) {
const found = DisplayService.devices.find(dev => dev.name === pinnedDevice);
const found = devices.find(d => d.name === pinnedDevice);
if (found)
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) {
const found = DisplayService.devices.find(dev => dev.name === initialDeviceName);
const found = devices.find(d => d.name === initialDeviceName);
if (found)
return found.name;
}
const currentDeviceNameFromService = DisplayService.currentDevice;
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");
const backlight = devices.find(d => d.class === "backlight");
if (backlight)
return backlight.name;
const ddc = DisplayService.devices.find(d => d.class === "ddc");
const ddc = devices.find(d => d.class === "ddc");
if (ddc)
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: {
currentDeviceName = resolveDeviceName();
root.currentDeviceName = resolveCurrentDevice();
}
property bool isPinnedToScreen: {
function isDevicePinnedToScreen(deviceName) {
const pinKey = getScreenPinKey();
if (!pinKey || pinKey.length === 0)
if (!pinKey || !deviceName)
return false;
const pins = SettingsData.brightnessDevicePins || {};
return pins[pinKey] === currentDeviceName;
return pins[pinKey] === deviceName;
}
function togglePinToScreen() {
function togglePinForDevice(deviceName) {
const pinKey = getScreenPinKey();
if (!pinKey || pinKey.length === 0 || !currentDeviceName || currentDeviceName.length === 0)
if (!pinKey || !deviceName)
return;
const pins = JSON.parse(JSON.stringify(SettingsData.brightnessDevicePins || {}));
if (isPinnedToScreen) {
if (pins[pinKey] === deviceName) {
delete pins[pinKey];
} else {
pins[pinKey] = currentDeviceName;
pins[pinKey] = deviceName;
}
SettingsData.set("brightnessDevicePins", pins);
}
@@ -153,18 +177,23 @@ Rectangle {
}
Rectangle {
id: monitorHeader
width: parent.width
height: 40
visible: screenName && screenName.length > 0 && DisplayService.devices && DisplayService.devices.length > 1
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
property bool currentDevicePinned: root.isDevicePinnedToScreen(currentDeviceName)
Item {
anchors.fill: parent
anchors.margins: Theme.spacingM
Row {
anchors.left: parent.left
anchors.right: globalPinButton.left
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
@@ -180,47 +209,51 @@ Rectangle {
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
width: parent.width - Theme.iconSize - Theme.spacingM
}
}
Rectangle {
id: globalPinButton
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: pinRow.width + Theme.spacingS * 2
width: globalPinRow.width + Theme.spacingS * 2
height: 28
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 {
id: pinRow
id: globalPinRow
anchors.centerIn: parent
spacing: 4
DankIcon {
name: isPinnedToScreen ? "push_pin" : "push_pin"
name: "push_pin"
size: 16
color: isPinnedToScreen ? Theme.primary : Theme.surfaceText
color: monitorHeader.currentDevicePinned ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: isPinnedToScreen ? I18n.tr("Pinned") : I18n.tr("Pin")
text: monitorHeader.currentDevicePinned ? I18n.tr("Pinned") : I18n.tr("Pin")
font.pixelSize: Theme.fontSizeSmall
color: isPinnedToScreen ? Theme.primary : Theme.surfaceText
color: monitorHeader.currentDevicePinned ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankRipple {
id: pinRipple
id: globalPinRipple
cornerRadius: parent.radius
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
onClicked: root.togglePinToScreen()
enabled: currentDeviceName && currentDeviceName.length > 0
onPressed: mouse => globalPinRipple.trigger(mouse.x, mouse.y)
onClicked: root.togglePinForDevice(currentDeviceName)
}
}
}
@@ -229,9 +262,17 @@ Rectangle {
Repeater {
model: DisplayService.devices || []
delegate: Rectangle {
id: deviceCard
required property var modelData
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: {
DisplayService.brightnessVersion;
return DisplayService.getDeviceBrightness(modelData.name);
@@ -241,8 +282,8 @@ Rectangle {
height: 100
radius: Theme.cornerRadius
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.width: modelData.name === currentDeviceName ? 2 : 0
border.color: selected ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: selected ? 2 : 0
Column {
anchors.fill: parent
@@ -251,10 +292,12 @@ Rectangle {
Item {
width: parent.width
height: Math.max(deviceIconColumn.height, deviceInfoColumn.height, exponentControls.height)
height: Math.max(deviceIconColumn.height, deviceInfoColumn.height, rightControls.height)
Row {
anchors.left: parent.left
anchors.right: rightControls.left
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
@@ -281,7 +324,7 @@ Rectangle {
}
}
size: Theme.iconSize
color: modelData.name === currentDeviceName ? Theme.primary : Theme.surfaceText
color: deviceCard.selected ? Theme.primary : Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
@@ -296,7 +339,7 @@ Rectangle {
Column {
id: deviceInfoColumn
anchors.verticalCenter: parent.verticalCenter
width: parent.parent.width - deviceIconColumn.width - exponentControls.width - Theme.spacingM * 3
width: parent.width - deviceIconColumn.width - Theme.spacingM
StyledText {
text: {
@@ -309,7 +352,7 @@ Rectangle {
}
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData.name === currentDeviceName ? Font.Medium : Font.Normal
font.weight: deviceCard.selected ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
horizontalAlignment: Text.AlignLeft
@@ -345,80 +388,107 @@ Rectangle {
}
Row {
id: exponentControls
width: 140
id: rightControls
height: 28
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
visible: SessionData.getBrightnessExponential(modelData.name)
spacing: Theme.spacingS
z: 1
StyledRect {
width: 28
Row {
id: exponentControls
height: 28
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
opacity: SessionData.getBrightnessExponent(modelData.name) > 1.0 ? 1.0 : 0.4
spacing: Theme.spacingXS
visible: SessionData.getBrightnessExponential(modelData.name)
DankIcon {
anchors.centerIn: parent
name: "remove"
size: 14
color: Theme.surfaceText
StyledRect {
width: 28
height: 28
radius: Theme.cornerRadius
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 {
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);
StyledRect {
width: 50
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
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 {
width: 50
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 {
id: pinButton
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
visible: root.screenName && root.screenName.length > 0 && DisplayService.devices && DisplayService.devices.length > 1
color: devicePinnedHere ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
DankIcon {
anchors.centerIn: parent
name: "add"
name: "push_pin"
size: 14
color: Theme.surfaceText
color: devicePinnedHere ? Theme.primary : 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);
}
onClicked: root.togglePinForDevice(modelData.name)
}
}
}
@@ -474,22 +544,11 @@ Rectangle {
MouseArea {
anchors.fill: parent
anchors.bottomMargin: 28
anchors.rightMargin: SessionData.getBrightnessExponential(modelData.name) ? 145 : 0
anchors.rightMargin: rightControls.width + Theme.spacingS
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: mouse => deviceRipple.trigger(mouse.x, mouse.y)
onClicked: {
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);
}
onClicked: root.selectDevice(modelData.name)
}
}
}
@@ -541,7 +541,11 @@ Rectangle {
return -1;
if (b.ssid === ssid)
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;
}
@@ -1,4 +1,5 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
@@ -31,8 +32,10 @@ Row {
}
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 pinnedDevice = pins[screenName];
const pinnedDevice = pins[pinKey];
if (pinnedDevice && pinnedDevice.length > 0) {
const found = DisplayService.devices.find(dev => dev.name === pinnedDevice);
if (found) {
@@ -10,6 +10,7 @@ Rectangle {
property string iconName: ""
property color iconColor: Theme.surfaceText
property bool iconBlinking: false
property string primaryText: ""
property string secondaryText: ""
property bool expanded: false
@@ -109,10 +110,16 @@ Rectangle {
}
DankIcon {
id: pillIcon
anchors.centerIn: parent
name: iconName
size: Theme.iconSize
color: isActive ? _tileIconActive : _tileIconInactive
DankBlink {
target: pillIcon
running: root.iconBlinking
}
}
DankRipple {
+42 -11
View File
@@ -10,13 +10,15 @@ Item {
required property var axis
required property var barConfig
visible: !SettingsData.frameEnabled
readonly property bool frameShapesBar: SettingsData.frameEnabled && barWindow.usesFrameBarChrome
visible: !frameShapesBar
anchors.fill: parent
anchors.left: parent.left
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.rightMargin: -(gothEnabled && axis.isVertical && axis.edge === "left" ? barWindow._wingR : 0)
anchors.topMargin: -(gothEnabled && !axis.isVertical && axis.edge === "bottom" ? barWindow._wingR : 0)
@@ -39,11 +41,11 @@ Item {
}
property real rt: {
if (SettingsData.frameEnabled)
if (frameShapesBar)
return SettingsData.frameRounding;
if (barConfig?.squareCorners ?? false)
return 0;
if (barWindow.hasMaximizedToplevel)
if (barWindow.flattenForMaximizedWindow && barWindow.hasMaximizedToplevel)
return 0;
return Theme.cornerRadius;
}
@@ -113,9 +115,32 @@ Item {
readonly property real shadowOffsetX: Theme.elevationOffsetXFor(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 borderFullPath: generateBorderFullPath(width, height)
readonly property string borderEdgePath: generateBorderEdgePath(width, height)
readonly property string mainPath: {
frameShapesBar;
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 borderFullPathCorrectShape: false
property bool borderEdgePathCorrectShape: false
@@ -136,6 +161,12 @@ Item {
}
}
onFrameShapesBarChanged: {
mainPathCorrectShape = false;
borderFullPathCorrectShape = false;
borderEdgePathCorrectShape = false;
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
@@ -259,7 +290,7 @@ Item {
h = h - wing;
const r = wing;
const cr = rt;
const crE = SettingsData.frameEnabled ? 0 : cr;
const crE = frameShapesBar ? 0 : cr;
let d = `M ${crE} 0`;
d += ` L ${w - crE} 0`;
@@ -290,7 +321,7 @@ Item {
h = h - wing;
const r = wing;
const cr = rt;
const crE = SettingsData.frameEnabled ? 0 : cr;
const crE = frameShapesBar ? 0 : cr;
let d = `M ${crE} ${fullH}`;
d += ` L ${w - crE} ${fullH}`;
@@ -320,7 +351,7 @@ Item {
w = w - wing;
const r = wing;
const cr = rt;
const crE = SettingsData.frameEnabled ? 0 : cr;
const crE = frameShapesBar ? 0 : cr;
let d = `M 0 ${crE}`;
d += ` L 0 ${h - crE}`;
@@ -351,7 +382,7 @@ Item {
w = w - wing;
const r = wing;
const cr = rt;
const crE = SettingsData.frameEnabled ? 0 : cr;
const crE = frameShapesBar ? 0 : cr;
let d = `M ${fullW} ${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 outlineThickness: (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0
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 _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 string _barScreenName: _hasBarWindow ? (barWindow.screenName || "") : ""
readonly property bool hasAdjacentTopBarLive: _hasBarWindow && barWindow.hasAdjacentTopBar
@@ -47,22 +48,22 @@ Item {
_hadAdjacentRightBar = true
readonly property real _frameLeftInset: {
if (!_hasBarWindow || !SettingsData.frameEnabled || _barIsVertical)
if (!_hasBarWindow || !SettingsData.frameEnabled || !_usesFrameBarChrome || _barIsVertical)
return 0;
return hasAdjacentLeftBarLive ? SettingsData.frameBarSize : (_hadAdjacentLeftBar ? _frameEdgeFloorInset : 0);
}
readonly property real _frameRightInset: {
if (!_hasBarWindow || !SettingsData.frameEnabled || _barIsVertical)
if (!_hasBarWindow || !SettingsData.frameEnabled || !_usesFrameBarChrome || _barIsVertical)
return 0;
return hasAdjacentRightBarLive ? SettingsData.frameBarSize : (_hadAdjacentRightBar ? _frameEdgeFloorInset : 0);
}
readonly property real _frameTopInset: {
if (!_hasBarWindow || !SettingsData.frameEnabled || !_barIsVertical)
if (!_hasBarWindow || !SettingsData.frameEnabled || !_usesFrameBarChrome || !_barIsVertical)
return 0;
return hasAdjacentTopBarLive ? SettingsData.frameThickness : (_hadAdjacentTopBar ? _frameEdgeFloorInset : 0);
}
readonly property real _frameBottomInset: {
if (!_hasBarWindow || !SettingsData.frameEnabled || !_barIsVertical)
if (!_hasBarWindow || !SettingsData.frameEnabled || !_usesFrameBarChrome || !_barIsVertical)
return 0;
return hasAdjacentBottomBarLive ? SettingsData.frameThickness : (_hadAdjacentBottomBar ? _frameEdgeFloorInset : 0);
}
@@ -95,7 +96,7 @@ Item {
}
Behavior on anchors.leftMargin {
enabled: _animateFrameInsets && SettingsData.frameEnabled
enabled: _animateFrameInsets && _usesFrameBarChrome
NumberAnimation {
duration: Theme.shortDuration
easing.type: Easing.OutCubic
@@ -103,7 +104,7 @@ Item {
}
Behavior on anchors.rightMargin {
enabled: _animateFrameInsets && SettingsData.frameEnabled
enabled: _animateFrameInsets && _usesFrameBarChrome
NumberAnimation {
duration: Theme.shortDuration
easing.type: Easing.OutCubic
@@ -111,7 +112,7 @@ Item {
}
Behavior on anchors.topMargin {
enabled: _animateFrameInsets && SettingsData.frameEnabled
enabled: _animateFrameInsets && _usesFrameBarChrome
NumberAnimation {
duration: Theme.shortDuration
easing.type: Easing.OutCubic
@@ -119,7 +120,7 @@ Item {
}
Behavior on anchors.bottomMargin {
enabled: _animateFrameInsets && SettingsData.frameEnabled
enabled: _animateFrameInsets && _usesFrameBarChrome
NumberAnimation {
duration: Theme.shortDuration
easing.type: Easing.OutCubic
+33 -29
View File
@@ -108,6 +108,8 @@ PanelWindow {
triggerDashTab(2);
}
readonly property bool usesOverlayLayer: CompositorService.framePeerSurfacesUseOverlayForScreen(barWindow.screen) || (barConfig?.useOverlayLayer ?? false)
readonly property var dBarLayer: {
switch (Quickshell.env("DMS_DANKBAR_LAYER")) {
case "bottom":
@@ -119,10 +121,7 @@ PanelWindow {
case "top":
return WlrLayer.Top;
default:
// Elevate to Overlay when Frame is enabled so the bar stays above
// 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;
return barWindow.usesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top;
}
}
@@ -152,6 +151,16 @@ PanelWindow {
onTriggered: barBlur.rebuild()
}
Connections {
target: barWindow
function onUsesConnectedFrameChromeChanged() {
_blurRebuildTimer.restart();
}
function onUsesFrameBarChromeChanged() {
_blurRebuildTimer.restart();
}
}
Component {
id: blurRegionComp
Region {}
@@ -179,7 +188,7 @@ PanelWindow {
// In frame mode, FrameWindow owns the blur region for the entire screen edge
// (including the bar area). The bar must not set its own competing blur region
// so that frameBlurEnabled acts as the single control for all blur in frame mode.
if (SettingsData.frameEnabled)
if (SettingsData.frameEnabled && barWindow.usesFrameBarChrome)
return;
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 string _barId: barConfig?.id ?? "default"
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() {
const live = SettingsData.barConfigs.find(c => c.id === _barId);
@@ -316,16 +325,14 @@ PanelWindow {
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
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
function _updateHasMaximizedToplevel() {
@@ -427,7 +434,7 @@ PanelWindow {
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 {
enabled: barWindow.visible
@@ -438,7 +445,7 @@ PanelWindow {
}
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 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.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 {
id: inputMask
@@ -647,9 +654,9 @@ PanelWindow {
readonly property bool inOverviewWithShow: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview
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: {
if (!axis.isVertical) {
@@ -719,7 +726,7 @@ PanelWindow {
item: clickThroughEnabled ? null : inputMask
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,
"y": 0,
"w": 0,
@@ -732,7 +739,7 @@ PanelWindow {
}
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,
"y": 0,
"w": 0,
@@ -745,7 +752,7 @@ PanelWindow {
}
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,
"y": 0,
"w": 0,
@@ -826,9 +833,6 @@ PanelWindow {
}
property bool reveal: {
if (barWindow.hasFullscreenToplevel)
return false;
const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview;
if (inOverviewWithShow)
return true;
@@ -897,9 +901,9 @@ PanelWindow {
bottom: barWindow.isVertical ? parent.bottom : undefined
}
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
enabled: (barConfig?.autoHide ?? false) && !inOverview && !barWindow.hasFullscreenToplevel
enabled: (barConfig?.autoHide ?? false) && !inOverview
Item {
id: topBarContainer
@@ -131,9 +131,19 @@ BasePill {
function getNetworkIconColor() {
if (NetworkService.wifiToggling)
return Theme.primary;
if (NetworkService.isConnecting && !NetworkService.ethernetConnected)
return Theme.primary;
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() {
if (!AudioService.sink?.audio)
return "volume_up";
@@ -485,6 +495,7 @@ BasePill {
}
DankIcon {
id: vIconOnlyItem
anchors.centerIn: parent
visible: !verticalGroupItem.modelData.composite
name: {
@@ -515,7 +526,7 @@ BasePill {
case "vpn":
return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText;
case "bluetooth":
return BluetoothService.connected ? Theme.primary : Theme.surfaceText;
return (BluetoothService.connected || BluetoothService.connecting) ? Theme.primary : Theme.surfaceText;
case "battery":
return root.getBatteryIconColor();
case "printer":
@@ -524,6 +535,11 @@ BasePill {
return Theme.widgetIconColor;
}
}
DankBlink {
target: vIconOnlyItem
running: root.getIconBlinking(verticalGroupItem.modelData.id)
}
}
DankIcon {
@@ -687,7 +703,7 @@ BasePill {
case "vpn":
return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText;
case "bluetooth":
return BluetoothService.connected ? Theme.primary : Theme.surfaceText;
return (BluetoothService.connected || BluetoothService.connecting) ? Theme.primary : Theme.surfaceText;
case "battery":
return root.getBatteryIconColor();
case "printer":
@@ -696,6 +712,11 @@ BasePill {
return Theme.widgetIconColor;
}
}
DankBlink {
target: iconOnlyItem
running: root.getIconBlinking(horizontalGroupItem.modelData.id)
}
}
Rectangle {
@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
@@ -14,9 +15,20 @@ BasePill {
property var widgetData: null
property bool compactMode: widgetData?.focusedWindowCompactMode !== undefined ? widgetData.focusedWindowCompactMode : SettingsData.focusedWindowCompactMode
property int availableWidth: 400
readonly property int maxNormalWidth: 456
readonly property int maxCompactWidth: 288
readonly property int maxWidth: {
const size = widgetData?.focusedWindowSize !== undefined ? widgetData.focusedWindowSize : SettingsData.focusedWindowSize;
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 var activeDesktopEntry: null
property bool isHovered: mouseArea.containsMouse
@@ -171,8 +183,7 @@ BasePill {
return 0;
if (root.isVerticalOrientation)
return root.widgetThickness - root.horizontalPadding * 2;
const baseWidth = contentRow.implicitWidth;
return compactMode ? Math.min(baseWidth, maxCompactWidth - root.horizontalPadding * 2) : Math.min(baseWidth, maxNormalWidth - root.horizontalPadding * 2);
return contentRow.implicitWidth;
}
implicitHeight: root.widgetThickness - root.horizontalPadding * 2
clip: false
@@ -222,7 +233,7 @@ BasePill {
color: Theme.widgetTextColor
}
Row {
RowLayout {
id: contentRow
anchors.centerIn: parent
spacing: Theme.spacingS
@@ -231,24 +242,23 @@ BasePill {
StyledText {
id: appText
text: {
if (!activeWindow || !activeWindow.appId)
if (compactMode || !activeWindow || !activeWindow.appId)
return "";
return Paths.getAppName(activeWindow.appId, activeDesktopEntry);
}
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
maximumLineCount: 1
width: Math.min(implicitWidth, compactMode ? 80 : 180)
visible: !compactMode && text.length > 0
Layout.maximumWidth: compactMode ? 80 : 180
visible: text.length > 0
}
StyledText {
text: "•"
id: appSeparator
text: compactMode ? "" : "•"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.outlineButton
anchors.verticalCenter: parent.verticalCenter
visible: !compactMode && appText.text && titleText.text
}
@@ -276,10 +286,9 @@ BasePill {
}
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
maximumLineCount: 1
width: Math.min(implicitWidth, compactMode ? 280 : 250)
Layout.maximumWidth: maxWidth - appText.implicitWidth - appSeparator.implicitWidth
visible: text.length > 0
}
}
@@ -11,13 +11,14 @@ BasePill {
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 targetScreenName: parentScreen?.name || focusedScreenName
function resolveNotepadInstance() {
if (typeof notepadSlideoutVariants === "undefined" || !notepadSlideoutVariants || !notepadSlideoutVariants.instances) {
return null;
}
const targetScreen = focusedScreenName;
const targetScreen = targetScreenName;
if (targetScreen) {
for (var i = 0; i < notepadSlideoutVariants.instances.length; i++) {
var slideout = notepadSlideoutVariants.instances[i];
@@ -34,6 +35,12 @@ BasePill {
readonly property bool isActive: notepadInstance?.isVisible ?? false
property bool isAutoHideBar: false
function prepareNotepadInstance(instance) {
if (instance)
instance.triggerUsesOverlayLayer = root.barUsesOverlayLayer;
return instance;
}
readonly property real minTooltipY: {
if (!parentScreen || !(axis?.isVertical ?? false)) {
return 0;
@@ -68,8 +75,9 @@ BasePill {
function openTabByIndex(tabIndex) {
if (tabIndex < 0)
return;
if (root.notepadInstance && typeof root.notepadInstance.show === "function") {
root.notepadInstance.show();
const instance = prepareNotepadInstance(root.notepadInstance);
if (instance && typeof instance.show === "function") {
instance.show();
}
Qt.callLater(() => {
NotepadStorageService.switchToTab(tabIndex);
@@ -77,8 +85,9 @@ BasePill {
}
function openNewNote() {
if (root.notepadInstance && typeof root.notepadInstance.show === "function") {
root.notepadInstance.show();
const instance = prepareNotepadInstance(root.notepadInstance);
if (instance && typeof instance.show === "function") {
instance.show();
}
Qt.callLater(() => {
NotepadStorageService.createNewTab();
@@ -138,7 +147,7 @@ BasePill {
openContextMenu();
return;
}
const inst = root.notepadInstance;
const inst = prepareNotepadInstance(root.notepadInstance);
if (inst) {
inst.toggle();
}
@@ -978,7 +978,7 @@ BasePill {
visible: root.useOverflowPopup && root.menuOpen
screen: root.parentScreen
WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.layer: root.barUsesOverlayLayer ? WlrLayershell.Overlay : WlrLayershell.Top
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: {
if (!root.menuOpen)
@@ -1446,7 +1446,7 @@ BasePill {
WlrLayershell.namespace: "dms:tray-menu-window"
visible: menuRoot.showMenu && (menuRoot.trayItem?.hasMenu ?? false)
screen: menuRoot.parentScreen
WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.layer: root.barUsesOverlayLayer ? WlrLayershell.Overlay : WlrLayershell.Top
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: {
if (!menuRoot.showMenu)
+36 -106
View File
@@ -20,16 +20,16 @@ Variants {
WindowBlur {
targetWindow: dock
blurEnabled: dock.effectiveBlurEnabled && !SettingsData.connectedFrameModeActive
blurEnabled: dock.effectiveBlurEnabled && !dock.usesConnectedFrameChrome
blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x
blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y
blurWidth: dock.hasApps && dock.reveal ? dockBackground.width : 0
blurHeight: dock.hasApps && dock.reveal ? dockBackground.height : 0
blurRadius: Theme.isConnectedEffect ? Theme.connectedCornerRadius : dock.surfaceRadius
blurRadius: dock.usesConnectedFrameChrome ? Theme.connectedCornerRadius : dock.surfaceRadius
}
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
@@ -50,16 +50,16 @@ Variants {
readonly property bool connectedBarActiveOnEdge: dockGeometry.connectedBarActiveOnEdge
readonly property real connectedJoinInset: dockGeometry.connectedJoinInset
readonly property real dockFrameInset: dockGeometry.frameInset
readonly property real surfaceRadius: Theme.connectedSurfaceRadius
readonly property color surfaceColor: Theme.isConnectedEffect ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency)
readonly property color surfaceBorderColor: Theme.isConnectedEffect ? "transparent" : BlurService.borderColor
readonly property real surfaceBorderWidth: Theme.isConnectedEffect ? 0 : BlurService.borderWidth
readonly property real surfaceTopLeftRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius
readonly property real surfaceTopRightRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius
readonly property real surfaceBottomLeftRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius
readonly property real surfaceBottomRightRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius
readonly property real horizontalConnectorExtent: Theme.isConnectedEffect && !isVertical ? Theme.connectedCornerRadius : 0
readonly property real verticalConnectorExtent: Theme.isConnectedEffect && isVertical ? Theme.connectedCornerRadius : 0
readonly property real surfaceRadius: usesConnectedFrameChrome ? Theme.connectedSurfaceRadius : Theme.cornerRadius
readonly property color surfaceColor: usesConnectedFrameChrome ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency)
readonly property color surfaceBorderColor: usesConnectedFrameChrome ? "transparent" : BlurService.borderColor
readonly property real surfaceBorderWidth: usesConnectedFrameChrome ? 0 : BlurService.borderWidth
readonly property real surfaceTopLeftRadius: usesConnectedFrameChrome && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius
readonly property real surfaceTopRightRadius: usesConnectedFrameChrome && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius
readonly property real surfaceBottomLeftRadius: usesConnectedFrameChrome && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius
readonly property real surfaceBottomRightRadius: usesConnectedFrameChrome && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius
readonly property real horizontalConnectorExtent: usesConnectedFrameChrome && !isVertical ? Theme.connectedCornerRadius : 0
readonly property real verticalConnectorExtent: usesConnectedFrameChrome && isVertical ? Theme.connectedCornerRadius : 0
readonly property int hasApps: dockApps.implicitWidth > 0 || dockApps.implicitHeight > 0
@@ -149,7 +149,6 @@ Variants {
edge: dock.connectedBarSide
dockVisible: dock.visible
autoHide: dock.autoHide
hasFullscreenToplevel: dock.hasFullscreenToplevel
iconSize: dock.widgetHeight
spacing: SettingsData.dockSpacing
borderThickness: dock.borderThickness
@@ -176,25 +175,13 @@ Variants {
}
readonly property string _dockScreenName: dock.modelData ? dock.modelData.name : (dock.screen ? dock.screen.name : "")
readonly property bool hasFullscreenToplevel: {
if (!SettingsData.dockHideOnFullscreen)
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);
}
readonly property bool usesConnectedFrameChrome: CompositorService.usesConnectedFrameChromeForScreen(dock._dockScreenName)
readonly property bool usesOverlayLayer: CompositorService.framePeerSurfacesUseOverlayForScreen(dock._dockScreenName) || SettingsData.dockUseOverlayLayer
function _syncDockChromeState() {
if (!dock._dockScreenName)
return;
if (!SettingsData.connectedFrameModeActive) {
if (!dock.usesConnectedFrameChrome) {
ConnectedModeState.clearDockState(dock._dockScreenName);
return;
}
@@ -212,19 +199,19 @@ Variants {
}
function _syncDockSlide() {
if (!dock._dockScreenName || !SettingsData.connectedFrameModeActive)
if (!dock._dockScreenName || !dock.usesConnectedFrameChrome)
return;
ConnectedModeState.setDockSlide(dock._dockScreenName, dockSlide.x, dockSlide.y);
}
DeferredAction {
id: dockSlideSync
enabled: SettingsData.connectedFrameModeActive
enabled: dock.usesConnectedFrameChrome
onTriggered: dock._syncDockSlide()
}
function _queueSlideSync() {
if (!SettingsData.connectedFrameModeActive)
if (!dock.usesConnectedFrameChrome)
return;
dockSlideSync.schedule();
}
@@ -304,65 +291,10 @@ Variants {
return false;
}
// Hyprland implementation
// Hyprland implementation (current workspace + visible special workspaces)
Hyprland.focusedWorkspace;
const filtered = CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName);
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;
Hyprland.toplevels;
return CompositorService.hyprlandDockOverlapForSmartAutoHide(screenName, SettingsData.dockPosition, dockThickness, screenWidth, screenHeight);
}
Timer {
@@ -383,9 +315,6 @@ Variants {
if (_modalRetractActive)
return false;
if (dock.hasFullscreenToplevel)
return false;
if (CompositorService.isNiri && NiriService.inOverview && SettingsData.dockOpenOnOverview) {
return true;
}
@@ -421,7 +350,7 @@ Variants {
onVisibleChanged: dock._syncDockChromeState()
onHasAppsChanged: dock._syncDockChromeState()
onConnectedBarSideChanged: dock._syncDockChromeState()
onHasFullscreenToplevelChanged: dock._syncDockChromeState()
onUsesConnectedFrameChromeChanged: dock._syncDockChromeState()
Connections {
target: SettingsData
@@ -680,7 +609,7 @@ Variants {
return 0;
if (dock.reveal)
return 0;
if (Theme.isConnectedEffect) {
if (dock.usesConnectedFrameChrome) {
const retractDist = dockBackground.width + SettingsData.dockSpacing + 10;
return SettingsData.dockPosition === SettingsData.Position.Right ? retractDist : -retractDist;
}
@@ -696,7 +625,7 @@ Variants {
return 0;
if (dock.reveal)
return 0;
if (Theme.isConnectedEffect) {
if (dock.usesConnectedFrameChrome) {
const retractDist = dockBackground.height + SettingsData.dockSpacing + 10;
return SettingsData.dockPosition === SettingsData.Position.Bottom ? retractDist : -retractDist;
}
@@ -711,9 +640,9 @@ Variants {
Behavior on x {
NumberAnimation {
id: slideXAnimation
duration: Theme.isConnectedEffect ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration
easing.type: Theme.isConnectedEffect ? Easing.BezierSpline : Easing.OutCubic
easing.bezierCurve: Theme.isConnectedEffect ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : []
duration: dock.usesConnectedFrameChrome ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration
easing.type: dock.usesConnectedFrameChrome ? Easing.BezierSpline : Easing.OutCubic
easing.bezierCurve: dock.usesConnectedFrameChrome ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : []
onRunningChanged: if (!running)
dock._syncDockChromeState()
}
@@ -722,9 +651,9 @@ Variants {
Behavior on y {
NumberAnimation {
id: slideYAnimation
duration: Theme.isConnectedEffect ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration
easing.type: Theme.isConnectedEffect ? Easing.BezierSpline : Easing.OutCubic
easing.bezierCurve: Theme.isConnectedEffect ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : []
duration: dock.usesConnectedFrameChrome ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration
easing.type: dock.usesConnectedFrameChrome ? Easing.BezierSpline : Easing.OutCubic
easing.bezierCurve: dock.usesConnectedFrameChrome ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : []
onRunningChanged: if (!running)
dock._syncDockChromeState()
}
@@ -756,12 +685,12 @@ Variants {
height: implicitHeight
// Avoid an offscreen texture seam where the connected dock meets the frame.
layer.enabled: !Theme.isConnectedEffect
layer.enabled: !usesConnectedFrameChrome
clip: false
Rectangle {
anchors.fill: parent
visible: !SettingsData.connectedFrameModeActive && !(Theme.isConnectedEffect && dock.reveal)
visible: !usesConnectedFrameChrome && (!SettingsData.connectedFrameModeActive || dock.reveal)
color: dock.surfaceColor
topLeftRadius: dock.surfaceTopLeftRadius
topRightRadius: dock.surfaceTopRightRadius
@@ -771,7 +700,7 @@ Variants {
Rectangle {
anchors.fill: parent
visible: !SettingsData.connectedFrameModeActive && !(Theme.isConnectedEffect && dock.reveal)
visible: !usesConnectedFrameChrome && (!SettingsData.connectedFrameModeActive || dock.reveal)
color: "transparent"
topLeftRadius: dock.surfaceTopLeftRadius
topRightRadius: dock.surfaceTopRightRadius
@@ -807,7 +736,7 @@ Variants {
y: dockBackground.y - borderThickness
width: dockBackground.width + borderThickness * 2
height: dockBackground.height + borderThickness * 2
visible: SettingsData.dockBorderEnabled && dock.hasApps && !Theme.isConnectedEffect
visible: SettingsData.dockBorderEnabled && dock.hasApps && !usesConnectedFrameChrome
preferredRendererType: Shape.CurveRenderer
readonly property real borderThickness: Math.max(1, dock.borderThickness)
@@ -883,6 +812,7 @@ Variants {
isVertical: dock.isVertical
dockScreen: dock.screen
iconSize: dock.widgetHeight
usesOverlayLayer: dock.usesOverlayLayer
}
}
}
+1
View File
@@ -15,6 +15,7 @@ Item {
property bool isVertical: false
property var dockScreen: null
property real iconSize: 40
property bool usesOverlayLayer: false
property int draggedIndex: -1
property int dropTargetIndex: -1
property bool suppressShiftAnimation: false
+11 -11
View File
@@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Services
QtObject {
id: root
@@ -10,7 +11,6 @@ QtObject {
property string edge: "bottom"
property bool dockVisible: false
property bool autoHide: false
property bool hasFullscreenToplevel: false
property real iconSize: 40
property real spacing: 4
property real borderThickness: 0
@@ -23,14 +23,14 @@ QtObject {
return Math.round(value * dpr) / dpr;
}
readonly property bool frameExclusionActive: SettingsData.frameEnabled && !!screen && SettingsData.isScreenInPreferences(screen, SettingsData.frameScreenPreferences)
readonly property bool connectedMode: Theme.isConnectedEffect
readonly property bool connectedBarActiveOnEdge: connectedMode && !!screen && SettingsData.getActiveBarEdgesForScreen(screen).includes(edge)
readonly property bool frameExclusionActive: CompositorService.frameWindowVisibleForScreen(screen)
readonly property bool usesConnectedFrameChrome: CompositorService.usesConnectedFrameChromeForScreen(screen)
readonly property bool connectedBarActiveOnEdge: usesConnectedFrameChrome && !!screen && SettingsData.getActiveBarEdgesForScreen(screen).includes(edge)
readonly property real connectedJoinInset: {
if (connectedMode)
if (usesConnectedFrameChrome)
return connectedBarActiveOnEdge ? SettingsData.frameBarSize : SettingsData.frameThickness;
if (SettingsData.frameEnabled)
if (frameExclusionActive)
return SettingsData.frameEdgeInsetForSide(screen, edge);
return 0;
}
@@ -38,15 +38,15 @@ QtObject {
readonly property real frameInset: {
if (!frameExclusionActive)
return 0;
if (connectedMode)
if (usesConnectedFrameChrome)
return connectedJoinInset;
return SettingsData.frameThickness;
}
readonly property real effectiveMargin: connectedMode ? 0 : margin
readonly property real visualOffset: connectedMode ? 0 : offset
readonly property real effectiveMargin: usesConnectedFrameChrome ? 0 : margin
readonly property real visualOffset: usesConnectedFrameChrome ? 0 : 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 bodyThickness: iconSize + spacing * 2 + borderThickness * 2
@@ -57,5 +57,5 @@ QtObject {
// Frame/bar edge exclusions already reserve the edge itself, so the dock
// reservation covers only the dock body and user offset beyond that edge.
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)
return;
PopoutService.toggleDankLauncherV2();
PopoutService.toggleDankLauncherV2(dockApps?.usesOverlayLayer ?? false);
}
onPositionChanged: mouse => {
if (longPressing && !dragging) {
+2 -1
View File
@@ -4,6 +4,7 @@ import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
Scope {
id: root
@@ -18,7 +19,7 @@ Scope {
// One thin invisible PanelWindow per edge.
// Skips any edge where a bar already provides its own exclusiveZone.
readonly property bool screenEnabled: SettingsData.frameEnabled && SettingsData.isScreenInPreferences(root.screen, SettingsData.frameScreenPreferences)
readonly property bool screenEnabled: CompositorService.frameWindowVisibleForScreen(root.screen)
Loader {
active: root.screenEnabled && !root.barEdges.includes("top")
+4 -3
View File
@@ -17,8 +17,9 @@ PanelWindow {
required property var targetScreen
screen: targetScreen
visible: _frameActive
updatesEnabled: _frameActive
readonly property bool _frameVisible: CompositorService.frameWindowVisibleForScreen(win.targetScreen)
visible: win._frameVisible
updatesEnabled: win._frameVisible
WlrLayershell.namespace: "dms:frame"
WlrLayershell.layer: WlrLayer.Top
@@ -52,7 +53,7 @@ PanelWindow {
readonly property var _notifState: ConnectedModeState.notificationStates[win._screenName] || ConnectedModeState.emptyNotificationState
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: {
const edges = win.barEdges;
if (edges.includes("top"))
+2 -1
View File
@@ -97,7 +97,8 @@ sudo rpm -ivh x86_64/dms-greeter-*.rpm
```
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
- Configures greetd with auto-detected compositor
- Applies SELinux contexts
+13
View File
@@ -36,6 +36,8 @@ Rectangle {
signal closed
signal switchUserRequested
function updateVisibleActions() {
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;
@@ -128,6 +130,12 @@ Rectangle {
"label": I18n.tr("Hibernate"),
"key": "H"
};
case "switchuser":
return {
"icon": "switch_account",
"label": I18n.tr("Switch User"),
"key": "U"
};
default:
return {
"icon": "help",
@@ -183,6 +191,11 @@ Rectangle {
function executeAction(action) {
if (!action)
return;
if (action === "switchuser") {
hide();
switchUserRequested();
return;
}
if (typeof SessionService === "undefined")
return;
hide();
+49 -6
View File
@@ -9,6 +9,7 @@ import Quickshell.Hyprland
import Quickshell.Io
import Quickshell.Services.Mpris
import qs.Common
import qs.Modals
import qs.Services
import qs.Widgets
@@ -73,6 +74,10 @@ Item {
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: {
WeatherService.addRef();
UserInfoService.getUserInfo();
@@ -761,6 +766,9 @@ Item {
if (enterButton.visible) {
margin += enterButton.width + 2;
}
if (securityKeyButton.visible) {
margin += securityKeyButton.width;
}
if (virtualKeyboardButton.visible) {
margin += virtualKeyboardButton.width;
}
@@ -854,7 +862,7 @@ Item {
anchors.left: lockIconContainer.right
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.verticalCenter: parent.verticalCenter
text: {
@@ -896,7 +904,7 @@ Item {
StyledText {
anchors.left: lockIconContainer.right
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.verticalCenter: parent.verticalCenter
text: {
@@ -926,7 +934,7 @@ Item {
DankActionButton {
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.verticalCenter: parent.verticalCenter
iconName: parent.showPassword ? "visibility_off" : "visibility"
@@ -936,10 +944,26 @@ Item {
onClicked: parent.showPassword = !parent.showPassword
}
DankActionButton {
id: virtualKeyboardButton
id: securityKeyButton
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
iconName: "keyboard"
buttonSize: 32
@@ -1438,6 +1462,7 @@ Item {
}
DankIcon {
id: lockNetworkIcon
name: {
if (NetworkService.wifiToggling)
return "sync";
@@ -1451,9 +1476,14 @@ Item {
}
}
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
visible: NetworkService.networkAvailable
DankBlink {
target: lockNetworkIcon
running: NetworkService.isWifiConnecting
}
}
DankIcon {
@@ -1465,11 +1495,17 @@ Item {
}
DankIcon {
id: lockBluetoothIcon
name: "bluetooth"
size: Theme.iconSize - 2
color: "white"
anchors.verticalCenter: parent.verticalCenter
visible: BluetoothService.available && BluetoothService.enabled
DankBlink {
target: lockBluetoothIcon
running: BluetoothService.connecting
}
}
DankIcon {
@@ -1693,5 +1729,12 @@ Item {
Qt.callLater(() => passwordField.forceActiveFocus());
}
}
onSwitchUserRequested: {
switchUserPicker.showFromLockScreen();
}
}
SwitchUserModal {
id: switchUserPicker
}
}
+31 -5
View File
@@ -20,6 +20,7 @@ Scope {
property string fprintState
property string u2fState
property bool u2fPending: false
property string u2fPendingMode
property string buffer
signal flashMsg
@@ -35,6 +36,7 @@ Scope {
passwdActiveTimeout.running = false;
unlockRequestTimeout.running = false;
root.u2fPending = false;
root.u2fPendingMode = "";
root.u2fState = "";
root.unlockInProgress = false;
}
@@ -58,6 +60,7 @@ Scope {
u2fErrorRetry.running = false;
u2fPendingTimeout.running = false;
root.u2fPending = false;
root.u2fPendingMode = "";
root.u2fState = "";
unlockRequestTimeout.restart();
unlockRequested();
@@ -79,6 +82,7 @@ Scope {
u2fErrorRetry.running = false;
u2fPendingTimeout.running = false;
root.u2fPending = false;
root.u2fPendingMode = "";
root.u2fState = "";
fprint.checkAvail();
}
@@ -142,6 +146,7 @@ Scope {
unlockRequestTimeout.running = false;
root.unlockInProgress = false;
root.u2fPending = false;
root.u2fPendingMode = "";
root.u2fState = "";
u2fPendingTimeout.running = false;
u2f.abort();
@@ -243,9 +248,8 @@ Scope {
return;
}
if (SettingsData.u2fMode === "or") {
start();
}
if (SettingsData.u2fMode === "or")
abort();
}
function startForSecondFactor(): void {
@@ -255,6 +259,18 @@ Scope {
}
abort();
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 = "";
u2fPendingTimeout.restart();
start();
@@ -281,9 +297,19 @@ Scope {
abort();
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") {
// AND mode: device was found but auth failed back to password
root.u2fPending = false;
root.u2fPendingMode = "";
root.u2fState = "";
fprint.checkAvail();
} else {
@@ -292,9 +318,7 @@ Scope {
u2fErrorRetry.restart();
}
} else {
// OR mode: prompt to insert key, silently retry
root.u2fState = "insert";
u2fErrorRetry.restart();
}
}
}
@@ -367,6 +391,7 @@ Scope {
root.fprintState = "";
root.u2fState = "";
root.u2fPending = false;
root.u2fPendingMode = "";
root.lockMessage = "";
root.resetAuthFlows();
fprint.checkAvail();
@@ -399,6 +424,7 @@ Scope {
u2fPendingTimeout.running = false;
unlockRequestTimeout.running = false;
root.u2fPending = false;
root.u2fPendingMode = "";
root.u2fState = "";
u2f.checkAvail();
}
@@ -182,26 +182,30 @@ Rectangle {
Row {
width: parent.width
spacing: Theme.spacingXS
readonly property real reservedTrailingWidth: historySeparator.implicitWidth + Math.max(historyTimeText.implicitWidth, 72) + spacing
StyledText {
id: historyTitleText
width: Math.min(implicitWidth, Math.max(0, parent.width - parent.reservedTrailingWidth))
text: {
let title = historyItem.summary || "";
const appName = historyItem.appName || "";
const prefix = appName + " • ";
if (appName && title.toLowerCase().startsWith(prefix.toLowerCase())) {
title = title.substring(prefix.length);
Item {
width: Math.max(0, parent.width - historySeparator.implicitWidth - Math.max(historyTimeText.implicitWidth, 72) - parent.spacing * 2)
height: historyTitleText.implicitHeight
visible: historyTitleText.text.length > 0
StyledText {
id: historyTitleText
anchors.fill: parent
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 {
id: historySeparator
@@ -10,7 +10,7 @@ import qs.Widgets
PanelWindow {
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: {
const pos = SettingsData.notificationPopupPosition;
if (pos === -1)
@@ -370,9 +370,9 @@ PanelWindow {
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) {
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));
return _frameEdgeInset("top") + cornerClear + screenY;
}
if (frameOnlyNoConnected)
if (frameVisibleWithoutConnectedChrome)
return _frameGapMargin("top") + screenY;
const barInfo = getBarInfo();
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));
return _frameEdgeInset("bottom") + cornerClear + screenY;
}
if (frameOnlyNoConnected)
if (frameVisibleWithoutConnectedChrome)
return _frameGapMargin("bottom") + screenY;
const barInfo = getBarInfo();
const base = barInfo.bottomBar > 0 ? barInfo.bottomBar : Theme.popupDistance;
@@ -422,7 +422,7 @@ PanelWindow {
if (connectedFrameMode)
return _frameEdgeInset("left");
if (frameOnlyNoConnected)
if (frameVisibleWithoutConnectedChrome)
return _frameGapMargin("left");
const barInfo = getBarInfo();
return barInfo.leftBar > 0 ? barInfo.leftBar : Theme.popupDistance;
@@ -439,7 +439,7 @@ PanelWindow {
if (connectedFrameMode)
return _frameEdgeInset("right");
if (frameOnlyNoConnected)
if (frameVisibleWithoutConnectedChrome)
return _frameGapMargin("right");
const barInfo = getBarInfo();
return barInfo.rightBar > 0 ? barInfo.rightBar : Theme.popupDistance;
@@ -10,7 +10,7 @@ QtObject {
property var modelData
property int topMargin: 0
readonly property bool compactMode: SettingsData.notificationCompactMode
readonly property bool notificationConnectedMode: SettingsData.frameEnabled && Theme.isConnectedEffect && SettingsData.isScreenInPreferences(manager.modelData, SettingsData.frameScreenPreferences)
readonly property bool notificationConnectedMode: CompositorService.usesConnectedFrameChromeForScreen(manager.modelData)
readonly property bool closeGapNotifications: notificationConnectedMode && SettingsData.frameCloseGaps
readonly property string notifBarSide: {
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 Quickshell
import qs.Common
import qs.Services
import qs.Widgets
@@ -38,6 +39,18 @@ Item {
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 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 rightClicked(real rootX, real rootY)
+39 -4
View File
@@ -137,7 +137,7 @@ Item {
popupGapsAuto: defaultBar.popupGapsAuto ?? true,
popupGapsManual: defaultBar.popupGapsManual ?? 4,
maximizeDetection: defaultBar.maximizeDetection ?? true,
fullscreenDetection: defaultBar.fullscreenDetection ?? true,
useOverlayLayer: defaultBar.useOverlayLayer ?? false,
scrollEnabled: defaultBar.scrollEnabled ?? true,
scrollXBehavior: defaultBar.scrollXBehavior ?? "column",
scrollYBehavior: defaultBar.scrollYBehavior ?? "workspace",
@@ -597,6 +597,7 @@ Item {
SettingsToggleRow {
text: I18n.tr("Auto-hide")
description: I18n.tr("Automatically hide the bar when the pointer moves away")
checked: selectedBarConfig?.autoHide ?? false
onToggled: toggled => {
SettingsData.updateBarConfig(selectedBarId, {
@@ -623,6 +624,7 @@ Item {
id: hideDelaySlider
width: parent.width - parent.parent.leftPadding
text: I18n.tr("Hide Delay")
description: I18n.tr("Time to wait before hiding after the pointer leaves")
value: selectedBarConfig?.autoHideDelay ?? 250
minimum: 0
maximum: 2000
@@ -645,6 +647,7 @@ Item {
SettingsToggleRow {
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")
description: I18n.tr("Hide the bar when the pointer leaves even if a popout is still open")
checked: selectedBarConfig?.autoHideStrict ?? false
onToggled: toggled => {
SettingsData.updateBarConfig(selectedBarId, {
@@ -658,6 +661,7 @@ Item {
width: parent.width - parent.leftPadding
visible: CompositorService.isNiri || CompositorService.isHyprland
text: I18n.tr("Hide When Windows Open")
description: I18n.tr("Show the bar only when no windows are open")
checked: selectedBarConfig?.showOnWindowsOpen ?? false
onToggled: toggled => {
SettingsData.updateBarConfig(selectedBarId, {
@@ -676,6 +680,7 @@ Item {
SettingsToggleRow {
text: I18n.tr("Manual Show/Hide")
description: I18n.tr("Toggle bar visibility manually via IPC")
checked: selectedBarConfig?.visible ?? true
onToggled: toggled => {
SettingsData.updateBarConfig(selectedBarId, {
@@ -694,6 +699,7 @@ Item {
SettingsToggleRow {
text: I18n.tr("Click Through")
description: I18n.tr("Mouse clicks pass through the bar to windows behind it")
checked: selectedBarConfig?.clickThrough ?? false
onToggled: toggled => SettingsData.updateBarConfig(selectedBarId, {
clickThrough: toggled
@@ -713,6 +719,7 @@ Item {
enabled: !SettingsData.frameEnabled
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
text: I18n.tr("Show on Overview")
description: I18n.tr("Show the bar when niri overview is active")
checked: selectedBarConfig?.openOnOverview ?? false
onToggled: toggled => {
SettingsData.updateBarConfig(selectedBarId, {
@@ -729,11 +736,14 @@ Item {
}
SettingsToggleRow {
text: I18n.tr("Hide When Fullscreen", "bar visibility toggle: hide the bar when a window is fullscreen")
checked: selectedBarConfig?.fullscreenDetection ?? true
settingKey: "barUseOverlayLayer"
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 => {
SettingsData.updateBarConfig(selectedBarId, {
fullscreenDetection: toggled
useOverlayLayer: toggled
});
notifyHorizontalBarChange();
}
@@ -756,6 +766,7 @@ Item {
SettingsSliderRow {
id: edgeSpacingSlider
text: I18n.tr("Edge Spacing")
description: I18n.tr("Space between the bar and screen edges")
value: selectedBarConfig?.spacing ?? 4
minimum: 0
maximum: 32
@@ -777,6 +788,7 @@ Item {
SettingsSliderRow {
id: exclusiveZoneSlider
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
minimum: -50
maximum: 50
@@ -798,6 +810,7 @@ Item {
SettingsSliderRow {
id: sizeSlider
text: I18n.tr("Size")
description: I18n.tr("Adjust the bar height via inner padding")
value: selectedBarConfig?.innerPadding ?? 4
minimum: -8
maximum: 24
@@ -819,6 +832,7 @@ Item {
SettingsSliderRow {
id: widgetPaddingSlider
text: I18n.tr("Padding")
description: I18n.tr("Inner padding applied to each widget")
value: selectedBarConfig?.widgetPadding ?? 8
minimum: 0
maximum: 32
@@ -849,6 +863,7 @@ Item {
SettingsToggleRow {
text: I18n.tr("Auto Popup Gaps")
description: I18n.tr("Automatically calculate popup gap based on bar spacing")
checked: selectedBarConfig?.popupGapsAuto ?? true
onToggled: checked => {
SettingsData.updateBarConfig(selectedBarId, {
@@ -874,6 +889,7 @@ Item {
id: popupGapsManualSlider
width: parent.width - parent.parent.leftPadding
text: I18n.tr("Manual Gap Size")
description: I18n.tr("Override the popup gap size when auto is disabled")
value: selectedBarConfig?.popupGapsManual ?? 4
minimum: 0
maximum: 50
@@ -904,6 +920,7 @@ Item {
id: barTransparencySlider
visible: !SettingsData.frameEnabled
text: I18n.tr("Bar Transparency")
description: I18n.tr("Opacity of the bar background")
value: (selectedBarConfig?.transparency ?? 1.0) * 100
minimum: 0
maximum: 100
@@ -926,6 +943,7 @@ Item {
SettingsSliderRow {
id: widgetTransparencySlider
text: I18n.tr("Widget Transparency")
description: I18n.tr("Opacity of widget backgrounds")
value: (selectedBarConfig?.widgetTransparency ?? 1.0) * 100
minimum: 0
maximum: 100
@@ -1020,6 +1038,7 @@ Item {
SettingsToggleRow {
text: I18n.tr("Square Corners")
description: I18n.tr("Remove corner rounding from the bar")
visible: !SettingsData.frameEnabled
checked: selectedBarConfig?.squareCorners ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
@@ -1029,6 +1048,7 @@ Item {
SettingsToggleRow {
text: I18n.tr("No Background")
description: I18n.tr("Make the bar background fully transparent")
visible: !SettingsData.frameEnabled
checked: selectedBarConfig?.noBackground ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
@@ -1038,6 +1058,7 @@ Item {
SettingsToggleRow {
text: I18n.tr("Maximize Widget Icons")
description: I18n.tr("Stretch widget icons to fill the available bar height")
checked: selectedBarConfig?.maximizeWidgetIcons ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
maximizeWidgetIcons: checked
@@ -1046,6 +1067,7 @@ Item {
SettingsToggleRow {
text: I18n.tr("Maximize Widget Text")
description: I18n.tr("Stretch widget text to fill the available bar height")
checked: selectedBarConfig?.maximizeWidgetText ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
maximizeWidgetText: checked
@@ -1054,6 +1076,7 @@ Item {
SettingsToggleRow {
text: I18n.tr("Remove Widget Padding")
description: I18n.tr("Remove inner padding from all widgets")
checked: selectedBarConfig?.removeWidgetPadding ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
removeWidgetPadding: checked
@@ -1069,6 +1092,7 @@ Item {
SettingsToggleRow {
text: I18n.tr("Goth Corners")
description: I18n.tr("Apply inverse concave corner cutouts to the bar")
visible: !SettingsData.frameEnabled
checked: selectedBarConfig?.gothCornersEnabled ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
@@ -1078,6 +1102,7 @@ Item {
SettingsToggleRow {
text: I18n.tr("Corner Radius Override")
description: I18n.tr("Use a custom radius for goth corner cutouts")
checked: selectedBarConfig?.gothCornerRadiusOverride ?? false
visible: selectedBarConfig?.gothCornersEnabled ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
@@ -1236,6 +1261,7 @@ Item {
SettingsButtonGroupRow {
text: I18n.tr("Color")
description: I18n.tr("Theme color used for the border")
model: ["Surface", "Secondary", "Primary"]
currentIndex: {
switch (selectedBarConfig?.borderColor || "surfaceText") {
@@ -1273,6 +1299,7 @@ Item {
SettingsSliderRow {
id: borderOpacitySlider
text: I18n.tr("Opacity")
description: I18n.tr("Transparency of the border")
value: (selectedBarConfig?.borderOpacity ?? 1.0) * 100
minimum: 0
maximum: 100
@@ -1295,6 +1322,7 @@ Item {
SettingsSliderRow {
id: borderThicknessSlider
text: I18n.tr("Thickness")
description: I18n.tr("Width of the border in pixels")
value: selectedBarConfig?.borderThickness ?? 1
minimum: 1
maximum: 10
@@ -1326,6 +1354,7 @@ Item {
SettingsButtonGroupRow {
text: I18n.tr("Color")
description: I18n.tr("Theme color used for the widget outline")
model: ["Surface", "Secondary", "Primary"]
currentIndex: {
switch (selectedBarConfig?.widgetOutlineColor || "primary") {
@@ -1363,6 +1392,7 @@ Item {
SettingsSliderRow {
id: widgetOutlineOpacitySlider
text: I18n.tr("Opacity")
description: I18n.tr("Transparency of the widget outline")
value: (selectedBarConfig?.widgetOutlineOpacity ?? 1.0) * 100
minimum: 0
maximum: 100
@@ -1385,6 +1415,7 @@ Item {
SettingsSliderRow {
id: widgetOutlineThicknessSlider
text: I18n.tr("Thickness")
description: I18n.tr("Width of the widget outline in pixels")
value: selectedBarConfig?.widgetOutlineThickness ?? 1
minimum: 1
maximum: 10
@@ -1455,6 +1486,7 @@ Item {
SettingsSliderRow {
visible: shadowCard.shadowActive
text: I18n.tr("Intensity", "shadow intensity slider")
description: I18n.tr("Shadow blur radius in pixels")
minimum: 0
maximum: 100
unit: "px"
@@ -1468,6 +1500,7 @@ Item {
SettingsSliderRow {
visible: shadowCard.shadowActive
text: I18n.tr("Opacity")
description: I18n.tr("Transparency of the shadow layer")
minimum: 10
maximum: 100
unit: "%"
@@ -1655,6 +1688,7 @@ Item {
SettingsButtonGroupRow {
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")]
currentIndex: {
switch (selectedBarConfig?.scrollYBehavior || "workspace") {
@@ -1691,6 +1725,7 @@ Item {
SettingsButtonGroupRow {
text: I18n.tr("X Axis")
description: I18n.tr("Action performed when scrolling horizontally on the bar")
visible: CompositorService.isNiri
model: [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")]
currentIndex: {
+6 -6
View File
@@ -90,13 +90,13 @@ Item {
}
SettingsToggleRow {
settingKey: "dockHideOnFullscreen"
tags: ["dock", "fullscreen", "hide"]
text: I18n.tr("Hide When Fullscreen", "dock visibility toggle: hide the dock when a window is fullscreen")
description: I18n.tr("Hide the dock when a window is fullscreen", "dock visibility toggle description")
checked: SettingsData.dockHideOnFullscreen
settingKey: "dockUseOverlayLayer"
tags: ["dock", "fullscreen", "overlay", "layer"]
text: I18n.tr("Use Overlay Layer", "dock layer toggle: use Wayland overlay layer")
description: I18n.tr("Place the dock on the Wayland overlay layer")
checked: SettingsData.dockUseOverlayLayer
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)
}
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 {
settingKey: "frameLauncherEmergeSide"
tags: ["frame", "connected", "launcher", "modal", "emerge", "direction", "bottom", "top"]
+24 -2
View File
@@ -16,6 +16,7 @@ Item {
property var parentModal: null
property string selectedCategory: ""
property string searchQuery: ""
property string requestedSearchQuery: ""
property string expandedKey: ""
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: {
if (!visible)
return;
Qt.callLater(scrollToTop);
_ensureCurrentProvider();
Qt.callLater(() => {
_applyRequestedSearch();
scrollToTop();
});
}
DankFlickable {
+219 -6
View File
@@ -9,6 +9,37 @@ Item {
id: root
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 {
id: logoFileBrowser
@@ -35,20 +66,20 @@ Item {
SettingsCard {
width: parent.width
iconName: "search"
title: I18n.tr("Launcher Style")
title: I18n.tr("Default Launcher")
settingKey: "launcherStyle"
SettingsControlledByFrame {
visible: SettingsData.connectedFrameModeActive
parentModal: root.parentModal
settingLabel: I18n.tr("Launcher Style")
reason: I18n.tr("Managed by Frame Mode")
settingLabel: I18n.tr("Default Launcher")
reason: I18n.tr("Connected Frame Mode uses the connected launcher for default launcher shortcuts.")
}
StyledText {
width: parent.width
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
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
@@ -57,8 +88,8 @@ Item {
SettingsButtonGroupRow {
visible: !SettingsData.connectedFrameModeActive
settingKey: "launcherStyleSelector"
tags: ["launcher", "style", "spotlight", "full", "minimal"]
text: I18n.tr("Style")
tags: ["launcher", "style", "default", "spotlight", "full", "minimal"]
text: I18n.tr("Default Opens")
model: [I18n.tr("Full"), I18n.tr("Spotlight")]
currentIndex: SettingsData.launcherStyle === "spotlight" ? 1 : 0
onSelectionChanged: (index, selected) => {
@@ -67,6 +98,179 @@ Item {
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 {
@@ -917,6 +1121,15 @@ Item {
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 {
settingKey: "rememberLastQuery"
tags: ["launcher", "remember", "last", "search", "query"]
@@ -273,6 +273,17 @@ Item {
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 {
settingKey: "notificationPopupShadowEnabled"
tags: ["notification", "popup", "shadow", "radius", "rounded"]
@@ -455,6 +455,11 @@ Item {
label: I18n.tr("Show Restart DMS"),
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",
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,
"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++) {
if (widget[keys[i]] !== undefined)
result[keys[i]] = widget[keys[i]];
@@ -625,9 +625,6 @@ Item {
var newWidget = cloneWidgetData(widget);
switch (widgetId) {
case "music":
newWidget.mediaSize = value;
break;
case "clock":
newWidget.clockCompactMode = value;
break;
@@ -647,6 +644,29 @@ Item {
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) {
var widgets = [];
var widgetData = getWidgetsForSection(sectionId);
@@ -708,6 +728,8 @@ Item {
item.clockCompactMode = widget.clockCompactMode;
if (widget.focusedWindowCompactMode !== undefined)
item.focusedWindowCompactMode = widget.focusedWindowCompactMode;
if (widget.focusedWindowSize !== undefined)
item.focusedWindowSize = widget.focusedWindowSize;
if (widget.runningAppsCompactMode !== undefined)
item.runningAppsCompactMode = widget.runningAppsCompactMode;
if (widget.runningAppsGroupByApp !== undefined)
@@ -1014,6 +1036,9 @@ Item {
onCompactModeChanged: (widgetId, value) => {
widgetsTab.handleCompactModeChanged(sectionId, widgetId, value);
}
onWidgetSizeChanged: (widgetId, value) => {
widgetsTab.handleWidgetSizeChanged(sectionId, widgetId, value);
}
onOverflowSettingChanged: (sectionId, widgetIndex, settingName, value) => {
widgetsTab.handleOverflowSettingChanged(sectionId, widgetIndex, settingName, value);
}
@@ -1084,6 +1109,9 @@ Item {
onCompactModeChanged: (widgetId, value) => {
widgetsTab.handleCompactModeChanged(sectionId, widgetId, value);
}
onWidgetSizeChanged: (widgetId, value) => {
widgetsTab.handleWidgetSizeChanged(sectionId, widgetId, value);
}
onOverflowSettingChanged: (sectionId, widgetIndex, settingName, value) => {
widgetsTab.handleOverflowSettingChanged(sectionId, widgetIndex, settingName, value);
}
@@ -1154,6 +1182,9 @@ Item {
onCompactModeChanged: (widgetId, value) => {
widgetsTab.handleCompactModeChanged(sectionId, widgetId, value);
}
onWidgetSizeChanged: (widgetId, value) => {
widgetsTab.handleWidgetSizeChanged(sectionId, widgetId, value);
}
onOverflowSettingChanged: (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 spacerSizeChanged(string sectionId, int widgetIndex, int newSize)
signal compactModeChanged(string widgetId, var value)
signal widgetSizeChanged(string widgetId, var value)
signal gpuSelectionChanged(string sectionId, int widgetIndex, int selectedIndex)
signal diskMountSelectionChanged(string sectionId, int widgetIndex, string mountPath)
signal controlCenterSettingChanged(string sectionId, int widgetIndex, string settingName, bool value)
@@ -41,7 +42,7 @@ Column {
"id": widget.id,
"enabled": widget.enabled
};
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "focusedWindowCompactMode", "runningAppsCompactMode", "keyboardLayoutNameCompactMode", "runningAppsGroupByApp", "runningAppsCurrentWorkspace", "runningAppsCurrentMonitor", "showNetworkIcon", "showBluetoothIcon", "showAudioIcon", "showAudioPercent", "showVpnIcon", "showBrightnessIcon", "showBrightnessPercent", "showMicIcon", "showMicPercent", "showBatteryIcon", "showPrinterIcon", "showScreenSharingIcon", "controlCenterGroupOrder", "barMaxVisibleApps", "barMaxVisibleRunningApps", "barShowOverflowBadge", "trayUseInlineExpansion"];
var keys = ["size", "selectedGpuIndex", "pciId", "mountPath", "diskUsageMode", "minimumWidth", "showSwap", "showInGb", "mediaSize", "clockCompactMode", "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++) {
if (widget[keys[i]] !== undefined)
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 {
id: musicMenuButton
visible: modelData.id === "music"
@@ -458,19 +492,17 @@ Column {
Row {
spacing: Theme.spacingXS
visible: modelData.id === "clock" || modelData.id === "focusedWindow" || modelData.id === "keyboard_layout_name" || modelData.id === "appsDock" || modelData.id === "systemTray"
visible: modelData.id === "clock" || modelData.id === "keyboard_layout_name" || modelData.id === "appsDock" || modelData.id === "systemTray"
DankActionButton {
id: compactModeButton
buttonSize: 28
visible: modelData.id === "clock" || modelData.id === "focusedWindow" || modelData.id === "keyboard_layout_name"
visible: modelData.id === "clock" || modelData.id === "keyboard_layout_name"
iconName: {
const isCompact = (() => {
switch (modelData.id) {
case "clock":
return modelData.clockCompactMode !== undefined ? modelData.clockCompactMode : SettingsData.clockCompactMode;
case "focusedWindow":
return modelData.focusedWindowCompactMode !== undefined ? modelData.focusedWindowCompactMode : SettingsData.focusedWindowCompactMode;
case "keyboard_layout_name":
return modelData.keyboardLayoutNameCompactMode !== undefined ? modelData.keyboardLayoutNameCompactMode : SettingsData.keyboardLayoutNameCompactMode;
default:
@@ -485,8 +517,6 @@ Column {
switch (modelData.id) {
case "clock":
return modelData.clockCompactMode !== undefined ? modelData.clockCompactMode : SettingsData.clockCompactMode;
case "focusedWindow":
return modelData.focusedWindowCompactMode !== undefined ? modelData.focusedWindowCompactMode : SettingsData.focusedWindowCompactMode;
case "keyboard_layout_name":
return modelData.keyboardLayoutNameCompactMode !== undefined ? modelData.keyboardLayoutNameCompactMode : SettingsData.keyboardLayoutNameCompactMode;
default:
@@ -500,8 +530,6 @@ Column {
switch (modelData.id) {
case "clock":
return modelData.clockCompactMode !== undefined ? modelData.clockCompactMode : SettingsData.clockCompactMode;
case "focusedWindow":
return modelData.focusedWindowCompactMode !== undefined ? modelData.focusedWindowCompactMode : SettingsData.focusedWindowCompactMode;
case "keyboard_layout_name":
return modelData.keyboardLayoutNameCompactMode !== undefined ? modelData.keyboardLayoutNameCompactMode : SettingsData.keyboardLayoutNameCompactMode;
default:
@@ -515,8 +543,6 @@ Column {
switch (modelData.id) {
case "clock":
return modelData.clockCompactMode !== undefined ? modelData.clockCompactMode : SettingsData.clockCompactMode;
case "focusedWindow":
return modelData.focusedWindowCompactMode !== undefined ? modelData.focusedWindowCompactMode : SettingsData.focusedWindowCompactMode;
case "keyboard_layout_name":
return modelData.keyboardLayoutNameCompactMode !== undefined ? modelData.keyboardLayoutNameCompactMode : SettingsData.keyboardLayoutNameCompactMode;
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 {
id: diskUsageContextMenu
@@ -2144,7 +2338,7 @@ Column {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.compactModeChanged("music", modelData.sizeValue);
root.widgetSizeChanged("music", modelData.sizeValue);
musicContextMenu.close();
}
}
@@ -338,45 +338,61 @@ Scope {
border.width: 1
}
LauncherContent {
id: launcherContent
FocusScope {
anchors.fill: parent
anchors.margins: 0
focus: true
property var fakeParentModal: QtObject {
property bool spotlightOpen: spotlightContainer.visible
property bool isClosing: niriOverviewScope.isClosing
function hide() {
if (niriOverviewScope.searchActive) {
niriOverviewScope.hideSpotlight();
return;
Keys.onPressed: event => launcherContent.activeContextMenu?.handleKey(event)
Keys.onEscapePressed: event => {
launcherContent.activeContextMenu?.handleKey(event);
if (!event.accepted)
launcherContent.parentModal?.hide();
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 {
target: launcherContent.searchField
function onTextChanged() {
if (launcherContent.searchField.text.length > 0 || !niriOverviewScope.searchActive)
return;
niriOverviewScope.hideSpotlight();
Connections {
target: launcherContent.searchField
function onTextChanged() {
if (launcherContent.searchField.text.length > 0 || !niriOverviewScope.searchActive)
return;
niriOverviewScope.hideSpotlight();
}
}
}
Component.onCompleted: {
parentModal = fakeParentModal;
}
Connections {
target: launcherContent.controller
function onItemExecuted() {
niriOverviewScope.releaseKeyboard = true;
Component.onCompleted: {
parentModal = fakeParentModal;
}
function onModeChanged(mode) {
if (launcherContent.controller.autoSwitchedToFiles)
return;
SessionData.setNiriOverviewLastMode(mode);
Connections {
target: launcherContent.controller
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 {
id: root
readonly property var log: Log.scoped("AppSearchService")
property int refCount: 0
property var applications: []
property var _cachedCategories: null
@@ -296,20 +297,18 @@ Singleton {
function getBuiltInLauncherItems(pluginId, query) {
if (pluginId === "dms_clipboard_search") {
ClipboardService.ensureLauncherHistory();
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 => ({
type: "clipboard",
data: entry
}));
type: "clipboard",
data: entry
}));
}
if (pluginId !== "dms_settings_search")
return [];
SettingsSearchService.search(query);
const results = SettingsSearchService.results;
const results = SettingsSearchService.searchForLauncher(query);
const items = [];
for (let i = 0; i < results.length; 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() {
Proc.runCommand("checkGsettings", ["sh", "-c", "gsettings get org.gnome.desktop.sound theme-name 2>/dev/null"], (output, exitCode) => {
gsettingsAvailable = (exitCode === 0);
@@ -844,6 +852,36 @@ EOFCONFIG
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 {
target: "audio"
@@ -892,9 +930,7 @@ EOFCONFIG
}
function micmute(): string {
const result = root.toggleMicMute();
root.micMuteChanged();
return result;
return root.toggleMicMute();
}
function status(): string {
@@ -957,7 +993,6 @@ EOFCONFIG
return `Switched to: ${result}`;
}
}
Connections {
target: SettingsData
function onUseSystemSoundThemeChanged() {
+14
View File
@@ -28,6 +28,20 @@ Singleton {
});
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: {
if (!adapter || !adapter.devices) {
return [];
+73
View File
@@ -27,9 +27,14 @@ Singleton {
property bool keyboardNavigationActive: false
property int refCount: 0
property real _launcherLastRefresh: 0
property bool _launcherCacheValid: false
property string _launcherCachedQuery: ""
property var _launcherCachedEntries: []
property int _launcherSearchSeq: 0
signal historyCopied
signal historyCleared
signal launcherSearchReady(string query)
Process {
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) {
if (!clipboardAvailable) {
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) {
if (!wtypeAvailable) {
ToastService.showError(I18n.tr("wtype not available - install wtype for paste support"));
+247 -1
View File
@@ -36,6 +36,7 @@ Singleton {
signal randrDataReady
property var sortedToplevels: []
property var hyprlandVisibleSpecialWorkspaces: ({})
property bool _sortScheduled: false
signal toplevelsChanged
@@ -153,10 +154,14 @@ Singleton {
enabled: isHyprland
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 {
Hyprland.refreshToplevels();
if (event.name === "workspace" || event.name === "workspacev2" || event.name === "focusedmon" || event.name === "focusedmonv2" || event.name === "activespecial")
Hyprland.refreshMonitors();
} catch (e) {}
if (event.name === "activespecial")
root.updateHyprlandVisibleSpecialWorkspaces(event);
root.scheduleSort();
}
}
@@ -171,6 +176,7 @@ Singleton {
Component.onCompleted: {
fetchRandrData();
detectCompositor();
updateHyprlandVisibleSpecialWorkspaces(null);
scheduleSort();
Qt.callLater(() => {
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() {
if (!Hyprland.toplevels || !Hyprland.toplevels.values)
return [];
@@ -451,6 +532,171 @@ Singleton {
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) {
if (!toplevels || toplevels.length === 0 || !Hyprland.toplevels)
return toplevels;
@@ -41,6 +41,9 @@ Singleton {
property var savedConnections: []
property var ssidToConnectionName: ({})
property var wifiSignalIcon: {
if (isConnecting) {
return "wifi";
}
if (!wifiConnected) {
return "wifi_off";
}
+18
View File
@@ -463,6 +463,24 @@ Singleton {
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) {
if (!bindData.key || !Actions.isValidAction(bindData.action))
return;
@@ -99,6 +99,9 @@ Singleton {
}
readonly property string wifiSignalIcon: {
if (isConnecting) {
return "wifi";
}
if (!wifiConnected || networkStatus !== "wifi") {
return "wifi_off";
}
+1
View File
@@ -42,6 +42,7 @@ Singleton {
property string userPreference: activeService?.userPreference ?? "auto"
property bool isConnecting: activeService?.isConnecting ?? false
readonly property bool isWifiConnecting: isConnecting && !ethernetConnected && !wifiToggling
property string connectingSSID: activeService?.connectingSSID ?? ""
property string connectionError: activeService?.connectionError ?? ""
+66 -21
View File
@@ -35,6 +35,8 @@ Singleton {
property int maxIngressPerSecond: 20
property double _lastIngressSec: 0
property int _ingressCountThisSec: 0
readonly property int notificationDedupBurstMs: 5000
property var _recentDedupKeys: []
property var _dismissQueue: []
property int _dismissBatchSize: 8
@@ -291,18 +293,58 @@ Singleton {
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) {
if (!source)
return "";
const app = (source.appName || source.desktopEntry || "").toString();
const summary = (source.summary || "").toString();
const body = (source.body || "").toString();
const app = _dedupAppId(source);
const summary = _normalizeDedupText(source.summary);
const body = _normalizeDedupText(source.body);
const urgency = typeof source.urgency === "number" ? source.urgency : NotificationUrgency.Normal;
const icon = (source.appIcon || "").toString();
if (!app && !summary && !body)
return "";
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) {
@@ -310,17 +352,14 @@ Singleton {
if (!key)
return null;
for (const w of visibleNotifications) {
for (const w of allWrappers) {
if (!w || !w.notification || !w.popup)
continue;
if (_notificationDedupKey(w.notification) === key)
return w;
}
for (const w of notificationQueue) {
if (!w || !w.notification)
if (_notificationDedupKey(w.notification) !== key)
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;
}
@@ -637,14 +676,17 @@ Singleton {
return;
}
const duplicate = _findActiveDuplicate(notif);
if (duplicate) {
if (duplicate.timer && duplicate.timer.running)
duplicate.timer.restart();
try {
notif.dismiss();
} catch (e) {}
return;
if (SettingsData.notificationDedupeEnabled) {
const dedupKey = _notificationDedupKey(notif);
const duplicate = _findActiveDuplicate(notif);
if (duplicate || _hasRecentDuplicate(dedupKey)) {
if (duplicate && duplicate.timer && duplicate.timer.running)
duplicate.timer.restart();
try {
notif.dismiss();
} catch (e) {}
return;
}
}
if (!_ingressAllowed(policy.urgency)) {
@@ -686,6 +728,9 @@ Singleton {
});
if (wrapper) {
if (SettingsData.notificationDedupeEnabled)
_recordDedupKey(_notificationDedupKey(notif));
root.allWrappers.push(wrapper);
if (shouldKeepInCenter) {
root.notifications.push(wrapper);
+62 -6
View File
@@ -34,6 +34,8 @@ Singleton {
property var clipboardHistoryModal: null
property var dankLauncherV2Modal: null
property var dankLauncherV2ModalLoader: null
property var spotlightBarModal: null
property var spotlightBarModalLoader: null
property var powerMenuModal: null
property var processListModal: null
property var processListModalLoader: null
@@ -500,8 +502,16 @@ Singleton {
property bool _dankLauncherV2WantsToggle: false
property string _dankLauncherV2PendingQuery: ""
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) {
dankLauncherV2Modal.show();
} else if (dankLauncherV2ModalLoader) {
@@ -511,7 +521,8 @@ Singleton {
}
}
function openDankLauncherV2WithQuery(query: string) {
function openDankLauncherV2WithQuery(query: string, triggerUsesOverlayLayer) {
_setDankLauncherV2TriggerUsesOverlayLayer(triggerUsesOverlayLayer);
if (dankLauncherV2Modal) {
dankLauncherV2Modal.showWithQuery(query);
} else if (dankLauncherV2ModalLoader) {
@@ -522,7 +533,8 @@ Singleton {
}
}
function openDankLauncherV2WithMode(mode: string) {
function openDankLauncherV2WithMode(mode: string, triggerUsesOverlayLayer) {
_setDankLauncherV2TriggerUsesOverlayLayer(triggerUsesOverlayLayer);
if (dankLauncherV2Modal) {
dankLauncherV2Modal.showWithMode(mode);
} else if (dankLauncherV2ModalLoader) {
@@ -544,7 +556,8 @@ Singleton {
}
}
function toggleDankLauncherV2() {
function toggleDankLauncherV2(triggerUsesOverlayLayer) {
_setDankLauncherV2TriggerUsesOverlayLayer(triggerUsesOverlayLayer);
if (dankLauncherV2Modal) {
dankLauncherV2Modal.toggle();
} else if (dankLauncherV2ModalLoader) {
@@ -554,7 +567,8 @@ Singleton {
}
}
function toggleDankLauncherV2WithMode(mode: string) {
function toggleDankLauncherV2WithMode(mode: string, triggerUsesOverlayLayer) {
_setDankLauncherV2TriggerUsesOverlayLayer(triggerUsesOverlayLayer);
if (dankLauncherV2Modal) {
dankLauncherV2Modal.toggleWithMode(mode);
} else if (dankLauncherV2ModalLoader) {
@@ -565,7 +579,8 @@ Singleton {
}
}
function toggleDankLauncherV2WithQuery(query: string) {
function toggleDankLauncherV2WithQuery(query: string, triggerUsesOverlayLayer) {
_setDankLauncherV2TriggerUsesOverlayLayer(triggerUsesOverlayLayer);
if (dankLauncherV2Modal) {
dankLauncherV2Modal.toggleWithQuery(query);
} else if (dankLauncherV2ModalLoader) {
@@ -577,6 +592,8 @@ Singleton {
}
function _onDankLauncherV2ModalLoaded() {
if (dankLauncherV2Modal)
dankLauncherV2Modal.triggerUsesOverlayLayer = _dankLauncherV2TriggerUsesOverlayLayer;
if (_dankLauncherV2WantsOpen) {
_dankLauncherV2WantsOpen = false;
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() {
powerMenuModal?.openCentered();
}
+4
View File
@@ -205,6 +205,8 @@ Singleton {
}
function launchDesktopEntry(desktopEntry, useNvidia) {
if (!desktopEntry || !desktopEntry.command)
return;
let cmd = desktopEntry.command;
const appId = desktopEntry.id || desktopEntry.execString || desktopEntry.exec || "";
@@ -261,6 +263,8 @@ Singleton {
}
function launchDesktopAction(desktopEntry, action, useNvidia) {
if (!desktopEntry || !action || !action.command)
return;
let cmd = action.command;
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