mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-24 03:55:23 -04:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aed731efb0 | |||
| cf0632c077 | |||
| e92da4a15f | |||
| 8abdff3220 | |||
| 584d57a8de | |||
| afb5e59c29 | |||
| d9525908f1 | |||
| 6093c37b41 | |||
| bb05cbb6c5 | |||
| 4d4af8f549 | |||
| 0b55fbcb15 | |||
| 7476a220b5 | |||
| aaff1ab61e | |||
| 39622eb62a | |||
| eea039f575 | |||
| ef5de19f6b | |||
| f0c31bd7b3 | |||
| 7ddd0ca90d | |||
| b84e5abc4a | |||
| fb9ec8e721 | |||
| 078c9b4890 | |||
| 37c98220a9 | |||
| fc07611b3b | |||
| a923308c09 | |||
| 0990b43a43 | |||
| 548c2305fb | |||
| 4634763840 | |||
| cdc1102092 | |||
| 4845299cc2 |
@@ -541,5 +541,6 @@ func getCommonCommands() []*cobra.Command {
|
||||
blurCmd,
|
||||
trashCmd,
|
||||
systemCmd,
|
||||
switchUserCmd,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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%)" },
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 [];
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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 ?? ""
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 || "";
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user