1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-20 18:15:24 -04:00

Compare commits

..

7 Commits

Author SHA1 Message Date
purian23 8d94117a69 Cleanup 2026-06-12 10:56:16 -04:00
purian23 92569d8b4d Refactor connected chrome rendering & remove legacy components 2026-06-12 10:56:16 -04:00
purian23 fdee09b583 refactor: enhance plugin visibility w/bar reveal state 2026-06-12 10:56:16 -04:00
purian23 b60af507d7 refactor: implement keyboard focus management 2026-06-12 10:56:16 -04:00
purian23 2cc12b70d2 Update frameBlur performance 2026-06-12 10:56:15 -04:00
purian23 2df1dfe0bd Refactor shadow handling & improve connected chrome rendering 2026-06-12 10:56:15 -04:00
purian23 abf084eea2 refactor(framemode): connected surfaces 2026-06-12 10:56:15 -04:00
174 changed files with 5813 additions and 26298 deletions
-8
View File
@@ -20,14 +20,6 @@ repos:
language: system language: system
files: ^core/.*\.(go|mod|sum)$ files: ^core/.*\.(go|mod|sum)$
pass_filenames: false pass_filenames: false
- repo: local
hooks:
- id: settings-search-index
name: settings search index is up to date
entry: bash -c 'python3 quickshell/translations/extract_settings_index.py >/dev/null || exit 1; if ! git diff --exit-code -- quickshell/translations/settings_search_index.json; then echo "settings_search_index.json is out of date; run quickshell/translations/extract_settings_index.py and stage the result" >&2; exit 1; fi'
language: system
files: ^quickshell/(Modules/Settings/.*\.qml|Modals/Settings/SettingsSidebar\.qml|translations/extract_settings_index\.py)$
pass_filenames: false
- repo: local - repo: local
hooks: hooks:
- id: no-console-in-qml - id: no-console-in-qml
+1 -9
View File
@@ -19,12 +19,7 @@ var (
var colorCmd = &cobra.Command{ var colorCmd = &cobra.Command{
Use: "color", Use: "color",
Short: "Color utilities", Short: "Color utilities",
Long: `Color utilities including picking colors from the screen. Long: "Color utilities including picking colors from the screen",
This is the screen eyedropper CLI. To open the in-shell color modal, use:
dms ipc call color-picker toggle
See: https://danklinux.com/docs/dankmaterialshell/keybinds-ipc`,
} }
var colorPickCmd = &cobra.Command{ var colorPickCmd = &cobra.Command{
@@ -34,9 +29,6 @@ var colorPickCmd = &cobra.Command{
Click on any pixel to capture its color, or press Escape to cancel. Click on any pixel to capture its color, or press Escape to cancel.
This is the screen eyedropper CLI. To open the in-shell color modal, use:
dms ipc call color-picker toggle
Output format flags (mutually exclusive, default: --hex): Output format flags (mutually exclusive, default: --hex):
--hex - Hexadecimal (#RRGGBB) --hex - Hexadecimal (#RRGGBB)
--rgb - RGB values (R G B) --rgb - RGB values (R G B)
+3 -16
View File
@@ -77,15 +77,10 @@ var killCmd = &cobra.Command{
} }
var ipcCmd = &cobra.Command{ var ipcCmd = &cobra.Command{
Use: "ipc", Use: "ipc [target] [function] [args...]",
Short: "Send IPC commands to running DMS shell", Short: "Send IPC commands to running DMS shell",
Long: `Send IPC commands to the running DMS shell.
dms ipc call <target> <function> [args...] invoke a command
dms ipc list list all targets and functions
Full reference: https://danklinux.com/docs/dankmaterialshell/keybinds-ipc`,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
_ = findConfig(cmd, args)
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
}, },
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
@@ -93,17 +88,9 @@ Full reference: https://danklinux.com/docs/dankmaterialshell/keybinds-ipc`,
}, },
} }
var ipcListCmd = &cobra.Command{
Use: "list",
Short: "List all IPC targets and functions",
Run: func(cmd *cobra.Command, args []string) {
printIPCHelp()
},
}
func init() { func init() {
ipcCmd.AddCommand(ipcListCmd)
ipcCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { ipcCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
_ = findConfig(cmd, args)
printIPCHelp() printIPCHelp()
}) })
} }
+27 -44
View File
@@ -601,30 +601,12 @@ func parseTargetsFromIPCShowOutput(output string) ipcTargets {
return targets return targets
} }
func buildQsIPCBaseArgs() ([]string, error) {
cmdArgs := []string{"ipc"}
switch pid, ok := getFirstDMSPID(); {
case ok:
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
default:
if err := findConfig(nil, nil); err != nil {
return nil, err
}
if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display")
}
cmdArgs = append(cmdArgs, "-p", configPath)
}
return cmdArgs, nil
}
func getShellIPCCompletions(args []string, _ string) []string { func getShellIPCCompletions(args []string, _ string) []string {
baseArgs, err := buildQsIPCBaseArgs() cmdArgs := []string{"ipc"}
if err != nil { if qsHasAnyDisplay() {
log.Debugf("Error building IPC args for completions: %v", err) cmdArgs = append(cmdArgs, "--any-display")
return nil
} }
cmdArgs := append(baseArgs, "show") cmdArgs = append(cmdArgs, "-p", configPath, "show")
cmd := exec.Command("qs", cmdArgs...) cmd := exec.Command("qs", cmdArgs...)
var targets ipcTargets var targets ipcTargets
@@ -641,7 +623,7 @@ func getShellIPCCompletions(args []string, _ string) []string {
if len(args) == 0 { if len(args) == 0 {
targetNames := make([]string, 0) targetNames := make([]string, 0)
targetNames = append(targetNames, "call", "list") targetNames = append(targetNames, "call")
for k := range targets { for k := range targets {
targetNames = append(targetNames, k) targetNames = append(targetNames, k)
} }
@@ -714,11 +696,23 @@ func runShellIPCCommand(args []string) {
args = append([]string{"call"}, args...) args = append([]string{"call"}, args...)
} }
baseArgs, err := buildQsIPCBaseArgs() cmdArgs := []string{"ipc"}
if err != nil {
log.Fatalf("Error finding config: %v", err) switch pid, ok := getFirstDMSPID(); {
case ok:
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
default:
if err := findConfig(nil, nil); err != nil {
log.Fatalf("Error finding config: %v", err)
}
// ! TODO - remove check when QS 0.3 is released
if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display")
}
cmdArgs = append(cmdArgs, "-p", configPath)
} }
cmdArgs := append(baseArgs, args...)
cmdArgs = append(cmdArgs, args...)
cmd := exec.Command("qs", cmdArgs...) cmd := exec.Command("qs", cmdArgs...)
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
@@ -730,20 +724,19 @@ func runShellIPCCommand(args []string) {
} }
func printIPCHelp() { func printIPCHelp() {
fmt.Println("Usage: dms ipc call <target> <function> [args...]") fmt.Println("Usage: dms ipc <target> <function> [args...]")
fmt.Println() fmt.Println()
baseArgs, err := buildQsIPCBaseArgs() cmdArgs := []string{"ipc"}
if err != nil { if qsHasAnyDisplay() {
printIPCHelpFailure(err) cmdArgs = append(cmdArgs, "--any-display")
return
} }
cmdArgs := append(baseArgs, "show") cmdArgs = append(cmdArgs, "-p", configPath, "show")
cmd := exec.Command("qs", cmdArgs...) cmd := exec.Command("qs", cmdArgs...)
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
printIPCHelpFailure(err) fmt.Println("Could not retrieve available IPC targets (is DMS running?)")
return return
} }
@@ -772,16 +765,6 @@ func printIPCHelp() {
} }
} }
func printIPCHelpFailure(err error) {
fmt.Println("Could not retrieve IPC targets.")
if err != nil {
fmt.Printf(" %v\n", err)
}
fmt.Println()
fmt.Println(" Full docs: https://danklinux.com/docs/dankmaterialshell/keybinds-ipc")
fmt.Println(" Try: dms ipc call <target> <function>")
}
// ensureFontCache rebuilds the fontconfig cache if user-configured fonts are missing while skipping defaults // ensureFontCache rebuilds the fontconfig cache if user-configured fonts are missing while skipping defaults
func ensureFontCache() { func ensureFontCache() {
if _, err := exec.LookPath("fc-list"); err != nil { if _, err := exec.LookPath("fc-list"); err != nil {
@@ -51,7 +51,7 @@ type NiriParser struct {
} }
func parseKDL(data []byte) (*document.Document, error) { func parseKDL(data []byte) (*document.Document, error) {
return kdl.Parse(strings.NewReader(normalizeKDLBraces(quoteLeadingUnderscoreIdents(string(data))))) return kdl.Parse(strings.NewReader(normalizeKDLBraces(string(data))))
} }
func normalizeKDLBraces(input string) string { func normalizeKDLBraces(input string) string {
@@ -94,93 +94,6 @@ func normalizeKDLBraces(input string) string {
return sb.String() return sb.String()
} }
// quoteLeadingUnderscoreIdents wraps bare KDL identifiers that begin with '_'
// in double quotes. kdl-go rejects '_' as the first character of a bare
// identifier (e.g. the common `_JAVA_AWT_WM_NONREPARENTING "1"` environment
// node), even though niri's own parser and the KDL spec accept it — so without
// this the whole config fails to parse and no keybinds load. Quoting lets
// kdl-go parse it; this is safe because the niri parser only dispatches on
// fixed node/section names (binds, recent-windows, include, ...) that never
// start with '_', so re-quoting such a name cannot change what DMS reads.
// Underscores elsewhere in an identifier (XDG_CURRENT_DESKTOP) are left
// untouched, and underscores inside strings or comments are skipped. Only a
// leading '_' is handled; other start characters kdl-go over-rejects (e.g. '.'
// or '?') do not occur in niri configs.
func quoteLeadingUnderscoreIdents(input string) string {
var sb strings.Builder
sb.Grow(len(input))
var prev byte
n := len(input)
for i := 0; i < n; {
c := input[i]
switch {
case c == '"':
end := findStringEnd(input, i)
sb.WriteString(input[i:end])
prev = '"'
i = end
case c == '/' && i+1 < n && input[i+1] == '/':
end := findLineCommentEnd(input, i)
sb.WriteString(input[i:end])
prev = '\n'
i = end
case c == '/' && i+1 < n && input[i+1] == '*':
end := findBlockCommentEnd(input, i)
sb.WriteString(input[i:end])
prev = ' '
i = end
case c == '/' && i+1 < n && input[i+1] == '-':
// KDL slashdash: /- comments out the next node/value. Keep the
// marker but treat what follows as a fresh token start, so a
// slashdashed leading-underscore node (e.g. `/-_FOO "1"`) still
// gets quoted instead of crashing kdl-go.
sb.WriteByte('/')
sb.WriteByte('-')
prev = ' '
i += 2
case c == '_' && isIdentBoundary(prev):
end := scanBareIdent(input, i)
sb.WriteByte('"')
sb.WriteString(input[i:end])
sb.WriteByte('"')
prev = '"'
i = end
default:
sb.WriteByte(c)
prev = c
i++
}
}
return sb.String()
}
// isIdentBoundary reports whether the previously emitted byte ends a token, so
// that a following '_' starts a fresh bare identifier rather than sitting in
// the middle of one.
func isIdentBoundary(prev byte) bool {
switch prev {
case 0, ' ', '\t', '\n', '\r', '{', '}', ';', '=', '(', ')', ',':
return true
}
return false
}
// scanBareIdent returns the index just past the bare identifier starting at
// start, stopping at whitespace or any KDL delimiter.
func scanBareIdent(s string, start int) int {
n := len(s)
for i := start; i < n; i++ {
switch s[i] {
case ' ', '\t', '\n', '\r', '"', '{', '}', '(', ')', ';', '=', ',', '/', '\\', '<', '>', '[', ']':
return i
}
}
return n
}
func findStringEnd(s string, start int) int { func findStringEnd(s string, start int) int {
n := len(s) n := len(s)
for i := start + 1; i < n; { for i := start + 1; i < n; {
@@ -71,101 +71,6 @@ func TestNormalizeKDLBraces(t *testing.T) {
} }
} }
func TestQuoteLeadingUnderscoreIdents(t *testing.T) {
tests := []struct {
name string
in string
out string
}{
{"leading underscore node", `_JAVA_AWT_WM_NONREPARENTING "1"`, `"_JAVA_AWT_WM_NONREPARENTING" "1"`},
{"mid underscore untouched", `XDG_CURRENT_DESKTOP "niri"`, `XDG_CURRENT_DESKTOP "niri"`},
{"indented node", "environment {\n _FOO \"1\"\n}", "environment {\n \"_FOO\" \"1\"\n}"},
{"underscore in string", `spawn "_not_a_node"`, `spawn "_not_a_node"`},
{"underscore in line comment", "// _comment\n_FOO \"1\"", "// _comment\n\"_FOO\" \"1\""},
{"underscore in block comment", "/* _x */ _FOO \"1\"", "/* _x */ \"_FOO\" \"1\""},
{"block comment abuts node", `/* x */_FOO "1"`, `/* x */"_FOO" "1"`},
{"slashdash before node", `/-_FOO "1"`, `/-"_FOO" "1"`},
{"node after closing paren", "node (u8)_v", `node (u8)"_v"`},
{"node before brace without space", "_FOO{ }", `"_FOO"{ }`},
{"lone underscore", `_ "x"`, `"_" "x"`},
{"property value", "node key=_val", `node key="_val"`},
{"no underscores", "node child", "node child"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := quoteLeadingUnderscoreIdents(tc.in)
if got != tc.out {
t.Errorf("quoteLeadingUnderscoreIdents(%q) = %q, want %q", tc.in, got, tc.out)
}
})
}
}
func TestNiriParseLeadingUnderscoreEnvironment(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
// A leading-underscore environment node (a common Java/tiling-WM fix) must
// not abort parsing of the rest of the config — keybinds still have to load.
content := `environment {
XDG_CURRENT_DESKTOP "niri"
_JAVA_AWT_WM_NONREPARENTING "1"
}
binds {
Mod+Q { close-window; }
Mod+KP_Home { focus-workspace 1; }
}
`
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed on config with leading-underscore env node: %v", err)
}
if len(result.Section.Keybinds) != 2 {
t.Errorf("Expected 2 keybinds, got %d", len(result.Section.Keybinds))
}
foundClose := false
for _, kb := range result.Section.Keybinds {
if kb.Action == "close-window" {
foundClose = true
}
}
if !foundClose {
t.Error("close-window keybind not found — leading-underscore env node broke parsing")
}
}
func TestNiriParseSlashdashLeadingUnderscore(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.kdl")
// A slashdashed leading-underscore node must not abort parsing either.
content := `environment {
/-_JAVA_AWT_WM_NONREPARENTING "1"
}
binds {
Mod+Q { close-window; }
}
`
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
result, err := ParseNiriKeys(tmpDir)
if err != nil {
t.Fatalf("ParseNiriKeys failed on config with slashdashed leading-underscore node: %v", err)
}
if len(result.Section.Keybinds) != 1 {
t.Errorf("Expected 1 keybind, got %d", len(result.Section.Keybinds))
}
}
func TestNiriParseKeyCombo(t *testing.T) { func TestNiriParseKeyCombo(t *testing.T) {
tests := []struct { tests := []struct {
combo string combo string
-2
View File
@@ -125,8 +125,6 @@ State updates are sent whenever network configuration changes:
- `wifiConnected`: Whether associated with an access point - `wifiConnected`: Whether associated with an access point
- `wifiSSID`: Currently connected network name - `wifiSSID`: Currently connected network name
- `wifiIP`: Assigned IP address (empty until DHCP completes) - `wifiIP`: Assigned IP address (empty until DHCP completes)
- `savedWifiNetworks` (API v26+): Saved WiFi profiles exposed at SSID granularity. If a backend has multiple profiles for the same SSID, DMS merges them into one SSID-level entry. Clients talking to older servers should derive saved visible networks from `wifiNetworks` entries where `saved` is true.
- `savedWifiNetworks[].outOfRange` (API v26+): Whether the saved profile is not currently visible in scan results. Fallback entries derived from `wifiNetworks` should be treated as visible (`outOfRange: false`).
- `lastError`: Error message from last failed connection attempt - `lastError`: Error message from last failed connection attempt
### network.credentials Service Events ### network.credentials Service Events
-1
View File
@@ -67,7 +67,6 @@ type BackendState struct {
WiFiBSSID string WiFiBSSID string
WiFiSignal uint8 WiFiSignal uint8
WiFiNetworks []WiFiNetwork WiFiNetworks []WiFiNetwork
SavedWiFiNetworks []WiFiNetwork
WiFiDevices []WiFiDevice WiFiDevices []WiFiDevice
WiredConnections []WiredConnection WiredConnections []WiredConnection
VPNProfiles []VPNProfile VPNProfiles []VPNProfile
@@ -27,19 +27,6 @@ func TestHybridIwdNetworkdBackend_GetCurrentState_MergesState(t *testing.T) {
wifi.state.WiFiBSSID = "00:11:22:33:44:55" wifi.state.WiFiBSSID = "00:11:22:33:44:55"
wifi.state.WiFiSignal = 75 wifi.state.WiFiSignal = 75
wifi.state.WiFiDevice = "wlan0" wifi.state.WiFiDevice = "wlan0"
wifi.state.SavedWiFiNetworks = []WiFiNetwork{
{
SSID: "TestNetwork",
Saved: true,
Autoconnect: true,
Connected: true,
},
{
SSID: "AwayNetwork",
Saved: true,
OutOfRange: true,
},
}
l3.state.WiFiIP = "192.168.1.100" l3.state.WiFiIP = "192.168.1.100"
l3.state.EthernetConnected = false l3.state.EthernetConnected = false
@@ -55,9 +42,6 @@ func TestHybridIwdNetworkdBackend_GetCurrentState_MergesState(t *testing.T) {
assert.True(t, state.WiFiConnected) assert.True(t, state.WiFiConnected)
assert.False(t, state.EthernetConnected) assert.False(t, state.EthernetConnected)
assert.Equal(t, StatusWiFi, state.NetworkStatus) assert.Equal(t, StatusWiFi, state.NetworkStatus)
assert.Len(t, state.SavedWiFiNetworks, 2)
assert.Equal(t, "TestNetwork", state.SavedWiFiNetworks[0].SSID)
assert.True(t, state.SavedWiFiNetworks[1].OutOfRange)
} }
func TestHybridIwdNetworkdBackend_GetCurrentState_EthernetPriority(t *testing.T) { func TestHybridIwdNetworkdBackend_GetCurrentState_EthernetPriority(t *testing.T) {
@@ -80,10 +80,6 @@ func (b *IWDBackend) Initialize() error {
return fmt.Errorf("failed to discover iwd devices: %w", err) return fmt.Errorf("failed to discover iwd devices: %w", err)
} }
if err := b.updateSavedWiFiNetworks(); err != nil {
log.Warnf("Failed to get initial saved WiFi networks: %v", err)
}
if err := b.updateState(); err != nil { if err := b.updateState(); err != nil {
conn.Close() conn.Close()
return fmt.Errorf("failed to get initial state: %w", err) return fmt.Errorf("failed to get initial state: %w", err)
@@ -149,7 +145,6 @@ func (b *IWDBackend) GetCurrentState() (*BackendState, error) {
state := *b.state state := *b.state
state.WiFiNetworks = append([]WiFiNetwork(nil), b.state.WiFiNetworks...) state.WiFiNetworks = append([]WiFiNetwork(nil), b.state.WiFiNetworks...)
state.SavedWiFiNetworks = append([]WiFiNetwork(nil), b.state.SavedWiFiNetworks...)
state.WiredConnections = append([]WiredConnection(nil), b.state.WiredConnections...) state.WiredConnections = append([]WiredConnection(nil), b.state.WiredConnections...)
state.WiFiDevices = b.getWiFiDevicesLocked() state.WiFiDevices = b.getWiFiDevicesLocked()
@@ -45,42 +45,12 @@ func (b *IWDBackend) StartMonitoring(onStateChange func()) error {
} }
} }
if err := b.conn.AddMatchSignal(
dbus.WithMatchInterface(dbusPropertiesInterface),
dbus.WithMatchMember("PropertiesChanged"),
dbus.WithMatchArg(0, iwdKnownNetworkInterface),
); err != nil {
return fmt.Errorf("failed to add known network signal match: %w", err)
}
if err := b.conn.AddMatchSignal(
dbus.WithMatchInterface(dbusObjectManager),
dbus.WithMatchMember("InterfacesAdded"),
); err != nil {
return fmt.Errorf("failed to add iwd interfaces-added signal match: %w", err)
}
if err := b.conn.AddMatchSignal(
dbus.WithMatchInterface(dbusObjectManager),
dbus.WithMatchMember("InterfacesRemoved"),
); err != nil {
return fmt.Errorf("failed to add iwd interfaces-removed signal match: %w", err)
}
b.sigWG.Add(1) b.sigWG.Add(1)
go b.signalHandler(sigChan) go b.signalHandler(sigChan)
return nil return nil
} }
func (b *IWDBackend) refreshWiFiNetworkState() bool {
_, err := b.updateWiFiNetworks()
if err == nil {
return true
}
return b.updateSavedWiFiNetworks() == nil
}
func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) { func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) {
defer b.sigWG.Done() defer b.sigWG.Done()
@@ -96,36 +66,11 @@ func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) {
return return
} }
if sig.Name == dbusObjectManager+".InterfacesAdded" { if sig.Name != dbusPropertiesInterface+".PropertiesChanged" {
if len(sig.Body) >= 2 {
if interfaces, ok := sig.Body[1].(map[string]map[string]dbus.Variant); ok {
if _, ok := interfaces[iwdKnownNetworkInterface]; ok {
if b.refreshWiFiNetworkState() && b.onStateChange != nil {
b.onStateChange()
}
}
}
}
continue continue
} }
if sig.Name == dbusObjectManager+".InterfacesRemoved" { if len(sig.Body) < 2 {
if len(sig.Body) >= 2 {
if interfaces, ok := sig.Body[1].([]string); ok {
for _, iface := range interfaces {
if iface == iwdKnownNetworkInterface {
if b.refreshWiFiNetworkState() && b.onStateChange != nil {
b.onStateChange()
}
break
}
}
}
}
continue
}
if sig.Name != dbusPropertiesInterface+".PropertiesChanged" || len(sig.Body) < 2 {
continue continue
} }
@@ -142,9 +87,6 @@ func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) {
stateChanged := false stateChanged := false
switch iface { switch iface {
case iwdKnownNetworkInterface:
stateChanged = b.refreshWiFiNetworkState()
case iwdDeviceInterface: case iwdDeviceInterface:
if sig.Path == b.devicePath { if sig.Path == b.devicePath {
if poweredVar, ok := changed["Powered"]; ok { if poweredVar, ok := changed["Powered"]; ok {
@@ -163,7 +105,13 @@ func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) {
if sig.Path == b.stationPath { if sig.Path == b.stationPath {
if scanningVar, ok := changed["Scanning"]; ok { if scanningVar, ok := changed["Scanning"]; ok {
if scanning, ok := scanningVar.Value().(bool); ok && !scanning { if scanning, ok := scanningVar.Value().(bool); ok && !scanning {
stateChanged = b.refreshWiFiNetworkState() || stateChanged networks, err := b.updateWiFiNetworks()
if err == nil {
b.stateMutex.Lock()
b.state.WiFiNetworks = networks
b.stateMutex.Unlock()
stateChanged = true
}
b.stateMutex.RLock() b.stateMutex.RLock()
wifiConnected := b.state.WiFiConnected wifiConnected := b.state.WiFiConnected
@@ -288,7 +236,6 @@ func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) {
} }
} }
b.refreshWiFiNetworkState()
stateChanged = true stateChanged = true
if att != nil && isTarget { if att != nil && isTarget {
@@ -335,7 +282,6 @@ func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) {
b.state.NetworkStatus = StatusDisconnected b.state.NetworkStatus = StatusDisconnected
} }
b.stateMutex.Unlock() b.stateMutex.Unlock()
b.refreshWiFiNetworkState()
stateChanged = true stateChanged = true
} }
} }
@@ -396,7 +342,6 @@ func (b *IWDBackend) signalHandler(sigChan chan *dbus.Signal) {
stateChanged = true stateChanged = true
} }
b.stateMutex.Unlock() b.stateMutex.Unlock()
b.refreshWiFiNetworkState()
} }
} }
} }
@@ -4,7 +4,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/godbus/dbus/v5"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -169,92 +168,6 @@ func TestIWDBackend_MapIwdDBusError(t *testing.T) {
} }
} }
func TestIWDSavedWiFiProfilesFromManagedObjects(t *testing.T) {
objects := map[dbus.ObjectPath]map[string]map[string]dbus.Variant{
"/net/connman/iwd/known_network/1": {
iwdKnownNetworkInterface: {
"Name": dbus.MakeVariant("Home"),
"AutoConnect": dbus.MakeVariant(false),
"Hidden": dbus.MakeVariant(true),
"Type": dbus.MakeVariant("psk"),
},
},
"/net/connman/iwd/known_network/2": {
iwdKnownNetworkInterface: {
"Name": dbus.MakeVariant("Office"),
"Type": dbus.MakeVariant("8021x"),
},
},
"/net/connman/iwd/known_network/3": {
iwdKnownNetworkInterface: {
"Name": dbus.MakeVariant("Cafe"),
"Type": dbus.MakeVariant("open"),
},
},
"/net/connman/iwd/network/1": {
iwdNetworkInterface: {
"Name": dbus.MakeVariant("VisibleOnly"),
},
},
}
profiles := iwdSavedWiFiProfilesFromManagedObjects(objects)
assert.Len(t, profiles, 3)
assert.False(t, profiles["Home"].Autoconnect)
assert.True(t, profiles["Home"].Hidden)
assert.True(t, profiles["Home"].Secured)
assert.False(t, profiles["Home"].Enterprise)
assert.True(t, profiles["Office"].Autoconnect)
assert.True(t, profiles["Office"].Secured)
assert.True(t, profiles["Office"].Enterprise)
assert.True(t, profiles["Cafe"].Autoconnect)
assert.False(t, profiles["Cafe"].Secured)
assert.False(t, profiles["Cafe"].Enterprise)
}
func TestIWDWiFiNetworksFromVisibleIncludesConnectedHiddenFallback(t *testing.T) {
profiles := map[string]savedWiFiProfile{
"Home": {
Autoconnect: true,
Secured: true,
Hidden: true,
Mode: "infrastructure",
},
}
visible := []WiFiNetwork{
{
SSID: "Cafe",
Signal: 42,
Secured: false,
},
}
networks := iwdWiFiNetworksFromVisible(visible, profiles, "Home", true, 68)
savedNetworks := savedWiFiNetworksFromProfiles(profiles, map[string]WiFiNetwork{
networks[0].SSID: networks[0],
networks[1].SSID: networks[1],
}, "Home", true)
assert.Len(t, networks, 2)
assert.Equal(t, "Cafe", networks[0].SSID)
assert.False(t, networks[0].Connected)
assert.Equal(t, "Home", networks[1].SSID)
assert.True(t, networks[1].Connected)
assert.True(t, networks[1].Hidden)
assert.True(t, networks[1].Saved)
assert.True(t, networks[1].Autoconnect)
assert.Equal(t, uint8(68), networks[1].Signal)
assert.Len(t, savedNetworks, 1)
assert.Equal(t, "Home", savedNetworks[0].SSID)
assert.True(t, savedNetworks[0].Connected)
assert.False(t, savedNetworks[0].OutOfRange)
}
func TestConnectAttempt_Finalization(t *testing.T) { func TestConnectAttempt_Finalization(t *testing.T) {
backend, _ := NewIWDBackend() backend, _ := NewIWDBackend()
backend.state = &BackendState{} backend.state = &BackendState{}
+60 -143
View File
@@ -164,18 +164,22 @@ func (b *IWDBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
return nil, fmt.Errorf("failed to get networks: %w", err) return nil, fmt.Errorf("failed to get networks: %w", err)
} }
savedProfiles, err := b.getIWDSavedWiFiProfiles() knownNetworks, err := b.getKnownNetworks()
if err != nil { if err != nil {
savedProfiles = make(map[string]savedWiFiProfile) knownNetworks = make(map[string]bool)
}
autoconnectMap, err := b.getAutoconnectSettings()
if err != nil {
autoconnectMap = make(map[string]bool)
} }
b.stateMutex.RLock() b.stateMutex.RLock()
currentSSID := b.state.WiFiSSID currentSSID := b.state.WiFiSSID
wifiConnected := b.state.WiFiConnected wifiConnected := b.state.WiFiConnected
wifiSignal := b.state.WiFiSignal
b.stateMutex.RUnlock() b.stateMutex.RUnlock()
visibleNetworks := make([]WiFiNetwork, 0, len(orderedNetworks)) networks := make([]WiFiNetwork, 0, len(orderedNetworks))
for _, netData := range orderedNetworks { for _, netData := range orderedNetworks {
if len(netData) < 2 { if len(netData) < 2 {
continue continue
@@ -221,26 +225,23 @@ func (b *IWDBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
secured := netType != "open" secured := netType != "open"
visibleNetworks = append(visibleNetworks, WiFiNetwork{ network := WiFiNetwork{
SSID: name, SSID: name,
Signal: signal, Signal: signal,
Secured: secured, Secured: secured,
Enterprise: netType == "8021x", Connected: wifiConnected && name == currentSSID,
}) Saved: knownNetworks[name],
} Autoconnect: autoconnectMap[name],
Enterprise: netType == "8021x",
}
networks := iwdWiFiNetworksFromVisible(visibleNetworks, savedProfiles, currentSSID, wifiConnected, wifiSignal) networks = append(networks, network)
visibleNetworkMap := make(map[string]WiFiNetwork, len(networks))
for _, network := range networks {
visibleNetworkMap[network.SSID] = network
} }
savedNetworks := savedWiFiNetworksFromProfiles(savedProfiles, visibleNetworkMap, currentSSID, wifiConnected)
sortWiFiNetworks(networks) sortWiFiNetworks(networks)
b.stateMutex.Lock() b.stateMutex.Lock()
b.state.WiFiNetworks = networks b.state.WiFiNetworks = networks
b.state.SavedWiFiNetworks = savedNetworks
b.stateMutex.Unlock() b.stateMutex.Unlock()
now := time.Now() now := time.Now()
@@ -253,129 +254,7 @@ func (b *IWDBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
return networks, nil return networks, nil
} }
func (b *IWDBackend) updateSavedWiFiNetworks() error { func (b *IWDBackend) getKnownNetworks() (map[string]bool, error) {
savedProfiles, err := b.getIWDSavedWiFiProfiles()
if err != nil {
return err
}
b.stateMutex.RLock()
currentSSID := b.state.WiFiSSID
wifiConnected := b.state.WiFiConnected
wifiNetworks := append([]WiFiNetwork(nil), b.state.WiFiNetworks...)
b.stateMutex.RUnlock()
wifiNetworks, savedNetworks := refreshSavedWiFiState(wifiNetworks, savedProfiles, currentSSID, wifiConnected)
b.stateMutex.Lock()
b.state.WiFiNetworks = wifiNetworks
b.state.SavedWiFiNetworks = savedNetworks
b.stateMutex.Unlock()
return nil
}
func iwdWiFiNetworksFromVisible(visibleNetworks []WiFiNetwork, savedProfiles map[string]savedWiFiProfile, currentSSID string, wifiConnected bool, wifiSignal uint8) []WiFiNetwork {
networks := make([]WiFiNetwork, 0, len(visibleNetworks)+1)
seenSSIDs := make(map[string]struct{}, len(visibleNetworks)+1)
for _, network := range visibleNetworks {
profile, saved := savedProfiles[network.SSID]
network.Connected = wifiConnected && network.SSID == currentSSID
network.Saved = saved
network.Autoconnect = profile.Autoconnect
network.Hidden = network.Hidden || profile.Hidden
network.Secured = network.Secured || profile.Secured
network.Enterprise = network.Enterprise || profile.Enterprise
if network.Mode == "" {
network.Mode = profile.Mode
}
networks = append(networks, network)
seenSSIDs[network.SSID] = struct{}{}
}
if wifiConnected && currentSSID != "" {
if _, exists := seenSSIDs[currentSSID]; !exists {
profile, saved := savedProfiles[currentSSID]
secured := profile.Secured
if !saved {
secured = true
}
mode := profile.Mode
if mode == "" {
mode = "infrastructure"
}
networks = append(networks, WiFiNetwork{
SSID: currentSSID,
Signal: wifiSignal,
Secured: secured,
Enterprise: profile.Enterprise,
Connected: true,
Saved: saved,
Autoconnect: profile.Autoconnect,
Hidden: true,
Mode: mode,
})
}
}
return networks
}
func iwdSavedWiFiProfilesFromManagedObjects(objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant) map[string]savedWiFiProfile {
profiles := make(map[string]savedWiFiProfile)
for _, interfaces := range objects {
knownProps, ok := interfaces[iwdKnownNetworkInterface]
if !ok {
continue
}
nameVar, ok := knownProps["Name"]
if !ok {
continue
}
name, ok := nameVar.Value().(string)
if !ok || name == "" {
continue
}
profile := savedWiFiProfile{
Autoconnect: true,
Mode: "infrastructure",
}
if acVar, ok := knownProps["AutoConnect"]; ok {
if autoconnect, ok := acVar.Value().(bool); ok {
profile.Autoconnect = autoconnect
}
}
if hiddenVar, ok := knownProps["Hidden"]; ok {
if hidden, ok := hiddenVar.Value().(bool); ok {
profile.Hidden = hidden
}
}
if typeVar, ok := knownProps["Type"]; ok {
if networkType, ok := typeVar.Value().(string); ok {
profile.Secured = networkType != "" && networkType != "open"
profile.Enterprise = networkType == "8021x"
}
}
if existing, ok := profiles[name]; ok {
profile.Autoconnect = profile.Autoconnect || existing.Autoconnect
profile.Hidden = profile.Hidden || existing.Hidden
profile.Secured = profile.Secured || existing.Secured
profile.Enterprise = profile.Enterprise || existing.Enterprise
}
profiles[name] = profile
}
return profiles
}
func (b *IWDBackend) getIWDSavedWiFiProfiles() (map[string]savedWiFiProfile, error) {
obj := b.conn.Object(iwdBusName, iwdObjectPath) obj := b.conn.Object(iwdBusName, iwdObjectPath)
var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
@@ -384,7 +263,47 @@ func (b *IWDBackend) getIWDSavedWiFiProfiles() (map[string]savedWiFiProfile, err
return nil, err return nil, err
} }
return iwdSavedWiFiProfilesFromManagedObjects(objects), nil known := make(map[string]bool)
for _, interfaces := range objects {
if knownProps, ok := interfaces[iwdKnownNetworkInterface]; ok {
if nameVar, ok := knownProps["Name"]; ok {
if name, ok := nameVar.Value().(string); ok {
known[name] = true
}
}
}
}
return known, nil
}
func (b *IWDBackend) getAutoconnectSettings() (map[string]bool, error) {
obj := b.conn.Object(iwdBusName, iwdObjectPath)
var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
err := obj.Call(dbusObjectManager+".GetManagedObjects", 0).Store(&objects)
if err != nil {
return nil, err
}
autoconnectMap := make(map[string]bool)
for _, interfaces := range objects {
if knownProps, ok := interfaces[iwdKnownNetworkInterface]; ok {
if nameVar, ok := knownProps["Name"]; ok {
if name, ok := nameVar.Value().(string); ok {
autoconnect := true
if acVar, ok := knownProps["AutoConnect"]; ok {
if ac, ok := acVar.Value().(bool); ok {
autoconnect = ac
}
}
autoconnectMap[name] = autoconnect
}
}
}
}
return autoconnectMap, nil
} }
func (b *IWDBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInfoResponse, error) { func (b *IWDBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInfoResponse, error) {
@@ -695,8 +614,6 @@ func (b *IWDBackend) ForgetWiFiNetwork(ssid string) error {
b.stateMutex.Unlock() b.stateMutex.Unlock()
} }
_, _ = b.updateWiFiNetworks()
if b.onStateChange != nil { if b.onStateChange != nil {
b.onStateChange() b.onStateChange()
} }
@@ -222,10 +222,6 @@ func (b *NetworkManagerBackend) Initialize() error {
log.Warnf("Failed to update WiFi state: %v", err) log.Warnf("Failed to update WiFi state: %v", err)
} }
if err := b.updateSavedWiFiNetworks(); err != nil {
log.Warnf("Failed to get initial saved WiFi networks: %v", err)
}
if wifiEnabled { if wifiEnabled {
if _, err := b.updateWiFiNetworks(); err != nil { if _, err := b.updateWiFiNetworks(); err != nil {
log.Warnf("Failed to get initial networks: %v", err) log.Warnf("Failed to get initial networks: %v", err)
@@ -265,7 +261,6 @@ func (b *NetworkManagerBackend) GetCurrentState() (*BackendState, error) {
state := *b.state state := *b.state
state.WiFiNetworks = append([]WiFiNetwork(nil), b.state.WiFiNetworks...) state.WiFiNetworks = append([]WiFiNetwork(nil), b.state.WiFiNetworks...)
state.SavedWiFiNetworks = append([]WiFiNetwork(nil), b.state.SavedWiFiNetworks...)
state.WiFiDevices = append([]WiFiDevice(nil), b.state.WiFiDevices...) state.WiFiDevices = append([]WiFiDevice(nil), b.state.WiFiDevices...)
state.WiredConnections = append([]WiredConnection(nil), b.state.WiredConnections...) state.WiredConnections = append([]WiredConnection(nil), b.state.WiredConnections...)
state.EthernetDevices = append([]EthernetDevice(nil), b.state.EthernetDevices...) state.EthernetDevices = append([]EthernetDevice(nil), b.state.EthernetDevices...)
@@ -5,12 +5,6 @@ import (
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
const (
dbusNMSettingsPath = "/org/freedesktop/NetworkManager/Settings"
dbusNMSettingsInterface = "org.freedesktop.NetworkManager.Settings"
dbusNMSettingsConnectionInterface = "org.freedesktop.NetworkManager.Settings.Connection"
)
func (b *NetworkManagerBackend) startSignalPump() error { func (b *NetworkManagerBackend) startSignalPump() error {
conn, err := dbus.ConnectSystemBus() conn, err := dbus.ConnectSystemBus()
if err != nil { if err != nil {
@@ -33,8 +27,8 @@ func (b *NetworkManagerBackend) startSignalPump() error {
} }
if err := conn.AddMatchSignal( if err := conn.AddMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMSettingsPath)), dbus.WithMatchObjectPath(dbus.ObjectPath("/org/freedesktop/NetworkManager/Settings")),
dbus.WithMatchInterface(dbusNMSettingsInterface), dbus.WithMatchInterface("org.freedesktop.NetworkManager.Settings"),
dbus.WithMatchMember("NewConnection"), dbus.WithMatchMember("NewConnection"),
); err != nil { ); err != nil {
conn.RemoveMatchSignal( conn.RemoveMatchSignal(
@@ -48,8 +42,8 @@ func (b *NetworkManagerBackend) startSignalPump() error {
} }
if err := conn.AddMatchSignal( if err := conn.AddMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMSettingsPath)), dbus.WithMatchObjectPath(dbus.ObjectPath("/org/freedesktop/NetworkManager/Settings")),
dbus.WithMatchInterface(dbusNMSettingsInterface), dbus.WithMatchInterface("org.freedesktop.NetworkManager.Settings"),
dbus.WithMatchMember("ConnectionRemoved"), dbus.WithMatchMember("ConnectionRemoved"),
); err != nil { ); err != nil {
conn.RemoveMatchSignal( conn.RemoveMatchSignal(
@@ -58,8 +52,8 @@ func (b *NetworkManagerBackend) startSignalPump() error {
dbus.WithMatchMember("PropertiesChanged"), dbus.WithMatchMember("PropertiesChanged"),
) )
conn.RemoveMatchSignal( conn.RemoveMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMSettingsPath)), dbus.WithMatchObjectPath(dbus.ObjectPath("/org/freedesktop/NetworkManager/Settings")),
dbus.WithMatchInterface(dbusNMSettingsInterface), dbus.WithMatchInterface("org.freedesktop.NetworkManager.Settings"),
dbus.WithMatchMember("NewConnection"), dbus.WithMatchMember("NewConnection"),
) )
conn.RemoveSignal(signals) conn.RemoveSignal(signals)
@@ -67,31 +61,6 @@ func (b *NetworkManagerBackend) startSignalPump() error {
return err return err
} }
if err := conn.AddMatchSignal(
dbus.WithMatchPathNamespace(dbus.ObjectPath(dbusNMSettingsPath)),
dbus.WithMatchInterface(dbusNMSettingsConnectionInterface),
dbus.WithMatchMember("Updated"),
); err != nil {
conn.RemoveMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMPath)),
dbus.WithMatchInterface(dbusPropsInterface),
dbus.WithMatchMember("PropertiesChanged"),
)
conn.RemoveMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMSettingsPath)),
dbus.WithMatchInterface(dbusNMSettingsInterface),
dbus.WithMatchMember("NewConnection"),
)
conn.RemoveMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMSettingsPath)),
dbus.WithMatchInterface(dbusNMSettingsInterface),
dbus.WithMatchMember("ConnectionRemoved"),
)
conn.RemoveSignal(signals)
conn.Close()
return err
}
if err := conn.AddMatchSignal( if err := conn.AddMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMPath)), dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMPath)),
dbus.WithMatchInterface(dbusNMInterface), dbus.WithMatchInterface(dbusNMInterface),
@@ -168,32 +137,6 @@ func (b *NetworkManagerBackend) stopSignalPump() {
dbus.WithMatchMember("PropertiesChanged"), dbus.WithMatchMember("PropertiesChanged"),
) )
b.dbusConn.RemoveMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMSettingsPath)),
dbus.WithMatchInterface(dbusNMSettingsInterface),
dbus.WithMatchMember("NewConnection"),
)
b.dbusConn.RemoveMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMSettingsPath)),
dbus.WithMatchInterface(dbusNMSettingsInterface),
dbus.WithMatchMember("ConnectionRemoved"),
)
b.dbusConn.RemoveMatchSignal(
dbus.WithMatchPathNamespace(dbus.ObjectPath(dbusNMSettingsPath)),
dbus.WithMatchInterface(dbusNMSettingsConnectionInterface),
dbus.WithMatchMember("Updated"),
)
b.dbusConn.RemoveMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMPath)),
dbus.WithMatchInterface(dbusNMInterface),
dbus.WithMatchMember("DeviceAdded"),
)
b.dbusConn.RemoveMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(dbusNMPath)),
dbus.WithMatchInterface(dbusNMInterface),
dbus.WithMatchMember("DeviceRemoved"),
)
for _, info := range b.wifiDevices { for _, info := range b.wifiDevices {
b.dbusConn.RemoveMatchSignal( b.dbusConn.RemoveMatchSignal(
dbus.WithMatchObjectPath(dbus.ObjectPath(info.device.GetPath())), dbus.WithMatchObjectPath(dbus.ObjectPath(info.device.GetPath())),
@@ -221,13 +164,9 @@ func (b *NetworkManagerBackend) stopSignalPump() {
} }
func (b *NetworkManagerBackend) handleDBusSignal(sig *dbus.Signal) { func (b *NetworkManagerBackend) handleDBusSignal(sig *dbus.Signal) {
if sig.Name == dbusNMSettingsInterface+".NewConnection" || if sig.Name == "org.freedesktop.NetworkManager.Settings.NewConnection" ||
sig.Name == dbusNMSettingsInterface+".ConnectionRemoved" || sig.Name == "org.freedesktop.NetworkManager.Settings.ConnectionRemoved" {
sig.Name == dbusNMSettingsConnectionInterface+".Updated" {
b.ListVPNProfiles() b.ListVPNProfiles()
if err := b.updateSavedWiFiNetworks(); err != nil {
b.updateWiFiNetworks()
}
if b.onStateChange != nil { if b.onStateChange != nil {
b.onStateChange() b.onStateChange()
} }
@@ -225,14 +225,24 @@ func (b *NetworkManagerBackend) GetWiFiQRCodeContent(ssid string) (string, error
return "", fmt.Errorf("failed to identify security type of network `%s`", ssid) return "", fmt.Errorf("failed to identify security type of network `%s`", ssid)
} }
var securityType string
switch keyMgmt { switch keyMgmt {
case "none": case "none":
return "", fmt.Errorf("QR code generation only supports WPA-PSK connections, `%s` is open or WEP", ssid) authAlg, _ := secSettings["auth-alg"].(string)
switch authAlg {
case "open":
securityType = "nopass"
default:
securityType = "WEP"
}
case "ieee8021x": case "ieee8021x":
return "", fmt.Errorf("QR code generation only supports WPA-PSK connections, `%s` is enterprise", ssid) securityType = "WEP"
case "wpa-psk", "sae", "wpa-psk-sae":
default: default:
return "", fmt.Errorf("QR code generation only supports WPA-PSK connections, `%s` uses %s", ssid, keyMgmt) securityType = "WPA"
}
if securityType != "WPA" {
return "", fmt.Errorf("QR code generation only supports WPA connections, `%s` uses %s", ssid, securityType)
} }
var psk string var psk string
@@ -266,7 +276,7 @@ func (b *NetworkManagerBackend) GetWiFiQRCodeContent(ssid string) (string, error
return "", fmt.Errorf("failed to retrieve password for `%s`", ssid) return "", fmt.Errorf("failed to retrieve password for `%s`", ssid)
} }
return FormatWiFiQRString("WPA", ssid, psk), nil return FormatWiFiQRString(securityType, ssid, psk), nil
} }
func (b *NetworkManagerBackend) ConnectWiFi(req ConnectionRequest) error { func (b *NetworkManagerBackend) ConnectWiFi(req ConnectionRequest) error {
@@ -395,74 +405,6 @@ func (b *NetworkManagerBackend) ForgetWiFiNetwork(ssid string) error {
return nil return nil
} }
func getSavedWiFiProfiles(connections []gonetworkmanager.Connection) map[string]savedWiFiProfile {
profiles := make(map[string]savedWiFiProfile)
for _, conn := range connections {
connSettings, err := conn.GetSettings()
if err != nil {
continue
}
connMeta, ok := connSettings["connection"]
if !ok {
continue
}
connType, ok := connMeta["type"].(string)
if !ok || connType != "802-11-wireless" {
continue
}
wifiSettings, ok := connSettings["802-11-wireless"]
if !ok {
continue
}
ssidBytes, ok := wifiSettings["ssid"].([]byte)
if !ok || len(ssidBytes) == 0 {
continue
}
ssid := string(ssidBytes)
profile := savedWiFiProfile{
Autoconnect: true,
Mode: "infrastructure",
}
if ac, ok := connMeta["autoconnect"].(bool); ok {
profile.Autoconnect = ac
}
if hidden, ok := wifiSettings["hidden"].(bool); ok {
profile.Hidden = hidden
}
if mode, ok := wifiSettings["mode"].(string); ok && mode != "" {
profile.Mode = mode
}
if _, ok := connSettings["802-11-wireless-security"]; ok {
profile.Secured = true
}
if _, ok := connSettings["802-1x"]; ok {
profile.Enterprise = true
profile.Secured = true
}
if existing, ok := profiles[ssid]; ok {
profile.Autoconnect = profile.Autoconnect || existing.Autoconnect
profile.Hidden = profile.Hidden || existing.Hidden
profile.Secured = profile.Secured || existing.Secured
profile.Enterprise = profile.Enterprise || existing.Enterprise
if profile.Mode == "" {
profile.Mode = existing.Mode
}
}
profiles[ssid] = profile
}
return profiles
}
func (b *NetworkManagerBackend) IsConnectingTo(ssid string) bool { func (b *NetworkManagerBackend) IsConnectingTo(ssid string) bool {
b.stateMutex.RLock() b.stateMutex.RLock()
defer b.stateMutex.RUnlock() defer b.stateMutex.RUnlock()
@@ -500,7 +442,47 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
return nil, fmt.Errorf("failed to get connections: %w", err) return nil, fmt.Errorf("failed to get connections: %w", err)
} }
savedProfiles := getSavedWiFiProfiles(connections) savedSSIDs := make(map[string]bool)
autoconnectMap := make(map[string]bool)
hiddenSSIDs := make(map[string]bool)
for _, conn := range connections {
connSettings, err := conn.GetSettings()
if err != nil {
continue
}
connMeta, ok := connSettings["connection"]
if !ok {
continue
}
connType, ok := connMeta["type"].(string)
if !ok || connType != "802-11-wireless" {
continue
}
wifiSettings, ok := connSettings["802-11-wireless"]
if !ok {
continue
}
ssidBytes, ok := wifiSettings["ssid"].([]byte)
if !ok {
continue
}
ssid := string(ssidBytes)
savedSSIDs[ssid] = true
autoconnect := true
if ac, ok := connMeta["autoconnect"].(bool); ok {
autoconnect = ac
}
autoconnectMap[ssid] = autoconnect
if hidden, ok := wifiSettings["hidden"].(bool); ok && hidden {
hiddenSSIDs[ssid] = true
}
}
b.stateMutex.RLock() b.stateMutex.RLock()
currentSSID := b.state.WiFiSSID currentSSID := b.state.WiFiSSID
@@ -509,8 +491,8 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
wifiBSSID := b.state.WiFiBSSID wifiBSSID := b.state.WiFiBSSID
b.stateMutex.RUnlock() b.stateMutex.RUnlock()
seenSSIDs := make(map[string]int) seenSSIDs := make(map[string]*WiFiNetwork)
networks := make([]WiFiNetwork, 0, len(apPaths)+1) networks := []WiFiNetwork{}
for _, ap := range apPaths { for _, ap := range apPaths {
ssid, err := ap.GetPropertySSID() ssid, err := ap.GetPropertySSID()
@@ -518,8 +500,7 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
continue continue
} }
if existingIndex, exists := seenSSIDs[ssid]; exists { if existing, exists := seenSSIDs[ssid]; exists {
existing := &networks[existingIndex]
strength, _ := ap.GetPropertyStrength() strength, _ := ap.GetPropertyStrength()
if strength > existing.Signal { if strength > existing.Signal {
existing.Signal = strength existing.Signal = strength
@@ -569,7 +550,6 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
} }
} }
profile, saved := savedProfiles[ssid]
network := WiFiNetwork{ network := WiFiNetwork{
SSID: ssid, SSID: ssid,
BSSID: bssid, BSSID: bssid,
@@ -577,86 +557,45 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
Secured: secured, Secured: secured,
Enterprise: enterprise, Enterprise: enterprise,
Connected: isConnected, Connected: isConnected,
Saved: saved, Saved: savedSSIDs[ssid],
Autoconnect: profile.Autoconnect, Autoconnect: autoconnectMap[ssid],
Hidden: profile.Hidden, Hidden: hiddenSSIDs[ssid],
Frequency: freq, Frequency: freq,
Mode: modeStr, Mode: modeStr,
Rate: rate, Rate: rate,
Channel: channel, Channel: channel,
} }
seenSSIDs[ssid] = &network
networks = append(networks, network) networks = append(networks, network)
seenSSIDs[ssid] = len(networks) - 1
} }
if wifiConnected && currentSSID != "" { if wifiConnected && currentSSID != "" {
if _, exists := seenSSIDs[currentSSID]; !exists { if _, exists := seenSSIDs[currentSSID]; !exists {
profile, saved := savedProfiles[currentSSID]
hiddenNetwork := WiFiNetwork{ hiddenNetwork := WiFiNetwork{
SSID: currentSSID, SSID: currentSSID,
BSSID: wifiBSSID, BSSID: wifiBSSID,
Signal: wifiSignal, Signal: wifiSignal,
Secured: true, Secured: true,
Connected: true, Connected: true,
Saved: saved, Saved: savedSSIDs[currentSSID],
Autoconnect: profile.Autoconnect, Autoconnect: autoconnectMap[currentSSID],
Hidden: true, Hidden: true,
Mode: "infrastructure", Mode: "infrastructure",
} }
networks = append(networks, hiddenNetwork) networks = append(networks, hiddenNetwork)
seenSSIDs[currentSSID] = len(networks) - 1
} }
} }
visibleNetworks := wiFiNetworksBySSID(networks, true)
savedNetworks := savedWiFiNetworksFromProfiles(savedProfiles, visibleNetworks, currentSSID, wifiConnected)
sortWiFiNetworks(networks) sortWiFiNetworks(networks)
b.stateMutex.Lock() b.stateMutex.Lock()
b.state.WiFiNetworks = networks b.state.WiFiNetworks = networks
b.state.SavedWiFiNetworks = savedNetworks
b.stateMutex.Unlock() b.stateMutex.Unlock()
return networks, nil return networks, nil
} }
func (b *NetworkManagerBackend) updateSavedWiFiNetworks() error {
s := b.settings
if s == nil {
var err error
s, err = gonetworkmanager.NewSettings()
if err != nil {
return fmt.Errorf("failed to get settings: %w", err)
}
b.settings = s
}
settingsMgr := s.(gonetworkmanager.Settings)
connections, err := settingsMgr.ListConnections()
if err != nil {
return fmt.Errorf("failed to get connections: %w", err)
}
savedProfiles := getSavedWiFiProfiles(connections)
b.stateMutex.RLock()
currentSSID := b.state.WiFiSSID
wifiConnected := b.state.WiFiConnected
wifiNetworks := append([]WiFiNetwork(nil), b.state.WiFiNetworks...)
b.stateMutex.RUnlock()
wifiNetworks, savedNetworks := refreshSavedWiFiState(wifiNetworks, savedProfiles, currentSSID, wifiConnected)
b.stateMutex.Lock()
b.state.WiFiNetworks = wifiNetworks
b.state.SavedWiFiNetworks = savedNetworks
b.stateMutex.Unlock()
return nil
}
func (b *NetworkManagerBackend) findConnection(ssid string) (gonetworkmanager.Connection, error) { func (b *NetworkManagerBackend) findConnection(ssid string) (gonetworkmanager.Connection, error) {
s := b.settings s := b.settings
if s == nil { if s == nil {
@@ -1036,14 +975,49 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
return return
} }
savedProfiles := getSavedWiFiProfiles(connections) savedSSIDs := make(map[string]bool)
autoconnectMap := make(map[string]bool)
hiddenSSIDs := make(map[string]bool)
for _, conn := range connections {
connSettings, err := conn.GetSettings()
if err != nil {
continue
}
connMeta, ok := connSettings["connection"]
if !ok {
continue
}
connType, ok := connMeta["type"].(string)
if !ok || connType != "802-11-wireless" {
continue
}
wifiSettings, ok := connSettings["802-11-wireless"]
if !ok {
continue
}
ssidBytes, ok := wifiSettings["ssid"].([]byte)
if !ok {
continue
}
ssid := string(ssidBytes)
savedSSIDs[ssid] = true
autoconnect := true
if ac, ok := connMeta["autoconnect"].(bool); ok {
autoconnect = ac
}
autoconnectMap[ssid] = autoconnect
if hidden, ok := wifiSettings["hidden"].(bool); ok && hidden {
hiddenSSIDs[ssid] = true
}
}
var devices []WiFiDevice var devices []WiFiDevice
visibleNetworks := make(map[string]WiFiNetwork)
b.stateMutex.RLock()
currentSSID := b.state.WiFiSSID
wifiConnected := b.state.WiFiConnected
b.stateMutex.RUnlock()
for name, devInfo := range b.wifiDevices { for name, devInfo := range b.wifiDevices {
state, _ := devInfo.device.GetPropertyState() state, _ := devInfo.device.GetPropertyState()
@@ -1076,16 +1050,14 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
apPaths, err := devInfo.wireless.GetAccessPoints() apPaths, err := devInfo.wireless.GetAccessPoints()
var networks []WiFiNetwork var networks []WiFiNetwork
if err == nil { if err == nil {
seenSSIDs := make(map[string]int) seenSSIDs := make(map[string]*WiFiNetwork)
networks = make([]WiFiNetwork, 0, len(apPaths)+1)
for _, ap := range apPaths { for _, ap := range apPaths {
apSSID, err := ap.GetPropertySSID() apSSID, err := ap.GetPropertySSID()
if err != nil || apSSID == "" { if err != nil || apSSID == "" {
continue continue
} }
if existingIndex, exists := seenSSIDs[apSSID]; exists { if existing, exists := seenSSIDs[apSSID]; exists {
existing := &networks[existingIndex]
strength, _ := ap.GetPropertyStrength() strength, _ := ap.GetPropertyStrength()
if strength > existing.Signal { if strength > existing.Signal {
existing.Signal = strength existing.Signal = strength
@@ -1135,7 +1107,6 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
} }
} }
profile, saved := savedProfiles[apSSID]
network := WiFiNetwork{ network := WiFiNetwork{
SSID: apSSID, SSID: apSSID,
BSSID: apBSSID, BSSID: apBSSID,
@@ -1143,9 +1114,9 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
Secured: secured, Secured: secured,
Enterprise: enterprise, Enterprise: enterprise,
Connected: isConnected, Connected: isConnected,
Saved: saved, Saved: savedSSIDs[apSSID],
Autoconnect: profile.Autoconnect, Autoconnect: autoconnectMap[apSSID],
Hidden: profile.Hidden, Hidden: hiddenSSIDs[apSSID],
Frequency: freq, Frequency: freq,
Mode: modeStr, Mode: modeStr,
Rate: rate, Rate: rate,
@@ -1153,31 +1124,25 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
Device: name, Device: name,
} }
seenSSIDs[apSSID] = &network
networks = append(networks, network) networks = append(networks, network)
seenSSIDs[apSSID] = len(networks) - 1
if existing, ok := visibleNetworks[apSSID]; !ok || network.Signal > existing.Signal {
visibleNetworks[apSSID] = network
}
} }
if connected && ssid != "" { if connected && ssid != "" {
if _, exists := seenSSIDs[ssid]; !exists { if _, exists := seenSSIDs[ssid]; !exists {
profile, saved := savedProfiles[ssid]
hiddenNetwork := WiFiNetwork{ hiddenNetwork := WiFiNetwork{
SSID: ssid, SSID: ssid,
BSSID: bssid, BSSID: bssid,
Signal: signal, Signal: signal,
Secured: true, Secured: true,
Connected: true, Connected: true,
Saved: saved, Saved: savedSSIDs[ssid],
Autoconnect: profile.Autoconnect, Autoconnect: autoconnectMap[ssid],
Hidden: true, Hidden: true,
Mode: "infrastructure", Mode: "infrastructure",
Device: name, Device: name,
} }
networks = append(networks, hiddenNetwork) networks = append(networks, hiddenNetwork)
seenSSIDs[ssid] = len(networks) - 1
visibleNetworks[ssid] = hiddenNetwork
} }
} }
@@ -1203,7 +1168,6 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
b.stateMutex.Lock() b.stateMutex.Lock()
b.state.WiFiDevices = devices b.state.WiFiDevices = devices
b.state.SavedWiFiNetworks = savedWiFiNetworksFromProfiles(savedProfiles, visibleNetworks, currentSSID, wifiConnected)
b.stateMutex.Unlock() b.stateMutex.Unlock()
} }
@@ -4,7 +4,6 @@ import (
"testing" "testing"
mock_gonetworkmanager "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/Wifx/gonetworkmanager/v2" mock_gonetworkmanager "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/github.com/Wifx/gonetworkmanager/v2"
"github.com/Wifx/gonetworkmanager/v2"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -177,54 +176,6 @@ func TestNetworkManagerBackend_UpdateWiFiNetworks_NoDevice(t *testing.T) {
assert.Contains(t, err.Error(), "no WiFi device available") assert.Contains(t, err.Error(), "no WiFi device available")
} }
func TestNetworkManagerBackend_UpdateSavedWiFiNetworksPreservesVisibleSavedNetworks(t *testing.T) {
mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
mockSettings := mock_gonetworkmanager.NewMockSettings(t)
mockConn := mock_gonetworkmanager.NewMockConnection(t)
backend, err := NewNetworkManagerBackend(mockNM)
assert.NoError(t, err)
backend.settings = mockSettings
backend.stateMutex.Lock()
backend.state.WiFiNetworks = []WiFiNetwork{
{
SSID: "Home",
Signal: 76,
},
}
backend.stateMutex.Unlock()
settings := gonetworkmanager.ConnectionSettings{
"connection": {
"type": "802-11-wireless",
"autoconnect": true,
},
"802-11-wireless": {
"ssid": []byte("Home"),
},
"802-11-wireless-security": {},
}
mockSettings.EXPECT().ListConnections().Return([]gonetworkmanager.Connection{mockConn}, nil)
mockConn.EXPECT().GetSettings().Return(settings, nil)
err = backend.updateSavedWiFiNetworks()
assert.NoError(t, err)
backend.stateMutex.RLock()
savedNetworks := append([]WiFiNetwork(nil), backend.state.SavedWiFiNetworks...)
wifiNetworks := append([]WiFiNetwork(nil), backend.state.WiFiNetworks...)
backend.stateMutex.RUnlock()
assert.Len(t, wifiNetworks, 1)
assert.True(t, wifiNetworks[0].Saved)
assert.Len(t, savedNetworks, 1)
assert.Equal(t, "Home", savedNetworks[0].SSID)
assert.True(t, savedNetworks[0].Saved)
assert.False(t, savedNetworks[0].OutOfRange)
assert.Equal(t, uint8(76), savedNetworks[0].Signal)
}
func TestNetworkManagerBackend_FindConnection_NoSettings(t *testing.T) { func TestNetworkManagerBackend_FindConnection_NoSettings(t *testing.T) {
mockNM := mock_gonetworkmanager.NewMockNetworkManager(t) mockNM := mock_gonetworkmanager.NewMockNetworkManager(t)
+3 -26
View File
@@ -64,10 +64,9 @@ func NewManager() (*Manager, error) {
m := &Manager{ m := &Manager{
backend: backend, backend: backend,
state: &NetworkState{ state: &NetworkState{
NetworkStatus: StatusDisconnected, NetworkStatus: StatusDisconnected,
Preference: PreferenceAuto, Preference: PreferenceAuto,
WiFiNetworks: []WiFiNetwork{}, WiFiNetworks: []WiFiNetwork{},
SavedWiFiNetworks: []WiFiNetwork{},
}, },
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
@@ -121,7 +120,6 @@ func (m *Manager) syncStateFromBackend() error {
m.state.WiFiBSSID = backendState.WiFiBSSID m.state.WiFiBSSID = backendState.WiFiBSSID
m.state.WiFiSignal = backendState.WiFiSignal m.state.WiFiSignal = backendState.WiFiSignal
m.state.WiFiNetworks = backendState.WiFiNetworks m.state.WiFiNetworks = backendState.WiFiNetworks
m.state.SavedWiFiNetworks = backendState.SavedWiFiNetworks
m.state.WiFiDevices = backendState.WiFiDevices m.state.WiFiDevices = backendState.WiFiDevices
m.state.WiredConnections = backendState.WiredConnections m.state.WiredConnections = backendState.WiredConnections
m.state.VPNProfiles = backendState.VPNProfiles m.state.VPNProfiles = backendState.VPNProfiles
@@ -158,7 +156,6 @@ func (m *Manager) snapshotState() NetworkState {
defer m.stateMutex.RUnlock() defer m.stateMutex.RUnlock()
s := *m.state s := *m.state
s.WiFiNetworks = append([]WiFiNetwork(nil), m.state.WiFiNetworks...) s.WiFiNetworks = append([]WiFiNetwork(nil), m.state.WiFiNetworks...)
s.SavedWiFiNetworks = append([]WiFiNetwork(nil), m.state.SavedWiFiNetworks...)
s.WiFiDevices = append([]WiFiDevice(nil), m.state.WiFiDevices...) s.WiFiDevices = append([]WiFiDevice(nil), m.state.WiFiDevices...)
s.WiredConnections = append([]WiredConnection(nil), m.state.WiredConnections...) s.WiredConnections = append([]WiredConnection(nil), m.state.WiredConnections...)
s.EthernetDevices = append([]EthernetDevice(nil), m.state.EthernetDevices...) s.EthernetDevices = append([]EthernetDevice(nil), m.state.EthernetDevices...)
@@ -214,9 +211,6 @@ func stateChangedMeaningfully(old, new *NetworkState) bool {
if len(old.WiFiNetworks) != len(new.WiFiNetworks) { if len(old.WiFiNetworks) != len(new.WiFiNetworks) {
return true return true
} }
if len(old.SavedWiFiNetworks) != len(new.SavedWiFiNetworks) {
return true
}
if len(old.WiFiDevices) != len(new.WiFiDevices) { if len(old.WiFiDevices) != len(new.WiFiDevices) {
return true return true
} }
@@ -244,23 +238,6 @@ func stateChangedMeaningfully(old, new *NetworkState) bool {
} }
} }
for i := range old.SavedWiFiNetworks {
oldNet := &old.SavedWiFiNetworks[i]
newNet := &new.SavedWiFiNetworks[i]
if oldNet.SSID != newNet.SSID {
return true
}
if oldNet.Connected != newNet.Connected {
return true
}
if oldNet.Autoconnect != newNet.Autoconnect {
return true
}
if oldNet.OutOfRange != newNet.OutOfRange {
return true
}
}
for i := range old.WiredConnections { for i := range old.WiredConnections {
oldNet := &old.WiredConnections[i] oldNet := &old.WiredConnections[i]
newNet := &new.WiredConnections[i] newNet := &new.WiredConnections[i]
-2
View File
@@ -34,7 +34,6 @@ type WiFiNetwork struct {
Saved bool `json:"saved"` Saved bool `json:"saved"`
Autoconnect bool `json:"autoconnect"` Autoconnect bool `json:"autoconnect"`
Hidden bool `json:"hidden"` Hidden bool `json:"hidden"`
OutOfRange bool `json:"outOfRange"`
Frequency uint32 `json:"frequency"` Frequency uint32 `json:"frequency"`
Mode string `json:"mode"` Mode string `json:"mode"`
Rate uint32 `json:"rate"` Rate uint32 `json:"rate"`
@@ -112,7 +111,6 @@ type NetworkState struct {
WiFiBSSID string `json:"wifiBSSID"` WiFiBSSID string `json:"wifiBSSID"`
WiFiSignal uint8 `json:"wifiSignal"` WiFiSignal uint8 `json:"wifiSignal"`
WiFiNetworks []WiFiNetwork `json:"wifiNetworks"` WiFiNetworks []WiFiNetwork `json:"wifiNetworks"`
SavedWiFiNetworks []WiFiNetwork `json:"savedWifiNetworks"`
WiFiDevices []WiFiDevice `json:"wifiDevices"` WiFiDevices []WiFiDevice `json:"wifiDevices"`
WiredConnections []WiredConnection `json:"wiredConnections"` WiredConnections []WiredConnection `json:"wiredConnections"`
VPNProfiles []VPNProfile `json:"vpnProfiles"` VPNProfiles []VPNProfile `json:"vpnProfiles"`
-103
View File
@@ -1,103 +0,0 @@
package network
import "sort"
type savedWiFiProfile struct {
Autoconnect bool
Hidden bool
Secured bool
Enterprise bool
Mode string
}
// Saved WiFi state is keyed by SSID because the UI/API accepts SSID actions.
// Multiple backend profiles for the same SSID are intentionally collapsed here.
func mergeSavedProfilesIntoWiFiNetworks(networks []WiFiNetwork, profiles map[string]savedWiFiProfile, currentSSID string, wifiConnected bool) []WiFiNetwork {
merged := make([]WiFiNetwork, len(networks))
for i, network := range networks {
profile, saved := profiles[network.SSID]
network.Connected = wifiConnected && network.SSID == currentSSID
network.Saved = saved
if saved {
network.Autoconnect = profile.Autoconnect
network.Hidden = network.Hidden || profile.Hidden
network.Secured = network.Secured || profile.Secured
network.Enterprise = network.Enterprise || profile.Enterprise
if network.Mode == "" {
network.Mode = profile.Mode
}
} else {
network.Autoconnect = false
}
merged[i] = network
}
return merged
}
func wiFiNetworksBySSID(networks []WiFiNetwork, visibleOnly bool) map[string]WiFiNetwork {
visible := make(map[string]WiFiNetwork, len(networks))
for _, network := range networks {
if visibleOnly && network.OutOfRange {
continue
}
visible[network.SSID] = network
}
return visible
}
func refreshSavedWiFiState(networks []WiFiNetwork, profiles map[string]savedWiFiProfile, currentSSID string, wifiConnected bool) ([]WiFiNetwork, []WiFiNetwork) {
mergedNetworks := mergeSavedProfilesIntoWiFiNetworks(networks, profiles, currentSSID, wifiConnected)
visibleNetworks := wiFiNetworksBySSID(mergedNetworks, true)
savedNetworks := savedWiFiNetworksFromProfiles(profiles, visibleNetworks, currentSSID, wifiConnected)
return mergedNetworks, savedNetworks
}
func savedWiFiNetworksFromProfiles(profiles map[string]savedWiFiProfile, visible map[string]WiFiNetwork, currentSSID string, wifiConnected bool) []WiFiNetwork {
networks := make([]WiFiNetwork, 0, len(profiles))
for ssid, profile := range profiles {
if network, ok := visible[ssid]; ok {
network.Saved = true
network.Autoconnect = profile.Autoconnect
network.Hidden = network.Hidden || profile.Hidden
network.Secured = network.Secured || profile.Secured
network.Enterprise = network.Enterprise || profile.Enterprise
network.OutOfRange = false
if network.Mode == "" {
network.Mode = profile.Mode
}
networks = append(networks, network)
continue
}
isConnected := wifiConnected && ssid == currentSSID
networks = append(networks, WiFiNetwork{
SSID: ssid,
Secured: profile.Secured,
Enterprise: profile.Enterprise,
Connected: isConnected,
Saved: true,
Autoconnect: profile.Autoconnect,
Hidden: profile.Hidden,
OutOfRange: !isConnected,
Mode: profile.Mode,
})
}
sort.Slice(networks, func(i, j int) bool {
if networks[i].Connected && !networks[j].Connected {
return true
}
if !networks[i].Connected && networks[j].Connected {
return false
}
if networks[i].OutOfRange != networks[j].OutOfRange {
return !networks[i].OutOfRange
}
if networks[i].Signal != networks[j].Signal {
return networks[i].Signal > networks[j].Signal
}
return networks[i].SSID < networks[j].SSID
})
return networks
}
@@ -1,170 +0,0 @@
package network
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestMergeSavedProfilesIntoWiFiNetworks(t *testing.T) {
networks := []WiFiNetwork{
{
SSID: "Home",
Signal: 80,
Secured: false,
Autoconnect: false,
},
{
SSID: "Cafe",
Signal: 50,
Secured: false,
Autoconnect: true,
},
}
profiles := map[string]savedWiFiProfile{
"Home": {
Autoconnect: true,
Hidden: true,
Secured: true,
Mode: "infrastructure",
},
}
merged := mergeSavedProfilesIntoWiFiNetworks(networks, profiles, "Home", true)
assert.Len(t, merged, 2)
assert.Equal(t, "Home", merged[0].SSID)
assert.True(t, merged[0].Connected)
assert.True(t, merged[0].Saved)
assert.True(t, merged[0].Autoconnect)
assert.True(t, merged[0].Hidden)
assert.True(t, merged[0].Secured)
assert.Equal(t, "infrastructure", merged[0].Mode)
assert.Equal(t, "Cafe", merged[1].SSID)
assert.False(t, merged[1].Saved)
assert.False(t, merged[1].Autoconnect)
}
func TestSavedWiFiNetworksFromProfilesOutOfRangeWithoutVisibleNetworks(t *testing.T) {
profiles := map[string]savedWiFiProfile{
"Home": {
Autoconnect: true,
Secured: true,
Mode: "infrastructure",
},
}
networks := savedWiFiNetworksFromProfiles(profiles, nil, "", false)
assert.Len(t, networks, 1)
assert.Equal(t, "Home", networks[0].SSID)
assert.True(t, networks[0].Saved)
assert.True(t, networks[0].OutOfRange)
assert.Equal(t, uint8(0), networks[0].Signal)
}
func TestSavedWiFiNetworksFromProfilesKeepsConnectedCurrentNetworkInRange(t *testing.T) {
profiles := map[string]savedWiFiProfile{
"Home": {
Autoconnect: true,
Secured: true,
},
}
networks := savedWiFiNetworksFromProfiles(profiles, nil, "Home", true)
assert.Len(t, networks, 1)
assert.Equal(t, "Home", networks[0].SSID)
assert.True(t, networks[0].Connected)
assert.False(t, networks[0].OutOfRange)
}
func TestSavedWiFiNetworksFromProfilesIncludesOutOfRange(t *testing.T) {
profiles := map[string]savedWiFiProfile{
"Home": {
Autoconnect: true,
Hidden: true,
Secured: true,
Mode: "infrastructure",
},
"Office": {
Autoconnect: false,
Secured: true,
Enterprise: true,
Mode: "infrastructure",
},
}
visible := map[string]WiFiNetwork{
"Home": {
SSID: "Home",
Signal: 72,
Secured: true,
Connected: true,
},
}
networks := savedWiFiNetworksFromProfiles(profiles, visible, "Home", true)
assert.Len(t, networks, 2)
assert.Equal(t, "Home", networks[0].SSID)
assert.True(t, networks[0].Saved)
assert.True(t, networks[0].Connected)
assert.False(t, networks[0].OutOfRange)
assert.True(t, networks[0].Hidden)
assert.Equal(t, uint8(72), networks[0].Signal)
assert.Equal(t, "Office", networks[1].SSID)
assert.True(t, networks[1].Saved)
assert.False(t, networks[1].Autoconnect)
assert.True(t, networks[1].Enterprise)
assert.True(t, networks[1].OutOfRange)
}
func TestWiFiNetworksBySSIDVisibleOnlySkipsOutOfRange(t *testing.T) {
visible := wiFiNetworksBySSID([]WiFiNetwork{
{SSID: "Home", Signal: 70},
{SSID: "Office", Signal: 0, OutOfRange: true},
}, true)
assert.Contains(t, visible, "Home")
assert.NotContains(t, visible, "Office")
}
func TestRefreshSavedWiFiStatePreservesVisibleSavedNetworks(t *testing.T) {
networks := []WiFiNetwork{
{
SSID: "Home",
Signal: 82,
},
}
profiles := map[string]savedWiFiProfile{
"Home": {
Autoconnect: true,
Secured: true,
Mode: "infrastructure",
},
"Office": {
Autoconnect: false,
Secured: true,
Mode: "infrastructure",
},
}
mergedNetworks, savedNetworks := refreshSavedWiFiState(networks, profiles, "", false)
assert.Len(t, mergedNetworks, 1)
assert.Equal(t, "Home", mergedNetworks[0].SSID)
assert.True(t, mergedNetworks[0].Saved)
assert.True(t, mergedNetworks[0].Autoconnect)
assert.Len(t, savedNetworks, 2)
assert.Equal(t, "Home", savedNetworks[0].SSID)
assert.True(t, savedNetworks[0].Saved)
assert.False(t, savedNetworks[0].OutOfRange)
assert.Equal(t, uint8(82), savedNetworks[0].Signal)
assert.Equal(t, "Office", savedNetworks[1].SSID)
assert.True(t, savedNetworks[1].Saved)
assert.True(t, savedNetworks[1].OutOfRange)
}
+1 -1
View File
@@ -38,7 +38,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
const APIVersion = 26 const APIVersion = 25
var CLIVersion = "dev" var CLIVersion = "dev"
+10 -11
View File
@@ -66,17 +66,16 @@ func convertPeerStatus(ps *ipnstate.PeerStatus, users map[tailcfg.UserID]tailcfg
} }
peer := Peer{ peer := Peer{
ID: string(ps.ID), ID: string(ps.ID),
Hostname: hostname, Hostname: hostname,
DNSName: dnsName, DNSName: dnsName,
OS: ps.OS, OS: ps.OS,
Online: ps.Online, Online: ps.Online,
Active: ps.Active, Active: ps.Active,
ExitNode: ps.ExitNode, ExitNode: ps.ExitNode,
ExitNodeOption: ps.ExitNodeOption, Relay: ps.Relay,
Relay: ps.Relay, RxBytes: ps.RxBytes,
RxBytes: ps.RxBytes, TxBytes: ps.TxBytes,
TxBytes: ps.TxBytes,
} }
for _, ip := range ps.TailscaleIPs { for _, ip := range ps.TailscaleIPs {
@@ -14,14 +14,6 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
handleGetStatus(conn, req, manager) handleGetStatus(conn, req, manager)
case "tailscale.refresh": case "tailscale.refresh":
handleRefresh(conn, req, manager) handleRefresh(conn, req, manager)
case "tailscale.connect":
handleConnect(conn, req, manager)
case "tailscale.disconnect":
handleDisconnect(conn, req, manager)
case "tailscale.setExitNode":
handleSetExitNode(conn, req, manager)
case "tailscale.setAllowLanAccess":
handleSetAllowLanAccess(conn, req, manager)
default: default:
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method)) models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
} }
@@ -36,37 +28,3 @@ func handleRefresh(conn net.Conn, req models.Request, manager *Manager) {
manager.RefreshState() manager.RefreshState()
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "refreshed"}) models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "refreshed"})
} }
func handleConnect(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.Connect(); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "connected"})
}
func handleDisconnect(conn net.Conn, req models.Request, manager *Manager) {
if err := manager.Disconnect(); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "disconnected"})
}
func handleSetExitNode(conn net.Conn, req models.Request, manager *Manager) {
id := models.GetOr(req, "id", "")
if err := manager.SetExitNode(id); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "exit node updated"})
}
func handleSetAllowLanAccess(conn net.Conn, req models.Request, manager *Manager) {
enabled := models.GetOr(req, "enabled", false)
if err := manager.SetAllowLANAccess(enabled); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "lan access updated"})
}
@@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"net" "net"
"testing" "testing"
"time" "time"
@@ -79,63 +78,6 @@ func TestHandleRefresh(t *testing.T) {
assert.True(t, resp.Result.Success) assert.True(t, resp.Result.Success)
} }
func TestHandleActions(t *testing.T) {
cases := []struct {
name string
method string
params map[string]any
}{
{"connect", "tailscale.connect", nil},
{"disconnect", "tailscale.disconnect", nil},
{"setExitNode", "tailscale.setExitNode", map[string]any{"id": "nABC123"}},
{"clearExitNode", "tailscale.setExitNode", map[string]any{"id": ""}},
{"setAllowLanAccess", "tailscale.setAllowLanAccess", map[string]any{"enabled": true}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
m := handlerTestManager()
defer m.Close()
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{ID: 1, Method: tc.method, Params: tc.params}
HandleRequest(conn, req, m)
var resp models.Response[models.SuccessResult]
require.NoError(t, json.NewDecoder(buf).Decode(&resp))
assert.Equal(t, 1, resp.ID)
assert.Empty(t, resp.Error)
require.NotNil(t, resp.Result)
assert.True(t, resp.Result.Success)
})
}
}
func TestHandleAction_BackendError(t *testing.T) {
client := &mockClient{
watchFn: blockingWatch,
statusFn: func(ctx context.Context) (*ipnstate.Status, error) { return runningStatus(), nil },
editPrefsFn: func(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
return nil, fmt.Errorf("backend rejected edit")
},
}
m := newManager(client)
defer m.Close()
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{ID: 1, Method: "tailscale.connect"}
HandleRequest(conn, req, m)
var resp models.Response[models.SuccessResult]
require.NoError(t, json.NewDecoder(buf).Decode(&resp))
assert.Nil(t, resp.Result)
assert.Contains(t, resp.Error, "backend rejected edit")
}
func TestHandleRequest_UnknownMethod(t *testing.T) { func TestHandleRequest_UnknownMethod(t *testing.T) {
m := handlerTestManager() m := handlerTestManager()
defer m.Close() defer m.Close()
+4 -85
View File
@@ -11,7 +11,6 @@ import (
"tailscale.com/client/local" "tailscale.com/client/local"
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/ipn/ipnstate" "tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
) )
const ( const (
@@ -23,8 +22,6 @@ const (
type tailscaleClient interface { type tailscaleClient interface {
WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (ipnBusWatcher, error) WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (ipnBusWatcher, error)
Status(ctx context.Context) (*ipnstate.Status, error) Status(ctx context.Context) (*ipnstate.Status, error)
GetPrefs(ctx context.Context) (*ipn.Prefs, error)
EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error)
} }
// ipnBusWatcher abstracts the IPN bus watcher for testing. // ipnBusWatcher abstracts the IPN bus watcher for testing.
@@ -46,14 +43,6 @@ func (w *localClientWrapper) Status(ctx context.Context) (*ipnstate.Status, erro
return w.client.Status(ctx) return w.client.Status(ctx)
} }
func (w *localClientWrapper) GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
return w.client.GetPrefs(ctx)
}
func (w *localClientWrapper) EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
return w.client.EditPrefs(ctx, mp)
}
// Manager manages Tailscale state via IPN bus events and subscriber notifications. // Manager manages Tailscale state via IPN bus events and subscriber notifications.
type Manager struct { type Manager struct {
state *TailscaleState state *TailscaleState
@@ -180,34 +169,14 @@ func (m *Manager) fetchAndBroadcast(ctx context.Context) {
statusCtx, cancel := context.WithTimeout(ctx, statusTimeout) statusCtx, cancel := context.WithTimeout(ctx, statusTimeout)
defer cancel() defer cancel()
state, err := m.fetchState(statusCtx) status, err := m.client.Status(statusCtx)
if err != nil { if err != nil {
log.Warnf("[Tailscale] Failed to fetch status: %v", err) log.Warnf("[Tailscale] Failed to fetch status: %v", err)
return return
} }
m.updateState(state)
}
// fetchState fetches the current status and merges in pref-derived fields
// (e.g. exit-node LAN access) that are not present in the IPN status itself.
func (m *Manager) fetchState(ctx context.Context) (*TailscaleState, error) {
status, err := m.client.Status(ctx)
if err != nil {
return nil, err
}
state := convertStatus(status) state := convertStatus(status)
m.updateState(state)
// Prefs carry the exit-node LAN-access toggle, which the status does not
// expose. Treat a prefs failure as non-fatal so status still updates.
if prefs, err := m.client.GetPrefs(ctx); err != nil {
log.Warnf("[Tailscale] Failed to fetch prefs: %v", err)
} else if prefs != nil {
state.ExitNodeAllowLANAccess = prefs.ExitNodeAllowLANAccess
}
return state, nil
} }
func (m *Manager) updateState(state *TailscaleState) { func (m *Manager) updateState(state *TailscaleState) {
@@ -297,62 +266,12 @@ func (m *Manager) RefreshState() {
ctx, cancel := context.WithTimeout(m.ctx, statusTimeout) ctx, cancel := context.WithTimeout(m.ctx, statusTimeout)
defer cancel() defer cancel()
state, err := m.fetchState(ctx) status, err := m.client.Status(ctx)
if err != nil { if err != nil {
log.Warnf("[Tailscale] Failed to refresh state: %v", err) log.Warnf("[Tailscale] Failed to refresh state: %v", err)
return return
} }
state := convertStatus(status)
m.updateState(state) m.updateState(state)
} }
// Connect brings the Tailscale backend up (WantRunning = true).
func (m *Manager) Connect() error {
return m.editPrefs(&ipn.MaskedPrefs{
Prefs: ipn.Prefs{WantRunning: true},
WantRunningSet: true,
})
}
// Disconnect brings the Tailscale backend down (WantRunning = false).
func (m *Manager) Disconnect() error {
return m.editPrefs(&ipn.MaskedPrefs{
Prefs: ipn.Prefs{WantRunning: false},
WantRunningSet: true,
})
}
// SetExitNode selects the exit node identified by its stable node ID. An empty
// id clears the current exit node. Mirrors `tailscale set --exit-node=<id>`,
// which also clears any legacy IP-based exit node so a stale ExitNodeIP cannot
// silently take precedence over the now-empty ID.
func (m *Manager) SetExitNode(id string) error {
return m.editPrefs(&ipn.MaskedPrefs{
Prefs: ipn.Prefs{ExitNodeID: tailcfg.StableNodeID(id)},
ExitNodeIDSet: true,
ExitNodeIPSet: true,
})
}
// SetAllowLANAccess toggles whether locally accessible subnets remain
// reachable while an exit node is in use.
func (m *Manager) SetAllowLANAccess(enabled bool) error {
return m.editPrefs(&ipn.MaskedPrefs{
Prefs: ipn.Prefs{ExitNodeAllowLANAccess: enabled},
ExitNodeAllowLANAccessSet: true,
})
}
// editPrefs applies a masked prefs edit and refreshes state so subscribers see
// the result immediately, in addition to the IPN bus notification it triggers.
func (m *Manager) editPrefs(mp *ipn.MaskedPrefs) error {
ctx, cancel := context.WithTimeout(m.ctx, statusTimeout)
defer cancel()
if _, err := m.client.EditPrefs(ctx, mp); err != nil {
return err
}
m.RefreshState()
return nil
}
+2 -101
View File
@@ -12,16 +12,8 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/ipn/ipnstate" "tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
) )
// blockingWatch is a watchFn that blocks until the context is cancelled, used
// by tests that exercise direct manager calls rather than the watch loop.
func blockingWatch(ctx context.Context, mask ipn.NotifyWatchOpt) (ipnBusWatcher, error) {
<-ctx.Done()
return nil, ctx.Err()
}
// mockWatcher yields canned Notify events, then returns err or blocks until Close/context cancel. // mockWatcher yields canned Notify events, then returns err or blocks until Close/context cancel.
type mockWatcher struct { type mockWatcher struct {
events []ipn.Notify events []ipn.Notify
@@ -76,10 +68,8 @@ func (w *mockWatcher) Close() error {
// mockClient implements tailscaleClient for testing. // mockClient implements tailscaleClient for testing.
type mockClient struct { type mockClient struct {
watchFn func(ctx context.Context, mask ipn.NotifyWatchOpt) (ipnBusWatcher, error) watchFn func(ctx context.Context, mask ipn.NotifyWatchOpt) (ipnBusWatcher, error)
statusFn func(ctx context.Context) (*ipnstate.Status, error) statusFn func(ctx context.Context) (*ipnstate.Status, error)
getPrefsFn func(ctx context.Context) (*ipn.Prefs, error)
editPrefsFn func(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error)
} }
func (c *mockClient) WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (ipnBusWatcher, error) { func (c *mockClient) WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (ipnBusWatcher, error) {
@@ -90,20 +80,6 @@ func (c *mockClient) Status(ctx context.Context) (*ipnstate.Status, error) {
return c.statusFn(ctx) return c.statusFn(ctx)
} }
func (c *mockClient) GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
if c.getPrefsFn != nil {
return c.getPrefsFn(ctx)
}
return &ipn.Prefs{}, nil
}
func (c *mockClient) EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
if c.editPrefsFn != nil {
return c.editPrefsFn(ctx, mp)
}
return &ipn.Prefs{}, nil
}
func runningStatus() *ipnstate.Status { func runningStatus() *ipnstate.Status {
return &ipnstate.Status{ return &ipnstate.Status{
Version: "1.94.2", Version: "1.94.2",
@@ -320,78 +296,3 @@ func TestManager_RefreshState(t *testing.T) {
assert.True(t, state.Connected) assert.True(t, state.Connected)
assert.Equal(t, "cachyos", state.Self.Hostname) assert.Equal(t, "cachyos", state.Self.Hostname)
} }
func TestManager_RefreshState_MergesPrefs(t *testing.T) {
client := &mockClient{
watchFn: blockingWatch,
statusFn: func(ctx context.Context) (*ipnstate.Status, error) { return runningStatus(), nil },
getPrefsFn: func(ctx context.Context) (*ipn.Prefs, error) {
return &ipn.Prefs{ExitNodeAllowLANAccess: true}, nil
},
}
m := newManager(client)
defer m.Close()
m.RefreshState()
assert.True(t, m.GetState().ExitNodeAllowLANAccess)
}
func TestManager_Actions_EditPrefs(t *testing.T) {
var captured *ipn.MaskedPrefs
client := &mockClient{
watchFn: blockingWatch,
statusFn: func(ctx context.Context) (*ipnstate.Status, error) { return runningStatus(), nil },
editPrefsFn: func(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
captured = mp
return &ipn.Prefs{}, nil
},
}
m := newManager(client)
defer m.Close()
require.NoError(t, m.Connect())
require.NotNil(t, captured)
assert.True(t, captured.WantRunningSet)
assert.True(t, captured.WantRunning)
require.NoError(t, m.Disconnect())
assert.True(t, captured.WantRunningSet)
assert.False(t, captured.WantRunning)
require.NoError(t, m.SetExitNode("nABC123"))
assert.True(t, captured.ExitNodeIDSet)
assert.Equal(t, tailcfg.StableNodeID("nABC123"), captured.ExitNodeID)
// ExitNodeIPSet must also be set so a stale legacy ExitNodeIP cannot
// override the ID-based selection (mirrors `tailscale set --exit-node`).
assert.True(t, captured.ExitNodeIPSet)
require.NoError(t, m.SetExitNode(""))
assert.True(t, captured.ExitNodeIDSet)
assert.Equal(t, tailcfg.StableNodeID(""), captured.ExitNodeID)
// Clearing must zero both the ID and any legacy IP-based exit node.
assert.True(t, captured.ExitNodeIPSet)
require.NoError(t, m.SetAllowLANAccess(true))
assert.True(t, captured.ExitNodeAllowLANAccessSet)
assert.True(t, captured.ExitNodeAllowLANAccess)
}
func TestManager_Actions_PropagateError(t *testing.T) {
client := &mockClient{
watchFn: blockingWatch,
statusFn: func(ctx context.Context) (*ipnstate.Status, error) { return runningStatus(), nil },
editPrefsFn: func(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
return nil, fmt.Errorf("backend rejected edit")
},
}
m := newManager(client)
defer m.Close()
assert.Error(t, m.Connect())
assert.Error(t, m.SetExitNode("nABC123"))
assert.Error(t, m.SetAllowLANAccess(true))
}
+22 -24
View File
@@ -2,32 +2,30 @@ package tailscale
// TailscaleState represents the current state of the Tailscale daemon. // TailscaleState represents the current state of the Tailscale daemon.
type TailscaleState struct { type TailscaleState struct {
Connected bool `json:"connected"` Connected bool `json:"connected"`
Version string `json:"version"` Version string `json:"version"`
BackendState string `json:"backendState"` BackendState string `json:"backendState"`
MagicDNSSuffix string `json:"magicDnsSuffix"` MagicDNSSuffix string `json:"magicDnsSuffix"`
TailnetName string `json:"tailnetName"` TailnetName string `json:"tailnetName"`
ExitNodeAllowLANAccess bool `json:"exitNodeAllowLanAccess"` Self Peer `json:"self"`
Self Peer `json:"self"` Peers []Peer `json:"peers"`
Peers []Peer `json:"peers"`
} }
// Peer represents a single node in the Tailscale network. // Peer represents a single node in the Tailscale network.
type Peer struct { type Peer struct {
ID string `json:"id"` ID string `json:"id"`
Hostname string `json:"hostname"` Hostname string `json:"hostname"`
DNSName string `json:"dnsName"` DNSName string `json:"dnsName"`
TailscaleIP string `json:"tailscaleIp"` TailscaleIP string `json:"tailscaleIp"`
TailscaleIPv6 string `json:"tailscaleIpv6,omitempty"` TailscaleIPv6 string `json:"tailscaleIpv6,omitempty"`
OS string `json:"os"` OS string `json:"os"`
Online bool `json:"online"` Online bool `json:"online"`
LastSeen string `json:"lastSeen,omitempty"` LastSeen string `json:"lastSeen,omitempty"`
ExitNode bool `json:"exitNode"` ExitNode bool `json:"exitNode"`
ExitNodeOption bool `json:"exitNodeOption"` Tags []string `json:"tags,omitempty"`
Tags []string `json:"tags,omitempty"` Owner string `json:"owner"`
Owner string `json:"owner"` Relay string `json:"relay,omitempty"`
Relay string `json:"relay,omitempty"` Active bool `json:"active"`
Active bool `json:"active"` RxBytes int64 `json:"rxBytes"`
RxBytes int64 `json:"rxBytes"` TxBytes int64 `json:"txBytes"`
TxBytes int64 `json:"txBytes"`
} }
+7 -7
View File
@@ -3,10 +3,10 @@
# Usage: ./create-source.sh <package-dir> [ubuntu-series] # Usage: ./create-source.sh <package-dir> [ubuntu-series]
# #
# Example: # Example:
# ./create-source.sh ../dms resolute # Ubuntu 26.04 LTS (default series in ppa-upload) # ./create-source.sh ../dms questing # Ubuntu 25.10 (default series in ppa-upload)
# ./create-source.sh ../dms stonking # Ubuntu 26.10 # ./create-source.sh ../dms resolute # Ubuntu 26.04 LTS
# ./create-source.sh ../dms-git questing
# ./create-source.sh ../dms-git resolute # ./create-source.sh ../dms-git resolute
# ./create-source.sh ../dms-git stonking
set -e set -e
@@ -27,13 +27,13 @@ if [ $# -lt 1 ]; then
echo "Arguments:" echo "Arguments:"
echo " package-dir : Path to package directory (e.g., ../dms)" echo " package-dir : Path to package directory (e.g., ../dms)"
echo " ubuntu-series : Ubuntu series (optional, default: noble)" echo " ubuntu-series : Ubuntu series (optional, default: noble)"
echo " Options: noble, jammy, oracular, mantic, resolute, stonking" echo " Options: noble, jammy, oracular, mantic, questing, resolute"
echo echo
echo "Examples:" echo "Examples:"
echo " $0 ../dms questing"
echo " $0 ../dms resolute" echo " $0 ../dms resolute"
echo " $0 ../dms stonking" echo " $0 ../dms-git questing"
echo " $0 ../dms-git resolute" echo " $0 ../dms-git resolute"
echo " $0 ../dms-git stonking"
exit 1 exit 1
fi fi
@@ -135,7 +135,7 @@ check_ppa_version_exists() {
local CHECK_MODE="${4:-commit}" local CHECK_MODE="${4:-commit}"
local DISTRO_SERIES="${5:-}" local DISTRO_SERIES="${5:-}"
# Query Launchpad API (optionally scoped to one Ubuntu series so the same version can ship to resolute and stonking) # Query Launchpad API (optionally scoped to one Ubuntu series so the same version can ship to questing and resolute)
local API_URL="https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/$PPA_NAME?ws.op=getPublishedSources&source_name=$SOURCE_NAME&status=Published" local API_URL="https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/$PPA_NAME?ws.op=getPublishedSources&source_name=$SOURCE_NAME&status=Published"
if [[ -n "$DISTRO_SERIES" ]]; then if [[ -n "$DISTRO_SERIES" ]]; then
API_URL+="&distro_series=https://api.launchpad.net/1.0/ubuntu/${DISTRO_SERIES}" API_URL+="&distro_series=https://api.launchpad.net/1.0/ubuntu/${DISTRO_SERIES}"
+2 -2
View File
@@ -10,8 +10,8 @@
PPA_OWNER="avengemedia" PPA_OWNER="avengemedia"
LAUNCHPAD_API="https://api.launchpad.net/1.0" LAUNCHPAD_API="https://api.launchpad.net/1.0"
# Supported Ubuntu series for PPA builds (26.04 LTS resolute + 26.10 stonking) # Supported Ubuntu series for PPA builds (25.10 questing + 26.04 LTS resolute)
DISTRO_SERIES_LIST=(resolute stonking) DISTRO_SERIES_LIST=(questing resolute)
# Define packages (sync with ppa-upload.sh) # Define packages (sync with ppa-upload.sh)
ALL_PACKAGES=(dms dms-git dms-greeter) ALL_PACKAGES=(dms dms-git dms-greeter)
+3 -3
View File
@@ -5,7 +5,7 @@ set -euo pipefail
PPA_OWNER="avengemedia" PPA_OWNER="avengemedia"
LAUNCHPAD_API="https://api.launchpad.net/1.0" LAUNCHPAD_API="https://api.launchpad.net/1.0"
SERIES_LIST=(resolute stonking) SERIES_LIST=(questing resolute)
PACKAGE_FILTER="dms-git" PACKAGE_FILTER="dms-git"
REBUILD_RELEASE="" REBUILD_RELEASE=""
JSON=false JSON=false
@@ -72,12 +72,12 @@ embedded_commit() {
target_ppa() { target_ppa() {
local series="$1" local series="$1"
if [[ -n "$REBUILD_RELEASE" ]]; then if [[ -n "$REBUILD_RELEASE" ]]; then
if [[ "$series" == "stonking" ]]; then if [[ "$series" == "resolute" ]]; then
echo $((REBUILD_RELEASE + 1)) echo $((REBUILD_RELEASE + 1))
else else
echo "$REBUILD_RELEASE" echo "$REBUILD_RELEASE"
fi fi
elif [[ "$series" == "stonking" ]]; then elif [[ "$series" == "resolute" ]]; then
echo "2" echo "2"
else else
echo "1" echo "1"
+8 -8
View File
@@ -3,13 +3,13 @@
# Usage: ./ppa-upload.sh [package-name] [ppa-name] [ubuntu-series] [rebuild-number] [--keep-builds] [--rebuild=N] # Usage: ./ppa-upload.sh [package-name] [ppa-name] [ubuntu-series] [rebuild-number] [--keep-builds] [--rebuild=N]
# #
# Examples: # Examples:
# ./ppa-upload.sh dms # Upload to resolute + stonking (default) # ./ppa-upload.sh dms # Upload to questing + resolute (default)
# ./ppa-upload.sh dms 2 # Native: resolute ppa2, stonking ppa3 (auto +1 on second series) # ./ppa-upload.sh dms 2 # Native: questing ppa2, resolute ppa3 (auto +1 on second series)
# ./ppa-upload.sh dms --rebuild=2 # Rebuild with ppa2 (flag syntax) # ./ppa-upload.sh dms --rebuild=2 # Rebuild with ppa2 (flag syntax)
# ./ppa-upload.sh dms-git # Single package (both series) # ./ppa-upload.sh dms-git # Single package (both series)
# ./ppa-upload.sh all # All packages (each to both series) # ./ppa-upload.sh all # All packages (each to both series)
# ./ppa-upload.sh dms resolute # 26.04 LTS only (same as "dms dms resolute") # ./ppa-upload.sh dms resolute # 26.04 LTS only (same as "dms dms resolute")
# ./ppa-upload.sh dms stonking # 26.10 only # ./ppa-upload.sh dms questing # 25.10 only
# ./ppa-upload.sh dms dms resolute # Explicit PPA name + one series (optional form) # ./ppa-upload.sh dms dms resolute # Explicit PPA name + one series (optional form)
# ./ppa-upload.sh dms dms resolute 2 # One series + rebuild number # ./ppa-upload.sh dms dms resolute 2 # One series + rebuild number
# ./ppa-upload.sh distro/ubuntu/dms dms # Path-style (backward compatible) # ./ppa-upload.sh distro/ubuntu/dms dms # Path-style (backward compatible)
@@ -70,8 +70,8 @@ if [[ ${#POSITIONAL_ARGS[@]} -gt 0 ]]; then
fi fi
fi fi
# Shorthand: "dms resolute" / "dms stonking" (package + series; PPA inferred — no need for "dms dms resolute") # Shorthand: "dms resolute" / "dms questing" (package + series; PPA inferred — no need for "dms dms resolute")
if [[ ${#POSITIONAL_ARGS[@]} -eq 2 ]] && [[ "${POSITIONAL_ARGS[1]}" == "resolute" || "${POSITIONAL_ARGS[1]}" == "stonking" ]]; then if [[ ${#POSITIONAL_ARGS[@]} -eq 2 ]] && [[ "${POSITIONAL_ARGS[1]}" == "questing" || "${POSITIONAL_ARGS[1]}" == "resolute" ]]; then
PACKAGE_INPUT="${POSITIONAL_ARGS[0]}" PACKAGE_INPUT="${POSITIONAL_ARGS[0]}"
PPA_NAME_INPUT="" PPA_NAME_INPUT=""
UBUNTU_SERIES_RAW="${POSITIONAL_ARGS[1]}" UBUNTU_SERIES_RAW="${POSITIONAL_ARGS[1]}"
@@ -79,11 +79,11 @@ fi
SERIES_LIST=() SERIES_LIST=()
if [[ -z "$UBUNTU_SERIES_RAW" ]]; then if [[ -z "$UBUNTU_SERIES_RAW" ]]; then
SERIES_LIST=(resolute stonking) SERIES_LIST=(questing resolute)
elif [[ "$UBUNTU_SERIES_RAW" == "resolute" || "$UBUNTU_SERIES_RAW" == "stonking" ]]; then elif [[ "$UBUNTU_SERIES_RAW" == "questing" || "$UBUNTU_SERIES_RAW" == "resolute" ]]; then
SERIES_LIST=("$UBUNTU_SERIES_RAW") SERIES_LIST=("$UBUNTU_SERIES_RAW")
else else
error "Invalid Ubuntu series: $UBUNTU_SERIES_RAW (use resolute, stonking, or omit for both)" error "Invalid Ubuntu series: $UBUNTU_SERIES_RAW (use questing, resolute, or omit for both)"
exit 1 exit 1
fi fi
+4 -11
View File
@@ -40,17 +40,10 @@ override_dh_auto_install:
install -Dm644 DankMaterialShell-$(BASE_VERSION)/LICENSE \ install -Dm644 DankMaterialShell-$(BASE_VERSION)/LICENSE \
debian/dms-greeter/usr/share/doc/dms-greeter/LICENSE debian/dms-greeter/usr/share/doc/dms-greeter/LICENSE
# Install systemd tmpfiles/sysusers fragments only when present in the fetched source. install -Dpm0644 DankMaterialShell-$(BASE_VERSION)/quickshell/systemd/tmpfiles-dms-greeter.conf \
# sysusers-dms-greeter.conf landed upstream after v1.4.6; guarding both lets older debian/dms-greeter/usr/lib/tmpfiles.d/dms-greeter.conf
# release tarballs build, while future tags that ship the files install them automatically. install -Dm644 DankMaterialShell-$(BASE_VERSION)/quickshell/systemd/sysusers-dms-greeter.conf \
if [ -f DankMaterialShell-$(BASE_VERSION)/quickshell/systemd/tmpfiles-dms-greeter.conf ]; then \ debian/dms-greeter/usr/lib/sysusers.d/dms-greeter.conf
install -Dpm0644 DankMaterialShell-$(BASE_VERSION)/quickshell/systemd/tmpfiles-dms-greeter.conf \
debian/dms-greeter/usr/lib/tmpfiles.d/dms-greeter.conf; \
fi
if [ -f DankMaterialShell-$(BASE_VERSION)/quickshell/systemd/sysusers-dms-greeter.conf ]; then \
install -Dm644 DankMaterialShell-$(BASE_VERSION)/quickshell/systemd/sysusers-dms-greeter.conf \
debian/dms-greeter/usr/lib/sysusers.d/dms-greeter.conf; \
fi
# Create cache directory structure (will be created by postinst) # Create cache directory structure (will be created by postinst)
mkdir -p debian/dms-greeter/var/cache/dms-greeter mkdir -p debian/dms-greeter/var/cache/dms-greeter
+1 -21
View File
@@ -6,18 +6,6 @@ DankMaterialShell provides comprehensive IPC (Inter-Process Communication) funct
dms ipc call <target> <function> [parameters...] dms ipc call <target> <function> [parameters...]
``` ```
## Discovering IPC commands
List all available targets and functions while DMS is running:
```bash
dms ipc list
dms ipc # same
dms ipc --help # same, plus usage text
```
Live listing requires DMS to be running. If listing fails, use this document or the [Keybinds & IPC docs](https://danklinux.com/docs/dankmaterialshell/keybinds-ipc) as an offline reference.
## Target: `audio` ## Target: `audio`
Audio system control and information. Audio system control and information.
@@ -719,7 +707,7 @@ File browser controls for selecting wallpapers and profile images.
- Both browsers support common image formats (jpg, jpeg, png, bmp, gif, webp) - Both browsers support common image formats (jpg, jpeg, png, bmp, gif, webp)
### Target: `color-picker` ### Target: `color-picker`
In-shell color picker modal for theme and settings color selection. Color picker modal control.
**Functions:** **Functions:**
- `open` - Show color picker modal - `open` - Show color picker modal
@@ -730,14 +718,6 @@ In-shell color picker modal for theme and settings color selection.
- `toggle` - Toggle color picker modal visibility - `toggle` - Toggle color picker modal visibility
- `toggleInstant` - Toggle color picker modal visibility without animation on hide - `toggleInstant` - Toggle color picker modal visibility without animation on hide
**Note:** This controls the in-shell modal. To pick a pixel from the screen via CLI, use `dms color pick` instead (see [Color Picker CLI](https://danklinux.com/docs/dankmaterialshell/cli-color-picker)).
**Examples:**
```bash
dms ipc call color-picker toggle
dms ipc call color-picker openColor "#3f51b5"
```
### Target: `hypr` ### Target: `hypr`
Hyprland-specific controls including keybinds cheatsheet and workspace overview (Hyprland only). Hyprland-specific controls including keybinds cheatsheet and workspace overview (Hyprland only).
+18 -20
View File
@@ -7,31 +7,29 @@ Item {
property alias path: socket.path property alias path: socket.path
property alias parser: socket.parser property alias parser: socket.parser
property bool connected: false property bool connected: false
property bool linkUp: false
property int reconnectBaseMs: 400 property int reconnectBaseMs: 400
property int reconnectMaxMs: 15000 property int reconnectMaxMs: 15000
property int _reconnectAttempt: 0 property int _reconnectAttempt: 0
signal connectionStateChanged signal connectionStateChanged()
onConnectedChanged: { onConnectedChanged: {
socket.connected = connected; socket.connected = connected
} }
Socket { Socket {
id: socket id: socket
onConnectionStateChanged: { onConnectionStateChanged: {
root.linkUp = connected; root.connectionStateChanged()
root.connectionStateChanged();
if (connected) { if (connected) {
root._reconnectAttempt = 0; root._reconnectAttempt = 0
return; return
} }
if (root.connected) { if (root.connected) {
root._scheduleReconnect(); root._scheduleReconnect()
} }
} }
} }
@@ -41,24 +39,24 @@ Item {
interval: 0 interval: 0
repeat: false repeat: false
onTriggered: { onTriggered: {
socket.connected = false; socket.connected = false
Qt.callLater(() => socket.connected = true); Qt.callLater(() => socket.connected = true)
} }
} }
function send(data) { function send(data) {
const json = typeof data === "string" ? data : JSON.stringify(data); const json = typeof data === "string" ? data : JSON.stringify(data)
const message = json.endsWith("\n") ? json : json + "\n"; const message = json.endsWith("\n") ? json : json + "\n"
socket.write(message); socket.write(message)
socket.flush(); socket.flush()
} }
function _scheduleReconnect() { function _scheduleReconnect() {
const pow = Math.min(_reconnectAttempt, 10); const pow = Math.min(_reconnectAttempt, 10)
const base = Math.min(reconnectBaseMs * Math.pow(2, pow), reconnectMaxMs); const base = Math.min(reconnectBaseMs * Math.pow(2, pow), reconnectMaxMs)
const jitter = Math.floor(Math.random() * Math.floor(base / 4)); const jitter = Math.floor(Math.random() * Math.floor(base / 4))
reconnectTimer.interval = base + jitter; reconnectTimer.interval = base + jitter
reconnectTimer.restart(); reconnectTimer.restart()
_reconnectAttempt++; _reconnectAttempt++
} }
} }
+1 -34
View File
@@ -126,40 +126,7 @@ const KEY_MAP = {
161: "exclamdown" 161: "exclamdown"
}; };
// Numpad (keypad) keys. Qt reuses the same Qt::Key_* values for the numpad and function xkbKeyFromQtKey(qk) {
// the main rows/nav cluster; only Qt.KeypadModifier distinguishes them. niri and
// the other compositors bind against the xkb KP_* keysym names, so we must emit
// those instead of the collapsed twin. With NumLock off the numpad sends the
// navigation keysyms (KP_Home, KP_End, ...); with NumLock on it sends KP_0..KP_9
// (handled by the digit range in xkbKeyFromQtKey). Operators/Enter are the same
// in both states.
const KP_MAP = {
16777232: "KP_Home",
16777235: "KP_Up",
16777238: "KP_Prior",
16777234: "KP_Left",
16777227: "KP_Begin",
16777236: "KP_Right",
16777233: "KP_End",
16777237: "KP_Down",
16777239: "KP_Next",
16777222: "KP_Insert",
16777223: "KP_Delete",
16777221: "KP_Enter",
43: "KP_Add",
45: "KP_Subtract",
42: "KP_Multiply",
47: "KP_Divide",
46: "KP_Decimal"
};
function xkbKeyFromQtKey(qk, isKeypad) {
if (isKeypad) {
if (qk >= 48 && qk <= 57)
return "KP_" + (qk - 48);
if (KP_MAP[qk])
return KP_MAP[qk];
}
if (qk >= 65 && qk <= 90) if (qk >= 65 && qk <= 90)
return String.fromCharCode(qk); return String.fromCharCode(qk);
if (qk >= 97 && qk <= 122) if (qk >= 97 && qk <= 122)
-3
View File
@@ -56,9 +56,6 @@ const DMS_ACTIONS = [
{ id: "spawn dms ipc call dankdash wallpaper", label: "Wallpaper Browser" }, { id: "spawn dms ipc call dankdash wallpaper", label: "Wallpaper Browser" },
{ id: "spawn dms ipc call file browse wallpaper", label: "File: Browse Wallpaper" }, { id: "spawn dms ipc call file browse wallpaper", label: "File: Browse Wallpaper" },
{ id: "spawn dms ipc call file browse profile", label: "File: Browse Profile" }, { id: "spawn dms ipc call file browse profile", label: "File: Browse Profile" },
{ id: "spawn dms ipc call color-picker toggle", label: "Color Picker: Toggle" },
{ id: "spawn dms ipc call color-picker open", label: "Color Picker: Open" },
{ id: "spawn dms ipc call color-picker close", label: "Color Picker: Close" },
{ id: "spawn dms ipc call keybinds toggle niri", label: "Keybinds Cheatsheet: Toggle", compositor: "niri" }, { id: "spawn dms ipc call keybinds toggle niri", label: "Keybinds Cheatsheet: Toggle", compositor: "niri" },
{ id: "spawn dms ipc call keybinds open niri", label: "Keybinds Cheatsheet: Open", compositor: "niri" }, { id: "spawn dms ipc call keybinds open niri", label: "Keybinds Cheatsheet: Open", compositor: "niri" },
{ id: "spawn dms ipc call keybinds close", label: "Keybinds Cheatsheet: Close" }, { id: "spawn dms ipc call keybinds close", label: "Keybinds Cheatsheet: Close" },
+12 -22
View File
@@ -74,15 +74,6 @@ Singleton {
return appId; return appId;
} }
function themedIconPath(name: string): string {
if (!name)
return "";
const themed = (typeof IconThemeService !== "undefined") ? IconThemeService.resolve(name) : "";
if (themed)
return themed;
return Quickshell.iconPath(name, true);
}
function resolveIconPath(iconName: string): string { function resolveIconPath(iconName: string): string {
if (!iconName) if (!iconName)
return ""; return "";
@@ -92,24 +83,23 @@ Singleton {
return toFileUrl(expandTilde(moddedId)); return toFileUrl(expandTilde(moddedId));
if (moddedId.startsWith("file://")) if (moddedId.startsWith("file://"))
return moddedId; return moddedId;
return themedIconPath(moddedId); return Quickshell.iconPath(moddedId, true);
} }
return themedIconPath(iconName) || DesktopService.resolveIconPath(iconName); return Quickshell.iconPath(iconName, true) || DesktopService.resolveIconPath(iconName);
} }
function resolveIconUrl(iconName: string): string { function resolveIconUrl(iconName: string): string {
if (!iconName) if (!iconName)
return ""; return "";
const moddedId = moddedAppId(iconName); const moddedId = moddedAppId(iconName);
const target = (moddedId !== iconName) ? moddedId : iconName; if (moddedId !== iconName) {
if (target.startsWith("~") || target.startsWith("/")) if (moddedId.startsWith("~") || moddedId.startsWith("/"))
return toFileUrl(expandTilde(target)); return toFileUrl(expandTilde(moddedId));
if (target.startsWith("file://")) if (moddedId.startsWith("file://"))
return target; return moddedId;
const themed = (typeof IconThemeService !== "undefined") ? IconThemeService.resolve(target) : ""; return "image://icon/" + moddedId;
if (themed) }
return themed; return "image://icon/" + iconName;
return "image://icon/" + target;
} }
function getAppIcon(appId: string, desktopEntry: var): string { function getAppIcon(appId: string, desktopEntry: var): string {
@@ -123,10 +113,10 @@ Singleton {
return resolveIconPath(appId); return resolveIconPath(appId);
if (desktopEntry && desktopEntry.icon) { if (desktopEntry && desktopEntry.icon) {
return themedIconPath(desktopEntry.icon); return Quickshell.iconPath(desktopEntry.icon, true);
} }
const icon = themedIconPath(appId); const icon = Quickshell.iconPath(appId, true);
if (icon && icon !== "") if (icon && icon !== "")
return icon; return icon;
+8 -152
View File
@@ -108,9 +108,6 @@ Singleton {
} }
property bool clipboardEnterToPaste: false property bool clipboardEnterToPaste: false
property bool clipboardRememberTypeFilter: false
property string clipboardTypeFilter: "all"
property var clipboardVisibleEntryActions: ["pin", "edit", "delete"]
property var launcherPluginVisibility: ({}) property var launcherPluginVisibility: ({})
@@ -166,8 +163,6 @@ Singleton {
property real popupTransparency: 1.0 property real popupTransparency: 1.0
property real dockTransparency: 1 property real dockTransparency: 1
property string widgetBackgroundColor: "sch" property string widgetBackgroundColor: "sch"
property string widgetBackgroundCustomColor: "#6750A4"
property real widgetBackgroundCustomStrength: 0.50
property string widgetColorMode: "default" property string widgetColorMode: "default"
property string controlCenterTileColorMode: "primary" property string controlCenterTileColorMode: "primary"
property string buttonColorMode: "primary" property string buttonColorMode: "primary"
@@ -186,7 +181,6 @@ Singleton {
property int firstDayOfWeek: -1 property int firstDayOfWeek: -1
property bool showWeekNumber: false property bool showWeekNumber: false
property string calendarBackend: "auto"
property bool use24HourClock: true property bool use24HourClock: true
property bool showSeconds: false property bool showSeconds: false
property bool padHours12Hour: false property bool padHours12Hour: false
@@ -389,16 +383,11 @@ Singleton {
property bool dwlShowAllTags: false property bool dwlShowAllTags: false
property bool workspaceActiveAppHighlightEnabled: false property bool workspaceActiveAppHighlightEnabled: false
property string workspaceColorMode: "default" property string workspaceColorMode: "default"
property string workspaceFocusedCustomColor: "#6750A4"
property string workspaceOccupiedColorMode: "none" property string workspaceOccupiedColorMode: "none"
property string workspaceOccupiedCustomColor: "#625B71"
property string workspaceUnfocusedColorMode: "default" property string workspaceUnfocusedColorMode: "default"
property string workspaceUnfocusedCustomColor: "#49454E"
property string workspaceUrgentColorMode: "default" property string workspaceUrgentColorMode: "default"
property string workspaceUrgentCustomColor: "#B3261E"
property bool workspaceFocusedBorderEnabled: false property bool workspaceFocusedBorderEnabled: false
property string workspaceFocusedBorderColor: "primary" property string workspaceFocusedBorderColor: "primary"
property string workspaceFocusedBorderCustomColor: "#6750A4"
property int workspaceFocusedBorderThickness: 2 property int workspaceFocusedBorderThickness: 2
property var workspaceNameIcons: ({}) property var workspaceNameIcons: ({})
property bool waveProgressEnabled: true property bool waveProgressEnabled: true
@@ -407,7 +396,6 @@ Singleton {
property bool audioVisualizerEnabled: true property bool audioVisualizerEnabled: true
property string audioScrollMode: "volume" property string audioScrollMode: "volume"
property int audioWheelScrollAmount: 5 property int audioWheelScrollAmount: 5
property bool audioDeviceScrollVolumeEnabled: false
property bool clockCompactMode: false property bool clockCompactMode: false
property int focusedWindowSize: 1 property int focusedWindowSize: 1
property bool focusedWindowCompactMode: false property bool focusedWindowCompactMode: false
@@ -415,9 +403,6 @@ Singleton {
property int barMaxVisibleApps: 0 property int barMaxVisibleApps: 0
property int barMaxVisibleRunningApps: 0 property int barMaxVisibleRunningApps: 0
property bool barShowOverflowBadge: true property bool barShowOverflowBadge: true
property bool trayAutoOverflow: true
property bool trayPopupSingleLine: true
property int trayMaxVisibleItems: 0
property bool appsDockHideIndicators: false property bool appsDockHideIndicators: false
property bool appsDockColorizeActive: false property bool appsDockColorizeActive: false
property string appsDockActiveColorMode: "primary" property string appsDockActiveColorMode: "primary"
@@ -474,8 +459,6 @@ Singleton {
property bool launcherUseOverlayLayer: false property bool launcherUseOverlayLayer: false
property string launcherStyle: "full" property string launcherStyle: "full"
property bool spotlightBarShowModeChips: false property bool spotlightBarShowModeChips: false
property bool keybindsFloatingWindow: false
onKeybindsFloatingWindowChanged: saveSettings()
property string _legacyWeatherLocation: "New York, NY" property string _legacyWeatherLocation: "New York, NY"
property string _legacyWeatherCoordinates: "40.7128,-74.0060" property string _legacyWeatherCoordinates: "40.7128,-74.0060"
@@ -487,11 +470,7 @@ Singleton {
property string networkPreference: "auto" property string networkPreference: "auto"
property string iconThemeDark: "System Default" property string iconTheme: "System Default"
property string iconThemeLight: "System Default"
property bool iconThemePerMode: false
property string lastAppliedIconTheme: ""
readonly property string iconTheme: resolveIconTheme()
property var availableIconThemes: ["System Default"] property var availableIconThemes: ["System Default"]
property string systemDefaultIconTheme: "" property string systemDefaultIconTheme: ""
property bool qt5ctAvailable: false property bool qt5ctAvailable: false
@@ -539,39 +518,13 @@ Singleton {
property real notificationSummaryFontSize: Spec.SPEC.notificationSummaryFontSize.def property real notificationSummaryFontSize: Spec.SPEC.notificationSummaryFontSize.def
property real notificationBodyFontSize: Spec.SPEC.notificationBodyFontSize.def property real notificationBodyFontSize: Spec.SPEC.notificationBodyFontSize.def
property bool notepadShowLineNumbers: false property bool notepadShowLineNumbers: false
property bool notepadAutoSave: false
property string notepadSlideoutSide: "right"
property string notepadDefaultMode: "slideout"
property real notepadTransparencyOverride: -1 property real notepadTransparencyOverride: -1
property real notepadLastCustomTransparency: 0.7 property real notepadLastCustomTransparency: 0.7
property bool notepadUseCompositorGap: false
property int notepadEdgeGap: 0
// Compositor layout gap when enabled and available, else the manual value.
readonly property int notepadEffectiveEdgeGap: {
if (notepadUseCompositorGap) {
var g = -1;
if (CompositorService.isNiri)
g = niriLayoutGapsOverride;
else if (CompositorService.isHyprland)
g = hyprlandLayoutGapsOverride;
else if (CompositorService.isMango)
g = mangoLayoutGapsOverride;
if (g >= 0)
return g;
}
return Math.max(0, notepadEdgeGap);
}
onNotepadUseMonospaceChanged: saveSettings() onNotepadUseMonospaceChanged: saveSettings()
onNotepadFontFamilyChanged: saveSettings() onNotepadFontFamilyChanged: saveSettings()
onNotepadFontSizeChanged: saveSettings() onNotepadFontSizeChanged: saveSettings()
onNotepadShowLineNumbersChanged: saveSettings() onNotepadShowLineNumbersChanged: saveSettings()
onNotepadAutoSaveChanged: saveSettings()
onNotepadSlideoutSideChanged: saveSettings()
onNotepadDefaultModeChanged: saveSettings()
onNotepadUseCompositorGapChanged: saveSettings()
onNotepadEdgeGapChanged: saveSettings()
// onCenteringModeChanged: saveSettings() // onCenteringModeChanged: saveSettings()
onNotepadTransparencyOverrideChanged: { onNotepadTransparencyOverrideChanged: {
if (notepadTransparencyOverride > 0) { if (notepadTransparencyOverride > 0) {
@@ -587,7 +540,6 @@ Singleton {
property bool soundVolumeChanged: true property bool soundVolumeChanged: true
property bool soundPluggedIn: true property bool soundPluggedIn: true
property bool soundLogin: false property bool soundLogin: false
property bool muteSoundsWhenMediaPlaying: true
property int acMonitorTimeout: 0 property int acMonitorTimeout: 0
property int acLockTimeout: 0 property int acLockTimeout: 0
@@ -602,13 +554,6 @@ Singleton {
property string batteryProfileName: "" property string batteryProfileName: ""
property int batteryPostLockMonitorTimeout: 0 property int batteryPostLockMonitorTimeout: 0
property int batteryChargeLimit: 100 property int batteryChargeLimit: 100
property bool batteryNotifyChargeLimit: false
property int batteryCriticalThreshold: 10
property bool batteryNotifyCritical: true
property int batteryLowThreshold: 20
property bool batteryNotifyLow: false
property int batteryNotificationType: 0
property bool batteryAutoPowerSaver: false
property bool lockBeforeSuspend: false property bool lockBeforeSuspend: false
property bool loginctlLockIntegration: true property bool loginctlLockIntegration: true
property bool fadeToLockEnabled: true property bool fadeToLockEnabled: true
@@ -1283,67 +1228,14 @@ Singleton {
MangoService.generateLayoutConfig(); MangoService.generateLayoutConfig();
} }
function resolveIconTheme() {
if (iconThemePerMode && typeof SessionData !== "undefined" && SessionData.isLightMode)
return iconThemeLight;
return iconThemeDark;
}
function applyStoredIconTheme() { function applyStoredIconTheme() {
updateGtkIconTheme(); updateGtkIconTheme();
updateQtIconTheme(); updateQtIconTheme();
updateCosmicIconTheme(); updateCosmicIconTheme();
} }
function setIconThemeUnmanaged() {
iconThemePerMode = false;
iconThemeDark = "System Default";
iconThemeLight = "System Default";
lastAppliedIconTheme = "";
saveSettings();
}
function checkIconThemeDrift() {
if (isGreeterMode)
return;
if (resolveIconTheme() === "System Default")
return;
if (!lastAppliedIconTheme)
return;
const script = `if command -v gsettings >/dev/null 2>&1; then
gsettings get org.gnome.desktop.interface icon-theme 2>/dev/null | sed "s/'//g"
elif command -v dconf >/dev/null 2>&1; then
dconf read /org/gnome/desktop/interface/icon-theme 2>/dev/null | sed "s/'//g"
fi`;
Proc.runCommand("iconThemeDriftCheck", ["sh", "-c", script], (output, exitCode) => {
const platform = (output || "").trim();
if (!platform)
return;
if (platform === root.lastAppliedIconTheme || platform === root.iconThemeDark || platform === root.iconThemeLight)
return;
root.setIconThemeUnmanaged();
ToastService.showWarning(I18n.tr("Icon theme changed outside DMS; switched to System Default", "shown when an external tool overrides the icon theme DMS applied"));
});
}
Connections {
target: typeof SessionData !== "undefined" ? SessionData : null
function onIsLightModeChanged() {
if (!SessionData.isSwitchingMode)
return;
if (!root.iconThemePerMode)
return;
if (root.iconThemeLight === root.iconThemeDark)
return;
root.applyStoredIconTheme();
root.saveSettings();
}
}
function updateCosmicIconTheme() { function updateCosmicIconTheme() {
const resolved = resolveIconTheme(); let cosmicThemeName = (iconTheme === "System Default") ? systemDefaultIconTheme : iconTheme;
let cosmicThemeName = (resolved === "System Default") ? systemDefaultIconTheme : resolved;
if (!cosmicThemeName || cosmicThemeName === "System Default") { if (!cosmicThemeName || cosmicThemeName === "System Default") {
const detectScript = `if command -v gsettings >/dev/null 2>&1; then const detectScript = `if command -v gsettings >/dev/null 2>&1; then
gsettings get org.gnome.desktop.interface icon-theme 2>/dev/null | sed "s/'//g" gsettings get org.gnome.desktop.interface icon-theme 2>/dev/null | sed "s/'//g"
@@ -1379,11 +1271,9 @@ Singleton {
} }
function updateGtkIconTheme() { function updateGtkIconTheme() {
const resolved = resolveIconTheme(); const gtkThemeName = (iconTheme === "System Default") ? systemDefaultIconTheme : iconTheme;
const gtkThemeName = (resolved === "System Default") ? systemDefaultIconTheme : resolved;
if (gtkThemeName === "System Default" || gtkThemeName === "") if (gtkThemeName === "System Default" || gtkThemeName === "")
return; return;
lastAppliedIconTheme = gtkThemeName;
if (typeof DMSService !== "undefined" && DMSService.apiVersion >= 3 && typeof PortalService !== "undefined") { if (typeof DMSService !== "undefined" && DMSService.apiVersion >= 3 && typeof PortalService !== "undefined") {
PortalService.setSystemIconTheme(gtkThemeName); PortalService.setSystemIconTheme(gtkThemeName);
} }
@@ -1408,20 +1298,13 @@ Singleton {
fi fi
done done
if command -v gsettings >/dev/null 2>&1; then
gsettings set org.gnome.desktop.interface icon-theme '${gtkThemeName}' 2>/dev/null || true
elif command -v dconf >/dev/null 2>&1; then
dconf write /org/gnome/desktop/interface/icon-theme "'${gtkThemeName}'" 2>/dev/null || true
fi
pkill -HUP -f 'gtk' 2>/dev/null || true`; pkill -HUP -f 'gtk' 2>/dev/null || true`;
Quickshell.execDetached(["sh", "-lc", configScript]); Quickshell.execDetached(["sh", "-lc", configScript]);
} }
function updateQtIconTheme() { function updateQtIconTheme() {
const resolved = resolveIconTheme(); const qtThemeName = (iconTheme === "System Default") ? "" : iconTheme;
const qtThemeName = (resolved === "System Default") ? "" : resolved;
if (!qtThemeName) if (!qtThemeName)
return; return;
const home = _homeUrl.replace("file://", "").replace(/'/g, "'\\''"); const home = _homeUrl.replace("file://", "").replace(/'/g, "'\\''");
@@ -1508,9 +1391,6 @@ Singleton {
if (obj?.directionalAnimationMode === 3 && frameMode !== "connected") if (obj?.directionalAnimationMode === 3 && frameMode !== "connected")
frameMode = "connected"; frameMode = "connected";
if (obj?.iconTheme !== undefined && obj?.iconThemeDark === undefined)
iconThemeDark = obj.iconTheme;
if (obj?.weatherLocation !== undefined) if (obj?.weatherLocation !== undefined)
_legacyWeatherLocation = obj.weatherLocation; _legacyWeatherLocation = obj.weatherLocation;
if (obj?.weatherCoordinates !== undefined) if (obj?.weatherCoordinates !== undefined)
@@ -1526,7 +1406,6 @@ Singleton {
applyStoredTheme(); applyStoredTheme();
updateCompositorCursor(); updateCompositorCursor();
Processes.detectQtTools(); Processes.detectQtTools();
Qt.callLater(checkIconThemeDrift);
_checkSettingsWritable(); _checkSettingsWritable();
} catch (e) { } catch (e) {
@@ -1771,15 +1650,6 @@ Singleton {
}; };
} }
function effectiveBarConfigForRender(config, usesFrameBarChrome) {
if (!config || !connectedFrameModeActive || usesFrameBarChrome)
return config;
const backup = connectedFrameBarStyleBackups[config.id];
if (!backup)
return config;
return Object.assign({}, config, backup);
}
// Single entry point for connected-mode settings state. // Single entry point for connected-mode settings state.
// !active restore backups // !active restore backups
function _reconcileConnectedFrameBarStyles() { function _reconcileConnectedFrameBarStyles() {
@@ -2534,24 +2404,10 @@ Singleton {
} }
function setIconTheme(themeName) { function setIconTheme(themeName) {
const light = iconThemePerMode && typeof SessionData !== "undefined" && SessionData.isLightMode; iconTheme = themeName;
setIconThemeForMode(themeName, light); updateGtkIconTheme();
} updateQtIconTheme();
updateCosmicIconTheme();
function setIconThemeForMode(themeName, light) {
if (light)
iconThemeLight = themeName;
else
iconThemeDark = themeName;
applyStoredIconTheme();
saveSettings();
if (typeof Theme !== "undefined" && Theme.currentTheme === Theme.dynamic)
Theme.generateSystemThemesFromCurrentTheme();
}
function setIconThemePerMode(enabled) {
iconThemePerMode = enabled;
applyStoredIconTheme();
saveSettings(); saveSettings();
if (typeof Theme !== "undefined" && Theme.currentTheme === Theme.dynamic) if (typeof Theme !== "undefined" && Theme.currentTheme === Theme.dynamic)
Theme.generateSystemThemesFromCurrentTheme(); Theme.generateSystemThemesFromCurrentTheme();
+2 -66
View File
@@ -450,9 +450,7 @@ Singleton {
"primaryText": getMatugenColor("on_primary", "#ffffff"), "primaryText": getMatugenColor("on_primary", "#ffffff"),
"primaryContainer": getMatugenColor("primary_container", "#1976d2"), "primaryContainer": getMatugenColor("primary_container", "#1976d2"),
"secondary": getMatugenColor("secondary", "#8ab4f8"), "secondary": getMatugenColor("secondary", "#8ab4f8"),
"secondaryContainer": getMatugenColor("secondary_container", getMatugenColor("surface_container_high", "#292b2f")),
"tertiary": getMatugenColor("tertiary", "#efb8c8"), "tertiary": getMatugenColor("tertiary", "#efb8c8"),
"tertiaryContainer": getMatugenColor("tertiary_container", getMatugenColor("surface_container_high", "#292b2f")),
"surface": getMatugenColor("surface", "#1a1c1e"), "surface": getMatugenColor("surface", "#1a1c1e"),
"surfaceText": getMatugenColor("on_background", "#e3e8ef"), "surfaceText": getMatugenColor("on_background", "#e3e8ef"),
"surfaceVariant": getMatugenColor("surface_variant", "#44464f"), "surfaceVariant": getMatugenColor("surface_variant", "#44464f"),
@@ -523,6 +521,7 @@ Singleton {
property color primary: currentThemeData.primary property color primary: currentThemeData.primary
property color primaryText: currentThemeData.primaryText property color primaryText: currentThemeData.primaryText
property color primaryContainer: currentThemeData.primaryContainer
property color secondary: currentThemeData.secondary property color secondary: currentThemeData.secondary
property color tertiary: currentThemeData.tertiary || currentThemeData.secondary property color tertiary: currentThemeData.tertiary || currentThemeData.secondary
property color surface: currentThemeData.surface property color surface: currentThemeData.surface
@@ -537,9 +536,6 @@ Singleton {
property color surfaceContainer: currentThemeData.surfaceContainer property color surfaceContainer: currentThemeData.surfaceContainer
property color surfaceContainerHigh: currentThemeData.surfaceContainerHigh property color surfaceContainerHigh: currentThemeData.surfaceContainerHigh
property color surfaceContainerHighest: currentThemeData.surfaceContainerHighest || surfaceContainerHigh property color surfaceContainerHighest: currentThemeData.surfaceContainerHighest || surfaceContainerHigh
property color primaryContainer: currentThemeData.primaryContainer || blend(surfaceContainerHigh, primary, 0.45)
property color secondaryContainer: currentThemeData.secondaryContainer || blend(surfaceContainerHigh, secondary, 0.35)
property color tertiaryContainer: currentThemeData.tertiaryContainer || blend(surfaceContainerHigh, tertiary, 0.35)
property color onSurface: surfaceText property color onSurface: surfaceText
property color onSurfaceVariant: surfaceVariantText property color onSurfaceVariant: surfaceVariantText
@@ -581,45 +577,6 @@ Singleton {
readonly property int layerOutlineWidth: BlurService.enabled && layerOutlineOpacity > 0 ? 1 : 0 readonly property int layerOutlineWidth: BlurService.enabled && layerOutlineOpacity > 0 ? 1 : 0
property color surfaceTextHover: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.08) property color surfaceTextHover: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.08)
property color surfaceTextAlpha: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.3) property color surfaceTextAlpha: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.3)
function roleColor(mode) {
switch (mode) {
case "primary":
case "pri":
return primary;
case "primaryContainer":
return primaryContainer;
case "secondary":
case "sec":
return secondary;
case "secondaryContainer":
return secondaryContainer;
case "tertiary":
case "ter":
return tertiary;
case "tertiaryContainer":
return tertiaryContainer;
case "surfaceText":
return surfaceText;
case "surfaceVariant":
return surfaceVariant;
case "s":
return surface;
case "sc":
return surfaceContainer;
case "sch":
return surfaceContainerHigh;
case "schh":
return surfaceContainerHighest;
case "sth":
return surfaceTextHover;
case "error":
case "err":
return error;
default:
return "transparent";
}
}
property color surfaceTextLight: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.06) property color surfaceTextLight: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.06)
property color surfaceTextMedium: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.7) property color surfaceTextMedium: Qt.rgba(surfaceText.r, surfaceText.g, surfaceText.b, 0.7)
@@ -1473,22 +1430,9 @@ Singleton {
property bool widgetBackgroundHasAlpha: { property bool widgetBackgroundHasAlpha: {
const colorMode = typeof SettingsData !== "undefined" ? SettingsData.widgetBackgroundColor : "sch"; const colorMode = typeof SettingsData !== "undefined" ? SettingsData.widgetBackgroundColor : "sch";
return colorMode === "sth" || colorMode === "custom"; return colorMode === "sth";
} }
function safeColor(value, fallback) {
try {
if (value === undefined || value === null || value === "")
return fallback;
return Qt.color(value);
} catch (e) {
return fallback;
}
}
readonly property color widgetBackgroundCustomBaseColor: safeColor(typeof SettingsData !== "undefined" ? SettingsData.widgetBackgroundCustomColor : "#6750A4", primaryContainer)
readonly property real widgetBackgroundCustomStrength: Math.max(0, Math.min(1, typeof SettingsData !== "undefined" ? (SettingsData.widgetBackgroundCustomStrength ?? 0.4) : 0.4))
property var widgetBaseBackgroundColor: { property var widgetBaseBackgroundColor: {
const colorMode = typeof SettingsData !== "undefined" ? SettingsData.widgetBackgroundColor : "sch"; const colorMode = typeof SettingsData !== "undefined" ? SettingsData.widgetBackgroundColor : "sch";
switch (colorMode) { switch (colorMode) {
@@ -1498,14 +1442,6 @@ Singleton {
return surfaceContainer; return surfaceContainer;
case "sch": case "sch":
return surfaceContainerHigh; return surfaceContainerHigh;
case "primaryContainer":
return primaryContainer;
case "secondaryContainer":
return secondaryContainer;
case "tertiaryContainer":
return tertiaryContainer;
case "custom":
return blend(surfaceContainerHigh, widgetBackgroundCustomBaseColor, widgetBackgroundCustomStrength);
case "sth": case "sth":
default: default:
return surfaceTextHover; return surfaceTextHover;
+1 -33
View File
@@ -19,8 +19,6 @@ var SPEC = {
dockTransparency: { def: 1.0, coerce: percentToUnit }, dockTransparency: { def: 1.0, coerce: percentToUnit },
widgetBackgroundColor: { def: "sch" }, widgetBackgroundColor: { def: "sch" },
widgetBackgroundCustomColor: { def: "#6750A4" },
widgetBackgroundCustomStrength: { def: 0.50, coerce: percentToUnit },
widgetColorMode: { def: "default" }, widgetColorMode: { def: "default" },
controlCenterTileColorMode: { def: "primary" }, controlCenterTileColorMode: { def: "primary" },
buttonColorMode: { def: "primary" }, buttonColorMode: { def: "primary" },
@@ -39,7 +37,6 @@ var SPEC = {
firstDayOfWeek: { def: -1 }, firstDayOfWeek: { def: -1 },
showWeekNumber: { def: false }, showWeekNumber: { def: false },
calendarBackend: { def: "auto" },
use24HourClock: { def: true }, use24HourClock: { def: true },
showSeconds: { def: false }, showSeconds: { def: false },
padHours12Hour: { def: false }, padHours12Hour: { def: false },
@@ -146,16 +143,11 @@ var SPEC = {
dwlShowAllTags: { def: false }, dwlShowAllTags: { def: false },
workspaceActiveAppHighlightEnabled: { def: false }, workspaceActiveAppHighlightEnabled: { def: false },
workspaceColorMode: { def: "default" }, workspaceColorMode: { def: "default" },
workspaceFocusedCustomColor: { def: "#6750A4" },
workspaceOccupiedColorMode: { def: "none" }, workspaceOccupiedColorMode: { def: "none" },
workspaceOccupiedCustomColor: { def: "#625B71" },
workspaceUnfocusedColorMode: { def: "default" }, workspaceUnfocusedColorMode: { def: "default" },
workspaceUnfocusedCustomColor: { def: "#49454E" },
workspaceUrgentColorMode: { def: "default" }, workspaceUrgentColorMode: { def: "default" },
workspaceUrgentCustomColor: { def: "#B3261E" },
workspaceFocusedBorderEnabled: { def: false }, workspaceFocusedBorderEnabled: { def: false },
workspaceFocusedBorderColor: { def: "primary" }, workspaceFocusedBorderColor: { def: "primary" },
workspaceFocusedBorderCustomColor: { def: "#6750A4" },
workspaceFocusedBorderThickness: { def: 2 }, workspaceFocusedBorderThickness: { def: 2 },
workspaceNameIcons: { def: {} }, workspaceNameIcons: { def: {} },
waveProgressEnabled: { def: true }, waveProgressEnabled: { def: true },
@@ -164,7 +156,6 @@ var SPEC = {
audioVisualizerEnabled: { def: true }, audioVisualizerEnabled: { def: true },
audioScrollMode: { def: "volume" }, audioScrollMode: { def: "volume" },
audioWheelScrollAmount: { def: 5 }, audioWheelScrollAmount: { def: 5 },
audioDeviceScrollVolumeEnabled: { def: false },
clockCompactMode: { def: false }, clockCompactMode: { def: false },
focusedWindowCompactMode: { def: false }, focusedWindowCompactMode: { def: false },
focusedWindowSize: { def: 1 }, focusedWindowSize: { def: 1 },
@@ -172,9 +163,6 @@ var SPEC = {
barMaxVisibleApps: { def: 0 }, barMaxVisibleApps: { def: 0 },
barMaxVisibleRunningApps: { def: 0 }, barMaxVisibleRunningApps: { def: 0 },
barShowOverflowBadge: { def: true }, barShowOverflowBadge: { def: true },
trayAutoOverflow: { def: true },
trayPopupSingleLine: { def: true },
trayMaxVisibleItems: { def: 0 },
appsDockHideIndicators: { def: false }, appsDockHideIndicators: { def: false },
appsDockColorizeActive: { def: false }, appsDockColorizeActive: { def: false },
appsDockActiveColorMode: { def: "primary" }, appsDockActiveColorMode: { def: "primary" },
@@ -237,17 +225,13 @@ var SPEC = {
launcherUseOverlayLayer: { def: false }, launcherUseOverlayLayer: { def: false },
launcherStyle: { def: "full" }, launcherStyle: { def: "full" },
spotlightBarShowModeChips: { def: false }, spotlightBarShowModeChips: { def: false },
keybindsFloatingWindow: { def: false },
useAutoLocation: { def: false }, useAutoLocation: { def: false },
weatherEnabled: { def: true }, weatherEnabled: { def: true },
networkPreference: { def: "auto" }, networkPreference: { def: "auto" },
iconThemeDark: { def: "System Default", onChange: "applyStoredIconTheme" }, iconTheme: { def: "System Default", onChange: "applyStoredIconTheme" },
iconThemeLight: { def: "System Default", onChange: "applyStoredIconTheme" },
iconThemePerMode: { def: false, onChange: "applyStoredIconTheme" },
lastAppliedIconTheme: { def: "" },
availableIconThemes: { def: ["System Default"], persist: false }, availableIconThemes: { def: ["System Default"], persist: false },
systemDefaultIconTheme: { def: "", persist: false }, systemDefaultIconTheme: { def: "", persist: false },
qt5ctAvailable: { def: false, persist: false }, qt5ctAvailable: { def: false, persist: false },
@@ -279,13 +263,8 @@ var SPEC = {
notificationSummaryFontSize: { def: 0 }, notificationSummaryFontSize: { def: 0 },
notificationBodyFontSize: { def: 0 }, notificationBodyFontSize: { def: 0 },
notepadShowLineNumbers: { def: false }, notepadShowLineNumbers: { def: false },
notepadAutoSave: { def: false },
notepadSlideoutSide: { def: "right" },
notepadDefaultMode: { def: "slideout" },
notepadTransparencyOverride: { def: -1 }, notepadTransparencyOverride: { def: -1 },
notepadLastCustomTransparency: { def: 0.7 }, notepadLastCustomTransparency: { def: 0.7 },
notepadUseCompositorGap: { def: false },
notepadEdgeGap: { def: 0 },
soundsEnabled: { def: true }, soundsEnabled: { def: true },
useSystemSoundTheme: { def: false }, useSystemSoundTheme: { def: false },
@@ -293,7 +272,6 @@ var SPEC = {
soundNewNotification: { def: true }, soundNewNotification: { def: true },
soundVolumeChanged: { def: true }, soundVolumeChanged: { def: true },
soundPluggedIn: { def: true }, soundPluggedIn: { def: true },
muteSoundsWhenMediaPlaying: { def: true },
acMonitorTimeout: { def: 0 }, acMonitorTimeout: { def: 0 },
acLockTimeout: { def: 0 }, acLockTimeout: { def: 0 },
@@ -308,13 +286,6 @@ var SPEC = {
batteryProfileName: { def: "" }, batteryProfileName: { def: "" },
batteryPostLockMonitorTimeout: { def: 0 }, batteryPostLockMonitorTimeout: { def: 0 },
batteryChargeLimit: { def: 100 }, batteryChargeLimit: { def: 100 },
batteryNotifyChargeLimit: { def: false },
batteryCriticalThreshold: { def: 10 },
batteryNotifyCritical: { def: true },
batteryLowThreshold: { def: 20 },
batteryNotifyLow: { def: false },
batteryNotificationType: { def: 0 },
batteryAutoPowerSaver: { def: false },
lockBeforeSuspend: { def: false }, lockBeforeSuspend: { def: false },
loginctlLockIntegration: { def: true }, loginctlLockIntegration: { def: true },
fadeToLockEnabled: { def: true }, fadeToLockEnabled: { def: true },
@@ -601,9 +572,6 @@ var SPEC = {
builtInPluginSettings: { def: {} }, builtInPluginSettings: { def: {} },
clipboardEnterToPaste: { def: false }, clipboardEnterToPaste: { def: false },
clipboardRememberTypeFilter: { def: false },
clipboardTypeFilter: { def: "all" },
clipboardVisibleEntryActions: { def: ["pin", "edit", "delete"] },
launcherPluginVisibility: { def: {} }, launcherPluginVisibility: { def: {} },
launcherPluginOrder: { def: [] }, launcherPluginOrder: { def: [] },
+22 -96
View File
@@ -64,15 +64,27 @@ Item {
} }
} }
property bool wallpaperSurfacesLoaded: true
Loader { Loader {
id: blurredWallpaperBackgroundLoader id: blurredWallpaperBackgroundLoader
active: SettingsData.blurredWallpaperLayer && CompositorService.isNiri active: root.wallpaperSurfacesLoaded && SettingsData.blurredWallpaperLayer && CompositorService.isNiri
asynchronous: false asynchronous: false
sourceComponent: BlurredWallpaperBackground {} sourceComponent: BlurredWallpaperBackground {}
} }
WallpaperBackground {} DeferredAction {
id: wallpaperSurfaceReloadAction
onTriggered: root.wallpaperSurfacesLoaded = true
}
Loader {
id: wallpaperBackgroundLoader
active: root.wallpaperSurfacesLoaded
asynchronous: false
sourceComponent: WallpaperBackground {}
}
DesktopWidgetLayer {} DesktopWidgetLayer {}
@@ -116,12 +128,6 @@ Item {
fadeWindowLoader.item.cancelFade(); fadeWindowLoader.item.cancelFade();
} }
} }
function onDismissFadeToLock() {
if (fadeWindowLoader.item) {
fadeWindowLoader.item.dismiss();
}
}
} }
} }
} }
@@ -323,9 +329,6 @@ Item {
property bool hadRealScreen: true property bool hadRealScreen: true
property var previousRealScreenNames: [] property var previousRealScreenNames: []
// Guards for the screen-reconnect recovery path (see scheduleScreenReconnectRecovery).
property bool _screenRecoveryCooldown: false
property bool _screenRecoveryPending: false
function _getRealScreenNames() { function _getRealScreenNames() {
const names = []; const names = [];
@@ -368,60 +371,15 @@ Item {
const partialReconnect = root.previousRealScreenNames.length > 0 const partialReconnect = root.previousRealScreenNames.length > 0
&& currentNames.some(name => !root.previousRealScreenNames.includes(name)); && currentNames.some(name => !root.previousRealScreenNames.includes(name));
if (fullReconnect || partialReconnect) { if (fullReconnect || partialReconnect) {
log.info("Screen reconnect detected, scheduling surface recovery", log.info("Screen reconnect detected, triggering surface recovery",
"full:", fullReconnect, "partial:", partialReconnect); "full:", fullReconnect, "partial:", partialReconnect);
root.scheduleScreenReconnectRecovery(); root.triggerSurfaceRecovery("screen-reconnect");
} }
root.hadRealScreen = hasReal; root.hadRealScreen = hasReal;
root.previousRealScreenNames = currentNames; root.previousRealScreenNames = currentNames;
} }
} }
// A DPMS off/on cycle removes an output from the screen list and re-adds it,
// which is indistinguishable here from a hotplug. Recovering immediately on
// every such event lets a flapping monitor (or a recovery that itself perturbs
// the output) drive an endless recovery storm that power-cycles the display
// (#2642). Debounce a burst of changes into a single pass, then hold a cooldown
// so repeated flaps trigger at most one recovery per window. Recovery still runs
// once per resume, so a partial DPMS resume keeps redrawing its surfaces (#2579).
function scheduleScreenReconnectRecovery() {
if (root._screenRecoveryCooldown) {
root._screenRecoveryPending = true;
return;
}
screenReconnectDebounce.restart();
}
Timer {
id: screenReconnectDebounce
// Wide enough to collapse the output-remove + output-re-add pair that one
// DPMS off/on cycle emits as two near-simultaneous events into one recovery.
interval: 450
repeat: false
onTriggered: {
root._screenRecoveryCooldown = true;
root._screenRecoveryPending = false;
screenReconnectCooldown.restart();
root.triggerSurfaceRecovery("screen-reconnect");
}
}
Timer {
id: screenReconnectCooldown
// Must exceed the full two-pass surfaceResumeRecoveryTimer sequence
// (800 + 2000 ms) so the cooldown still covers an in-flight recovery;
// raise this if those passes are lengthened.
interval: 4000
repeat: false
onTriggered: {
root._screenRecoveryCooldown = false;
if (root._screenRecoveryPending) {
root._screenRecoveryPending = false;
screenReconnectDebounce.restart();
}
}
}
Timer { Timer {
id: surfaceResumeRecoveryTimer id: surfaceResumeRecoveryTimer
interval: 800 interval: 800
@@ -440,6 +398,11 @@ Item {
frameSurfaceReloadAction.schedule(); frameSurfaceReloadAction.schedule();
} }
if (root.wallpaperSurfacesLoaded) {
root.wallpaperSurfacesLoaded = false;
wallpaperSurfaceReloadAction.schedule();
}
root.dockEnabled = false; root.dockEnabled = false;
Qt.callLater(() => { Qt.callLater(() => {
root.dockEnabled = true; root.dockEnabled = true;
@@ -707,7 +670,7 @@ Item {
if (!wifiPasswordModalLoader.item) if (!wifiPasswordModalLoader.item)
return; return;
if (wifiPasswordModalLoader.item.shouldBeVisible && timeSinceLastPrompt < 1000) { if (wifiPasswordModalLoader.item.visible && timeSinceLastPrompt < 1000) {
NetworkService.cancelCredentials(lastCredentialsToken); NetworkService.cancelCredentials(lastCredentialsToken);
lastCredentialsToken = token; lastCredentialsToken = token;
lastCredentialsTime = now; lastCredentialsTime = now;
@@ -1051,14 +1014,6 @@ Item {
osdResumeRecreateTimer.interval = 400; osdResumeRecreateTimer.interval = 400;
osdResumeRecreateTimer.restart(); osdResumeRecreateTimer.restart();
// This path runs its own recovery directly, so drop any queued or
// in-flight screen-reconnect recovery to avoid a redundant pass once
// its cooldown expires.
screenReconnectDebounce.stop();
screenReconnectCooldown.stop();
root._screenRecoveryCooldown = false;
root._screenRecoveryPending = false;
root.triggerSurfaceRecovery("sessionResumed"); root.triggerSurfaceRecovery("sessionResumed");
} }
} }
@@ -1155,22 +1110,11 @@ Item {
slideoutWidth: 480 slideoutWidth: 480
expandable: true expandable: true
expandedWidthValue: 960 expandedWidthValue: 960
edgeGap: SettingsData.notepadEffectiveEdgeGap
slideEdge: SettingsData.notepadSlideoutSide
onIsVisibleChanged: {
if (isVisible)
PopoutService.notepadPopout?.hide();
}
content: Component { content: Component {
Notepad { Notepad {
slideout: notepadSlideout slideout: notepadSlideout
onHideRequested: notepadSlideout.hide() onHideRequested: notepadSlideout.hide()
onPopoutRequested: {
notepadSlideout.hide();
PopoutService.openNotepadPopout();
}
} }
} }
@@ -1187,24 +1131,6 @@ Item {
Component.onCompleted: PopoutService.notepadSlideouts = instances Component.onCompleted: PopoutService.notepadSlideouts = instances
} }
LazyLoader {
id: notepadPopoutLoader
active: false
Component.onCompleted: {
PopoutService.notepadPopoutLoader = notepadPopoutLoader;
}
onActiveChanged: {
if (active && item) {
PopoutService.notepadPopout = item;
PopoutService._onNotepadPopoutLoaded();
}
}
NotepadPopoutWindow {}
}
LazyLoader { LazyLoader {
id: powerMenuModalLoader id: powerMenuModalLoader
+1 -13
View File
@@ -373,10 +373,6 @@ Item {
} }
function open(): string { function open(): string {
if (SettingsData.notepadDefaultMode === "popout") {
PopoutService.openNotepadPopout();
return "NOTEPAD_OPEN_SUCCESS";
}
var instance = getActiveNotepadInstance(); var instance = getActiveNotepadInstance();
if (instance) { if (instance) {
instance.show(); instance.show();
@@ -386,10 +382,6 @@ Item {
} }
function close(): string { function close(): string {
if (SettingsData.notepadDefaultMode === "popout") {
PopoutService.notepadPopout?.hide();
return "NOTEPAD_CLOSE_SUCCESS";
}
var instance = getActiveNotepadInstance(); var instance = getActiveNotepadInstance();
if (instance) { if (instance) {
instance.hide(); instance.hide();
@@ -399,10 +391,6 @@ Item {
} }
function toggle(): string { function toggle(): string {
if (SettingsData.notepadDefaultMode === "popout") {
PopoutService.toggleNotepadPopout();
return "NOTEPAD_TOGGLE_SUCCESS";
}
var instance = getActiveNotepadInstance(); var instance = getActiveNotepadInstance();
if (instance) { if (instance) {
instance.toggle(); instance.toggle();
@@ -956,7 +944,7 @@ Item {
function tabs(): string { function tabs(): string {
if (!PopoutService.settingsModal) if (!PopoutService.settingsModal)
return "wallpaper\ntheme\ntypography\ntime_weather\nsounds\ndankbar\ndankbar_settings\ndankbar_appearance\ndankbar_widgets\nframe\nworkspaces\ncompositor\nmedia_player\nnotifications\nosd\nrunning_apps\nupdater\ndock\nlauncher\nkeybinds\ndisplays\nnetwork\nnetwork_status\nnetwork_ethernet\nnetwork_wifi\nnetwork_vpn\nprinters\nlock_screen\npower_sleep\nplugins\nabout"; return "wallpaper\ntheme\ntypography\ntime_weather\nsounds\ndankbar\ndankbar_settings\ndankbar_appearance\ndankbar_widgets\nframe\nworkspaces\ncompositor\nmedia_player\nnotifications\nosd\nrunning_apps\nupdater\ndock\nlauncher\nkeybinds\ndisplays\nnetwork\nprinters\nlock_screen\npower_sleep\nplugins\nabout";
var modal = PopoutService.settingsModal; var modal = PopoutService.settingsModal;
var ids = []; var ids = [];
var structure = modal.sidebar?.categoryStructure ?? []; var structure = modal.sidebar?.categoryStructure ?? [];
@@ -7,18 +7,11 @@ Item {
id: clipboardContent id: clipboardContent
required property var modal required property var modal
required property var clearConfirmDialog
property alias searchField: searchField property alias searchField: searchField
property alias clipboardListView: clipboardListView property alias clipboardListView: clipboardListView
readonly property var filterOptions: [I18n.tr("All"), I18n.tr("Text"), I18n.tr("Long Text"), I18n.tr("Image")]
readonly property var filterValues: ["all", "text", "long_text", "image"]
function closeFilterMenu() {
filterMenuLoader.active = false;
filterMenuLoader.active = true;
}
anchors.fill: parent anchors.fill: parent
Column { Column {
@@ -40,85 +33,38 @@ Item {
pinnedCount: modal.pinnedCount pinnedCount: modal.pinnedCount
onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints
onTabChanged: tabName => modal.activeTab = tabName onTabChanged: tabName => modal.activeTab = tabName
onClearAllClicked: modal.confirmClearAll() onClearAllClicked: {
const hasPinned = modal.pinnedCount > 0;
const message = hasPinned ? I18n.tr("This will delete all unpinned entries. %1 pinned entries will be kept.").arg(modal.pinnedCount) : I18n.tr("This will permanently delete all clipboard history.");
clearConfirmDialog.show(I18n.tr("Clear History?"), message, function () {
modal.clearAll();
modal.hide();
}, function () {});
}
onCloseClicked: modal.hide() onCloseClicked: modal.hide()
} }
Item { DankTextField {
id: searchRow id: searchField
width: parent.width width: parent.width
implicitHeight: searchField.height placeholderText: ""
leftIconName: "search"
DankTextField { showClearButton: true
id: searchField focus: true
ignoreTabKeys: true
width: parent.width keyForwardTargets: [modal.modalFocusScope]
rightAccessoryWidth: filterButton.width + Theme.spacingS onTextChanged: {
placeholderText: "" modal.searchText = text;
leftIconName: "search" modal.updateFilteredModel();
showClearButton: true
focus: true
ignoreTabKeys: true
keyForwardTargets: [modal.modalFocusScope]
onTextChanged: {
modal.searchText = text;
modal.updateFilteredModel();
}
Keys.onEscapePressed: function (event) {
modal.hide();
event.accepted = true;
}
Component.onCompleted: {
Qt.callLater(function () {
forceActiveFocus();
});
}
} }
Keys.onEscapePressed: function (event) {
DankActionButton { modal.hide();
id: filterButton event.accepted = true;
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
iconName: "filter_list"
iconColor: modal.activeFilter !== "all" ? Theme.primary : Theme.surfaceText
backgroundColor: modal.activeFilter !== "all" ? Theme.primarySelected : "transparent"
tooltipText: I18n.tr("Filter by type", "Clipboard history type filter button tooltip")
onClicked: filterMenuLoader.item?.openDropdownMenu()
} }
Component.onCompleted: {
Loader { Qt.callLater(function () {
id: filterMenuLoader forceActiveFocus();
});
active: true
sourceComponent: filterMenuComponent
}
Component {
id: filterMenuComponent
DankDropdown {
showTrigger: false
popupAnchorItem: filterButton
popupWidth: 180
alignPopupRight: true
options: clipboardContent.filterOptions
currentValue: {
const idx = clipboardContent.filterValues.indexOf(clipboardContent.modal.activeFilter);
return idx >= 0 ? clipboardContent.filterOptions[idx] : clipboardContent.filterOptions[0];
}
onValueChanged: value => {
const idx = clipboardContent.filterOptions.indexOf(value);
if (idx >= 0) {
clipboardContent.modal.activeFilter = clipboardContent.filterValues[idx];
}
}
}
} }
} }
} }
@@ -15,12 +15,6 @@ Item {
property var entry: null property var entry: null
property string editorText: "" property string editorText: ""
function releaseTextInputFocus() {
if (editField) {
editField.focus = false;
}
}
function decodeEntryData(data) { function decodeEntryData(data) {
if (!data) { if (!data) {
return ""; return "";
+7 -32
View File
@@ -22,14 +22,7 @@ Rectangle {
readonly property string entryType: modal ? modal.getEntryType(entry) : "text" readonly property string entryType: modal ? modal.getEntryType(entry) : "text"
readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : "" readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : ""
readonly property var pinnedDuplicateEntry: !entry.pinned ? ClipboardService.getPinnedEntryByHash(entry.hash) : null readonly property var pinnedDuplicateEntry: !entry.pinned ? ClipboardService.getPinnedEntryByHash(entry.hash) : null
readonly property bool hasPinnedDuplicate: pinnedDuplicateEntry !== null readonly property bool effectivePinned: entry.pinned || pinnedDuplicateEntry !== null
readonly property bool effectivePinned: entry.pinned || hasPinnedDuplicate
readonly property var visibleEntryActions: SettingsData.clipboardVisibleEntryActions || ["pin", "edit", "delete"]
readonly property bool showPinAction: visibleEntryActions.includes("pin")
readonly property bool showEditAction: visibleEntryActions.includes("edit")
readonly property bool showDeleteAction: visibleEntryActions.includes("delete")
readonly property bool showPinnedIndicator: hasPinnedDuplicate && !showPinAction
readonly property bool showAnyAction: showPinAction || showEditAction || showDeleteAction || showPinnedIndicator
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
@@ -70,28 +63,12 @@ Rectangle {
anchors.rightMargin: Theme.spacingS anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS spacing: Theme.spacingXS
visible: root.showAnyAction
Item {
width: 40
height: 40
visible: root.showPinnedIndicator
// Status indicator only; the Pin action remains hidden.
DankIcon {
anchors.centerIn: parent
name: "push_pin"
size: Theme.iconSize - 6
color: Theme.primary
}
}
DankActionButton { DankActionButton {
iconName: "push_pin" iconName: "push_pin"
iconSize: Theme.iconSize - 6 iconSize: Theme.iconSize - 6
iconColor: (entry.pinned || hasPinnedDuplicate) ? Theme.primary : Theme.surfaceText iconColor: effectivePinned ? Theme.primary : Theme.surfaceText
backgroundColor: (entry.pinned || hasPinnedDuplicate) ? Theme.primarySelected : "transparent" backgroundColor: effectivePinned ? Theme.primarySelected : "transparent"
visible: root.showPinAction
onClicked: { onClicked: {
if (entry.pinned) { if (entry.pinned) {
unpinRequested(entry); unpinRequested(entry);
@@ -109,7 +86,6 @@ Rectangle {
iconName: "edit" iconName: "edit"
iconSize: Theme.iconSize - 6 iconSize: Theme.iconSize - 6
iconColor: Theme.surfaceText iconColor: Theme.surfaceText
visible: root.showEditAction
onClicked: { onClicked: {
if (entryType === "image") { if (entryType === "image") {
@@ -123,7 +99,6 @@ Rectangle {
iconName: "close" iconName: "close"
iconSize: Theme.iconSize - 6 iconSize: Theme.iconSize - 6
iconColor: Theme.surfaceText iconColor: Theme.surfaceText
visible: root.showDeleteAction
onClicked: deleteRequested() onClicked: deleteRequested()
} }
} }
@@ -131,8 +106,8 @@ Rectangle {
Item { Item {
anchors.left: indexBadge.right anchors.left: indexBadge.right
anchors.leftMargin: Theme.spacingM anchors.leftMargin: Theme.spacingM
anchors.right: root.showAnyAction ? actionButtons.left : parent.right anchors.right: actionButtons.left
anchors.rightMargin: root.showAnyAction ? Theme.spacingM : Theme.spacingS anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
// height: contentColumn.implicitHeight // height: contentColumn.implicitHeight
height: ClipboardConstants.itemHeight height: ClipboardConstants.itemHeight
@@ -193,8 +168,8 @@ Rectangle {
MouseArea { MouseArea {
id: mouseArea id: mouseArea
anchors.left: parent.left anchors.left: parent.left
anchors.right: root.showAnyAction ? actionButtons.left : parent.right anchors.right: actionButtons.left
anchors.rightMargin: root.showAnyAction ? Theme.spacingS : 0 anchors.rightMargin: Theme.spacingS
anchors.top: parent.top anchors.top: parent.top
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
hoverEnabled: true hoverEnabled: true
@@ -38,7 +38,6 @@ Item {
font.weight: Font.Medium font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
Row { Row {
@@ -16,7 +16,6 @@ FocusScope {
property string mode: "history" property string mode: "history"
property string searchText: ClipboardService.searchText property string searchText: ClipboardService.searchText
property string activeFilter: SettingsData.clipboardRememberTypeFilter ? SettingsData.clipboardTypeFilter : "all"
readonly property bool clipboardAvailable: ClipboardService.clipboardAvailable readonly property bool clipboardAvailable: ClipboardService.clipboardAvailable
readonly property bool wtypeAvailable: ClipboardService.wtypeAvailable readonly property bool wtypeAvailable: ClipboardService.wtypeAvailable
@@ -51,56 +50,16 @@ FocusScope {
} }
onSearchTextChanged: ClipboardService.searchText = searchText onSearchTextChanged: ClipboardService.searchText = searchText
onActiveFilterChanged: {
ClipboardService.activeFilter = activeFilter;
ClipboardService.selectedIndex = 0;
ClipboardService.keyboardNavigationActive = false;
ClipboardService.updateFilteredModel();
if (SettingsData.clipboardRememberTypeFilter) {
SettingsData.set("clipboardTypeFilter", activeFilter);
}
}
function releaseTextInputFocus() {
// Drop text-input focus before hiding the Wayland surface.
if (searchField) {
searchField.setFocus(false);
}
if (editorView) {
editorView.releaseTextInputFocus();
}
root.forceActiveFocus();
}
function requestClose(instant) {
releaseTextInputFocus();
if (instant) {
root.instantCloseRequested();
} else {
root.closeRequested();
}
}
function hide() { function hide() {
requestClose(false); closeRequested();
} }
function pasteSelected() { function pasteSelected() {
const entry = selectedEntry(); ClipboardService.pasteSelected(() => root.instantCloseRequested());
if (!entry)
return;
ClipboardService.pasteEntry(entry, () => root.requestClose(true));
} }
function copyEntry(entry) { function copyEntry(entry) {
ClipboardService.copyEntry(entry, () => root.requestClose(false)); ClipboardService.copyEntry(entry, () => root.closeRequested());
}
function selectedEntry() {
const entries = activeTab === "saved" ? pinnedEntries : unpinnedEntries;
if (!entries || entries.length === 0 || selectedIndex < 0 || selectedIndex >= entries.length)
return null;
return entries[selectedIndex];
} }
function deleteEntry(entry) { function deleteEntry(entry) {
@@ -123,15 +82,6 @@ FocusScope {
ClipboardService.clearAll(); ClipboardService.clearAll();
} }
function confirmClearAll() {
const hasPinned = pinnedCount > 0;
const message = hasPinned ? I18n.tr("This will delete all unpinned entries. %1 pinned entries will be kept.").arg(pinnedCount) : I18n.tr("This will permanently delete all clipboard history.");
clearConfirmDialog.show(I18n.tr("Clear History?"), message, function () {
clearAll();
hide();
}, function () {});
}
function getEntryPreview(entry) { function getEntryPreview(entry) {
return ClipboardService.getEntryPreview(entry); return ClipboardService.getEntryPreview(entry);
} }
@@ -159,8 +109,6 @@ FocusScope {
function resetState() { function resetState() {
activeImageLoads = 0; activeImageLoads = 0;
mode = "history"; mode = "history";
historyContent.closeFilterMenu();
activeFilter = SettingsData.clipboardRememberTypeFilter ? SettingsData.clipboardTypeFilter : "all";
ClipboardService.reset(); ClipboardService.reset();
keyboardController.reset(); keyboardController.reset();
} }
@@ -187,6 +135,7 @@ FocusScope {
id: historyContent id: historyContent
anchors.fill: parent anchors.fill: parent
modal: root modal: root
clearConfirmDialog: root.clearConfirmDialog
} }
} }
@@ -45,22 +45,8 @@ DankModal {
}); });
} }
function releaseTextInputFocus() {
contentLoader.item?.releaseTextInputFocus();
}
function hide() { function hide() {
releaseTextInputFocus(); close();
Qt.callLater(function () {
clipboardHistoryModal.close();
});
}
function instantHide() {
releaseTextInputFocus();
Qt.callLater(function () {
clipboardHistoryModal.instantClose();
});
} }
onDialogClosed: { onDialogClosed: {
@@ -82,11 +68,6 @@ DankModal {
enableShadow: true enableShadow: true
closeOnEscapeKey: (contentLoader.item?.mode ?? "history") !== "editor" closeOnEscapeKey: (contentLoader.item?.mode ?? "history") !== "editor"
onBackgroundClicked: hide() onBackgroundClicked: hide()
onShouldBeVisibleChanged: {
if (!shouldBeVisible) {
releaseTextInputFocus();
}
}
Ref { Ref {
service: ClipboardService service: ClipboardService
@@ -96,42 +77,29 @@ DankModal {
id: clearConfirmDialog id: clearConfirmDialog
confirmButtonText: I18n.tr("Clear All") confirmButtonText: I18n.tr("Clear All")
confirmButtonColor: Theme.primary confirmButtonColor: Theme.primary
onShouldBeVisibleChanged: { onVisibleChanged: {
if (shouldBeVisible) { if (visible) {
clipboardHistoryModal.shouldHaveFocus = false; clipboardHistoryModal.shouldHaveFocus = false;
selectedButton = 0;
keyboardNavigation = true;
return; return;
} }
Qt.callLater(function () { Qt.callLater(function () {
if (!clipboardHistoryModal.shouldBeVisible) { if (!clipboardHistoryModal.shouldBeVisible) {
return; return;
} }
clipboardHistoryModal.shouldHaveFocus = Qt.binding(() => clipboardHistoryModal.shouldBeVisible); clipboardHistoryModal.shouldHaveFocus = true;
clipboardHistoryModal.modalFocusScope.forceActiveFocus(); clipboardHistoryModal.modalFocusScope.forceActiveFocus();
if (clipboardHistoryModal.contentLoader.item?.searchField) { if (clipboardHistoryModal.contentLoader.item?.searchField) {
clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus(); clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus();
} }
}); });
} }
Connections {
target: clearConfirmDialog.modalFocusScope.Keys
function onPressed(event) {
if (!clearConfirmDialog.shouldBeVisible || event.key !== Qt.Key_Backtab) {
return;
}
clearConfirmDialog.selectedButton = clearConfirmDialog.selectedButton === -1 ? 1 : (clearConfirmDialog.selectedButton - 1 + 2) % 2;
clearConfirmDialog.keyboardNavigation = true;
event.accepted = true;
}
}
} }
content: Component { content: Component {
ClipboardHistoryContent { ClipboardHistoryContent {
clearConfirmDialog: clearConfirmDialog clearConfirmDialog: clearConfirmDialog
onCloseRequested: clipboardHistoryModal.hide() onCloseRequested: clipboardHistoryModal.hide()
onInstantCloseRequested: clipboardHistoryModal.instantHide() onInstantCloseRequested: clipboardHistoryModal.instantClose()
} }
} }
} }
@@ -1,7 +1,6 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Modals.Clipboard import qs.Modals.Clipboard
import qs.Modals.Common import qs.Modals.Common
@@ -37,15 +36,8 @@ DankPopout {
}); });
} }
function releaseTextInputFocus() {
contentLoader.item?.releaseTextInputFocus();
}
function hide() { function hide() {
releaseTextInputFocus(); close();
Qt.callLater(function () {
root.close();
});
} }
function clearAll() { function clearAll() {
@@ -64,7 +56,6 @@ DankPopout {
onShouldBeVisibleChanged: { onShouldBeVisibleChanged: {
if (!shouldBeVisible) { if (!shouldBeVisible) {
releaseTextInputFocus();
return; return;
} }
if (clipboardAvailable) { if (clipboardAvailable) {
@@ -104,35 +95,6 @@ DankPopout {
id: clearConfirmDialog id: clearConfirmDialog
confirmButtonText: I18n.tr("Clear All") confirmButtonText: I18n.tr("Clear All")
confirmButtonColor: Theme.primary confirmButtonColor: Theme.primary
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
root.customKeyboardFocus = WlrKeyboardFocus.None;
selectedButton = 0;
keyboardNavigation = true;
return;
}
root.customKeyboardFocus = null;
Qt.callLater(function () {
if (!root.shouldBeVisible || !root.contentLoader.item) {
return;
}
root.contentLoader.item.forceActiveFocus();
if (root.contentLoader.item.searchField) {
root.contentLoader.item.searchField.forceActiveFocus();
}
});
}
Connections {
target: clearConfirmDialog.modalFocusScope.Keys
function onPressed(event) {
if (!clearConfirmDialog.shouldBeVisible || event.key !== Qt.Key_Backtab) {
return;
}
clearConfirmDialog.selectedButton = clearConfirmDialog.selectedButton === -1 ? 1 : (clearConfirmDialog.selectedButton - 1 + 2) % 2;
clearConfirmDialog.keyboardNavigation = true;
event.accepted = true;
}
}
} }
content: Component { content: Component {
@@ -142,7 +104,7 @@ DankPopout {
clearConfirmDialog: clearConfirmDialog clearConfirmDialog: clearConfirmDialog
onCloseRequested: root.hide() onCloseRequested: root.hide()
onInstantCloseRequested: root.hide() onInstantCloseRequested: root.close()
Component.onCompleted: { Component.onCompleted: {
activeTab = root.activeTab; activeTab = root.activeTab;
@@ -125,6 +125,8 @@ QtObject {
if (!ClipboardService.keyboardNavigationActive) { if (!ClipboardService.keyboardNavigationActive) {
ClipboardService.keyboardNavigationActive = true; ClipboardService.keyboardNavigationActive = true;
ClipboardService.selectedIndex = 0; ClipboardService.selectedIndex = 0;
} else if (ClipboardService.selectedIndex === 0) {
ClipboardService.keyboardNavigationActive = false;
} else { } else {
selectPrevious(); selectPrevious();
} }
@@ -153,6 +155,8 @@ QtObject {
if (!ClipboardService.keyboardNavigationActive) { if (!ClipboardService.keyboardNavigationActive) {
ClipboardService.keyboardNavigationActive = true; ClipboardService.keyboardNavigationActive = true;
ClipboardService.selectedIndex = 0; ClipboardService.selectedIndex = 0;
} else if (ClipboardService.selectedIndex === 0) {
ClipboardService.keyboardNavigationActive = false;
} else { } else {
selectPrevious(); selectPrevious();
} }
@@ -180,7 +184,8 @@ QtObject {
if (event.modifiers & Qt.ShiftModifier) { if (event.modifiers & Qt.ShiftModifier) {
switch (event.key) { switch (event.key) {
case Qt.Key_Delete: case Qt.Key_Delete:
modal.confirmClearAll(); modal.clearAll();
modal.hide();
event.accepted = true; event.accepted = true;
return; return;
case Qt.Key_Return: case Qt.Key_Return:
@@ -363,7 +363,7 @@ FocusScope {
width: buttonContent.width + Theme.spacingM * 2 width: buttonContent.width + Theme.spacingM * 2
height: 28 height: 28
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: controller.searchMode === modelData.id ? Theme.buttonBg : modeArea.containsMouse ? Theme.surfaceContainerHighest : "transparent" color: controller.searchMode === modelData.id || modeArea.containsMouse ? Theme.primaryContainer : "transparent"
Row { Row {
id: buttonContent id: buttonContent
@@ -374,14 +374,14 @@ FocusScope {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
name: modelData.icon name: modelData.icon
size: 14 size: 14
color: controller.searchMode === modelData.id ? Theme.buttonText : Theme.surfaceVariantText color: controller.searchMode === modelData.id ? Theme.primary : Theme.surfaceVariantText
} }
StyledText { StyledText {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: modelData.label text: modelData.label
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: controller.searchMode === modelData.id ? Theme.buttonText : Theme.surfaceText color: controller.searchMode === modelData.id ? Theme.primary : Theme.surfaceText
} }
} }
@@ -636,7 +636,7 @@ FocusScope {
width: chipContent.width + Theme.spacingM * 2 width: chipContent.width + Theme.spacingM * 2
height: sortDropdown.height height: sortDropdown.height
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: controller.fileSearchType === modelData.id ? Theme.buttonBg : chipArea.containsMouse ? Theme.surfaceContainerHighest : "transparent" color: controller.fileSearchType === modelData.id || chipArea.containsMouse ? Theme.primaryContainer : "transparent"
Row { Row {
id: chipContent id: chipContent
@@ -647,14 +647,14 @@ FocusScope {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
name: modelData.icon name: modelData.icon
size: 14 size: 14
color: controller.fileSearchType === modelData.id ? Theme.buttonText : Theme.surfaceVariantText color: controller.fileSearchType === modelData.id ? Theme.primary : Theme.surfaceVariantText
} }
StyledText { StyledText {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: modelData.label text: modelData.label
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: controller.fileSearchType === modelData.id ? Theme.buttonText : Theme.surfaceVariantText color: controller.fileSearchType === modelData.id ? Theme.primary : Theme.surfaceText
} }
} }
@@ -201,21 +201,6 @@ FocusScope {
keyboardSelectionRequested = true; keyboardSelectionRequested = true;
} }
function activateFile(path, name, isDir) {
if (isDir) {
navigateTo(path);
return;
}
if (saveMode) {
saveRow.fileName = name;
pendingFilePath = path;
showOverwriteConfirmation = true;
} else {
fileSelected(path);
closeRequested();
}
}
function handleSaveFile(filePath) { function handleSaveFile(filePath) {
var normalizedPath = filePath; var normalizedPath = filePath;
if (!normalizedPath.startsWith("file://")) { if (!normalizedPath.startsWith("file://")) {
@@ -667,7 +652,6 @@ FocusScope {
Row { Row {
anchors.fill: parent anchors.fill: parent
anchors.bottomMargin: root.saveMode ? 40 + Theme.spacingL * 2 : 0
spacing: 0 spacing: 0
Row { Row {
@@ -772,7 +756,12 @@ FocusScope {
onItemClicked: (index, path, name, isDir) => { onItemClicked: (index, path, name, isDir) => {
selectedIndex = index; selectedIndex = index;
setSelectedFileData(path, name, isDir); setSelectedFileData(path, name, isDir);
root.activateFile(path, name, isDir); if (isDir) {
navigateTo(path);
} else {
fileSelected(path);
root.closeRequested();
}
} }
onItemSelected: (index, path, name, isDir) => { onItemSelected: (index, path, name, isDir) => {
setSelectedFileData(path, name, isDir); setSelectedFileData(path, name, isDir);
@@ -787,7 +776,12 @@ FocusScope {
root.keyboardSelectionRequested = false; root.keyboardSelectionRequested = false;
selectedIndex = index; selectedIndex = index;
setSelectedFileData(filePath, fileName, fileIsDir); setSelectedFileData(filePath, fileName, fileIsDir);
root.activateFile(filePath, fileName, fileIsDir); if (fileIsDir) {
navigateTo(filePath);
} else {
fileSelected(filePath);
root.closeRequested();
}
} }
} }
@@ -823,7 +817,12 @@ FocusScope {
onItemClicked: (index, path, name, isDir) => { onItemClicked: (index, path, name, isDir) => {
selectedIndex = index; selectedIndex = index;
setSelectedFileData(path, name, isDir); setSelectedFileData(path, name, isDir);
root.activateFile(path, name, isDir); if (isDir) {
navigateTo(path);
} else {
fileSelected(path);
root.closeRequested();
}
} }
onItemSelected: (index, path, name, isDir) => { onItemSelected: (index, path, name, isDir) => {
setSelectedFileData(path, name, isDir); setSelectedFileData(path, name, isDir);
@@ -838,7 +837,12 @@ FocusScope {
root.keyboardSelectionRequested = false; root.keyboardSelectionRequested = false;
selectedIndex = index; selectedIndex = index;
setSelectedFileData(filePath, fileName, fileIsDir); setSelectedFileData(filePath, fileName, fileIsDir);
root.activateFile(filePath, fileName, fileIsDir); if (fileIsDir) {
navigateTo(filePath);
} else {
fileSelected(filePath);
root.closeRequested();
}
} }
} }
@@ -851,7 +855,6 @@ FocusScope {
} }
FileBrowserSaveRow { FileBrowserSaveRow {
id: saveRow
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
@@ -910,21 +913,21 @@ FocusScope {
} }
} }
} }
}
FileBrowserOverwriteDialog { FileBrowserOverwriteDialog {
anchors.fill: parent anchors.fill: parent
showDialog: showOverwriteConfirmation showDialog: showOverwriteConfirmation
pendingFilePath: root.pendingFilePath pendingFilePath: root.pendingFilePath
onConfirmed: filePath => { onConfirmed: filePath => {
showOverwriteConfirmation = false; showOverwriteConfirmation = false;
fileSelected(filePath); fileSelected(filePath);
pendingFilePath = ""; pendingFilePath = "";
Qt.callLater(() => root.closeRequested()); Qt.callLater(() => root.closeRequested());
} }
onCancelled: { onCancelled: {
showOverwriteConfirmation = false; showOverwriteConfirmation = false;
pendingFilePath = ""; pendingFilePath = "";
}
} }
} }
@@ -74,7 +74,7 @@ Item {
width: 80 width: 80
height: 36 height: 36
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: cancelArea.containsMouse ? Qt.lighter(Theme.surfaceVariant, 1.2) : Theme.surfaceVariant color: cancelArea.containsMouse ? Theme.surfaceVariantHover : Theme.surfaceVariant
border.color: Theme.outline border.color: Theme.outline
border.width: 1 border.width: 1
@@ -8,7 +8,6 @@ Row {
property bool saveMode: false property bool saveMode: false
property string defaultFileName: "" property string defaultFileName: ""
property string currentPath: "" property string currentPath: ""
property alias fileName: fileNameInput.text
signal saveRequested(string filePath) signal saveRequested(string filePath)
-336
View File
@@ -1,336 +0,0 @@
import QtQml
import QtQuick
import QtQuick.Layouts
import qs.Common
import qs.Services
import qs.Widgets
FocusScope {
id: content
property real scrollStep: 60
property var activeFlickable: mainFlickable
property bool showFloatingToggle: true
property bool floating: false
property alias searchField: searchField
signal closeRequested
signal floatingToggleRequested
function scrollDown() {
if (!activeFlickable)
return;
let newY = activeFlickable.contentY + scrollStep;
newY = Math.min(newY, activeFlickable.contentHeight - activeFlickable.height);
activeFlickable.contentY = newY;
}
function scrollUp() {
if (!activeFlickable)
return;
let newY = activeFlickable.contentY - scrollStep;
newY = Math.max(0, newY);
activeFlickable.contentY = newY;
}
Keys.onPressed: event => {
switch (event.key) {
case Qt.Key_J:
if (event.modifiers & Qt.ControlModifier) {
scrollDown();
event.accepted = true;
}
return;
case Qt.Key_K:
if (event.modifiers & Qt.ControlModifier) {
scrollUp();
event.accepted = true;
}
return;
case Qt.Key_Down:
scrollDown();
event.accepted = true;
return;
case Qt.Key_Up:
scrollUp();
event.accepted = true;
return;
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
RowLayout {
width: parent.width
spacing: Theme.spacingM
StyledText {
Layout.alignment: Qt.AlignLeft
text: KeybindsService.cheatsheet.title || I18n.tr("Keybinds")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.primary
}
Item {
Layout.fillWidth: true
}
DankActionButton {
visible: content.showFloatingToggle
iconName: content.floating ? "close_fullscreen" : "open_in_new"
tooltipText: content.floating ? I18n.tr("Dock window") : I18n.tr("Open as window")
onClicked: content.floatingToggleRequested()
}
DankTextField {
id: searchField
Layout.alignment: Qt.AlignRight
leftIconName: "search"
keyForwardTargets: [content]
onTextEdited: searchDebounce.restart()
Keys.onEscapePressed: event => {
content.closeRequested();
event.accepted = true;
}
}
}
Timer {
id: searchDebounce
interval: 50
repeat: false
onTriggered: {
mainFlickable.categories = mainFlickable.generateCategories(searchField.text);
}
}
DankFlickable {
id: mainFlickable
width: parent.width
height: parent.height - parent.spacing - 40
contentWidth: rowLayout.implicitWidth
contentHeight: rowLayout.implicitHeight
clip: true
property var rawBinds: KeybindsService.cheatsheet.binds || {}
function generateCategories(query) {
const lowerQuery = query ? query.toLowerCase().trim() : "";
const lowerQueryWords = query.split(/\s+/);
const processed = {};
for (const cat in rawBinds) {
const binds = rawBinds[cat];
const catLower = cat.toLowerCase();
const subcats = {};
let hasSubcats = false;
for (let i = 0; i < binds.length; i++) {
const bind = binds[i];
const keyLower = (bind.key || "").toLowerCase();
const descLower = (bind.desc || "").toLowerCase();
const actionLower = (bind.action || "").toLowerCase();
if (bind.hideOnOverlay)
continue;
let shouldContinue = false;
for (let j = 0; j < lowerQueryWords.length; j++) {
const word = lowerQueryWords[j];
if (!(word.length === 0 || keyLower.includes(word) || descLower.includes(word) || catLower.includes(word) || actionLower.includes(word))) {
shouldContinue = true;
break;
}
}
if (shouldContinue)
continue;
if (bind.subcat) {
hasSubcats = true;
if (!subcats[bind.subcat])
subcats[bind.subcat] = [];
subcats[bind.subcat].push(bind);
} else {
if (!subcats["_root"])
subcats["_root"] = [];
subcats["_root"].push(bind);
}
}
if (Object.keys(subcats).length === 0)
continue;
processed[cat] = {
hasSubcats: hasSubcats,
subcats: subcats,
subcatKeys: Object.keys(subcats)
};
}
return processed;
}
property var categories: generateCategories("")
function estimateCategoryHeight(catName) {
const catData = categories[catName];
if (!catData)
return 0;
let bindCount = 0;
for (const key of catData.subcatKeys) {
bindCount += catData.subcats[key]?.length || 0;
if (key !== "_root")
bindCount += 1;
}
return 40 + bindCount * 28;
}
property var categoryKeys: Object.keys(categories)
function distributeCategories(cols) {
const columns = [];
const heights = [];
for (let i = 0; i < cols; i++) {
columns.push([]);
heights.push(0);
}
const sorted = [...categoryKeys].sort((a, b) => estimateCategoryHeight(b) - estimateCategoryHeight(a));
for (const cat of sorted) {
let minIdx = 0;
for (let i = 1; i < cols; i++) {
if (heights[i] < heights[minIdx])
minIdx = i;
}
columns[minIdx].push(cat);
heights[minIdx] += estimateCategoryHeight(cat);
}
return columns;
}
Row {
id: rowLayout
width: mainFlickable.width
spacing: Theme.spacingM
property int numColumns: Math.max(1, Math.min(3, Math.floor(width / 350)))
property var columnCategories: mainFlickable.distributeCategories(numColumns)
Repeater {
model: rowLayout.numColumns
Column {
id: masonryColumn
width: (rowLayout.width - rowLayout.spacing * (rowLayout.numColumns - 1)) / rowLayout.numColumns
spacing: Theme.spacingXL
Repeater {
model: rowLayout.columnCategories[index] || []
Column {
id: categoryColumn
width: parent.width
spacing: Theme.spacingXS
property string catName: modelData
property var catData: mainFlickable.categories[catName]
StyledText {
text: categoryColumn.catName
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.primary
}
Rectangle {
width: parent.width
height: 1
color: Theme.primary
opacity: 0.3
}
Item {
width: 1
height: Theme.spacingXS
}
Column {
width: parent.width
spacing: Theme.spacingM
Repeater {
model: categoryColumn.catData?.subcatKeys || []
Column {
width: parent.width
spacing: Theme.spacingXS
property string subcatName: modelData
property var subcatBinds: categoryColumn.catData?.subcats?.[subcatName] || []
StyledText {
visible: parent.subcatName !== "_root"
text: parent.subcatName
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
color: Theme.primary
opacity: 0.7
}
Column {
width: parent.width
spacing: Theme.spacingXS
Repeater {
model: parent.parent.subcatBinds
Item {
width: parent.width
height: 24
StyledRect {
id: keyBadge
width: Math.min(keyText.implicitWidth + 12, 160)
height: 22
radius: 4
anchors.verticalCenter: parent.verticalCenter
StyledText {
id: keyText
anchors.centerIn: parent
color: Theme.secondary
text: (modelData.key || "").replace(/\+/g, " + ")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
isMonospace: true
elide: Text.ElideRight
width: Math.min(implicitWidth, 148)
}
}
StyledText {
anchors.left: parent.left
anchors.leftMargin: 170
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: modelData.desc || modelData.action || ""
font.pixelSize: Theme.fontSizeSmall
opacity: 0.9
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
+314 -58
View File
@@ -1,78 +1,334 @@
import QtQml
import QtQuick import QtQuick
import QtQuick.Layouts
import qs.Common import qs.Common
import qs.Modals import qs.Modals.Common
import qs.Services import qs.Services
import qs.Widgets
Item { DankModal {
id: root id: root
readonly property bool floating: SettingsData.keybindsFloatingWindow layerNamespace: "dms:keybinds"
readonly property bool shouldBeVisible: floating ? (windowLoader.item ? windowLoader.item.visible : false) : (overlayLoader.item ? overlayLoader.item.shouldBeVisible : false) useOverlayLayer: true
property real scrollStep: 60
property var activeFlickable: null
property real _maxW: Math.min(root.screenWidth * 0.92, 1200)
property real _maxH: Math.min(root.screenHeight * 0.92, 900)
modalWidth: _maxW
modalHeight: _maxH
onBackgroundClicked: close()
onOpened: {
Qt.callLater(() => {
modalFocusScope.forceActiveFocus();
if (contentLoader.item?.searchField)
contentLoader.item.searchField.forceActiveFocus();
});
if (!Object.keys(KeybindsService.cheatsheet).length && KeybindsService.cheatsheetAvailable)
KeybindsService.loadCheatsheet();
}
function open() { function scrollDown() {
if (floating) { if (!root.activeFlickable)
windowLoader.active = true;
windowLoader.item.show();
return; return;
} let newY = root.activeFlickable.contentY + scrollStep;
overlayLoader.active = true; newY = Math.min(newY, root.activeFlickable.contentHeight - root.activeFlickable.height);
overlayLoader.item.open(); root.activeFlickable.contentY = newY;
} }
function close() { function scrollUp() {
if (windowLoader.item) if (!root.activeFlickable)
windowLoader.item.hide();
if (overlayLoader.item)
overlayLoader.item.close();
}
function toggle() {
if (shouldBeVisible)
close();
else
open();
}
function _switchFloating(toFloating) {
if (toFloating) {
if (overlayLoader.item)
overlayLoader.item.close();
SettingsData.keybindsFloatingWindow = true;
windowLoader.active = true;
windowLoader.item.show();
return; return;
} let newY = root.activeFlickable.contentY - root.scrollStep;
if (windowLoader.item) newY = Math.max(0, newY);
windowLoader.item.hide(); root.activeFlickable.contentY = newY;
SettingsData.keybindsFloatingWindow = false;
overlayLoader.active = true;
overlayLoader.item.open();
} }
Loader { modalFocusScope.Keys.onPressed: event => {
id: overlayLoader if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
active: false scrollDown();
asynchronous: false event.accepted = true;
} else if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
sourceComponent: KeybindsModalOverlay { scrollUp();
onFloatingToggleRequested: root._switchFloating(true) event.accepted = true;
onDialogClosed: Qt.callLater(() => { } else if (event.key === Qt.Key_Down) {
if (!shouldBeVisible) scrollDown();
overlayLoader.active = false; event.accepted = true;
}) } else if (event.key === Qt.Key_Up) {
scrollUp();
event.accepted = true;
} }
} }
Loader { content: Component {
id: windowLoader Item {
active: false anchors.fill: parent
asynchronous: false property alias searchField: searchField
sourceComponent: KeybindsModalWindow { Column {
onFloatingToggleRequested: root._switchFloating(false) anchors.fill: parent
onVisibleChanged: { anchors.margins: Theme.spacingL
if (!visible) spacing: Theme.spacingL
Qt.callLater(() => windowLoader.active = false);
RowLayout {
width: parent.width
StyledText {
Layout.alignment: Qt.AlignLeft
text: KeybindsService.cheatsheet.title || I18n.tr("Keybinds")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.primary
}
DankTextField {
id: searchField
Layout.alignment: Qt.AlignRight
leftIconName: "search"
keyForwardTargets: [root.modalFocusScope]
onTextEdited: searchDebounce.restart()
Keys.onEscapePressed: event => {
root.close();
event.accepted = true;
}
}
}
Timer {
id: searchDebounce
interval: 50
repeat: false
onTriggered: {
mainFlickable.categories = mainFlickable.generateCategories(searchField.text);
}
}
DankFlickable {
id: mainFlickable
width: parent.width
height: parent.height - parent.spacing - 40
contentWidth: rowLayout.implicitWidth
contentHeight: rowLayout.implicitHeight
clip: true
Component.onCompleted: root.activeFlickable = mainFlickable
property var rawBinds: KeybindsService.cheatsheet.binds || {}
function generateCategories(query) {
const lowerQuery = query ? query.toLowerCase().trim() : "";
const lowerQueryWords = query.split(/\s+/);
const processed = {};
for (const cat in rawBinds) {
const binds = rawBinds[cat];
const catLower = cat.toLowerCase();
const subcats = {};
let hasSubcats = false;
for (let i = 0; i < binds.length; i++) {
const bind = binds[i];
const keyLower = (bind.key || "").toLowerCase();
const descLower = (bind.desc || "").toLowerCase();
const actionLower = (bind.action || "").toLowerCase();
if (bind.hideOnOverlay)
continue;
let shouldContinue = false;
for (let j = 0; j < lowerQueryWords.length; j++) {
const word = lowerQueryWords[j];
if (!(word.length === 0 || keyLower.includes(word) || descLower.includes(word) || catLower.includes(word) || actionLower.includes(word))) {
shouldContinue = true;
break;
}
}
if (shouldContinue)
continue;
if (bind.subcat) {
hasSubcats = true;
if (!subcats[bind.subcat])
subcats[bind.subcat] = [];
subcats[bind.subcat].push(bind);
} else {
if (!subcats["_root"])
subcats["_root"] = [];
subcats["_root"].push(bind);
}
}
if (Object.keys(subcats).length === 0)
continue;
processed[cat] = {
hasSubcats: hasSubcats,
subcats: subcats,
subcatKeys: Object.keys(subcats)
};
}
return processed;
}
property var categories: generateCategories("")
function estimateCategoryHeight(catName) {
const catData = categories[catName];
if (!catData)
return 0;
let bindCount = 0;
for (const key of catData.subcatKeys) {
bindCount += catData.subcats[key]?.length || 0;
if (key !== "_root")
bindCount += 1;
}
return 40 + bindCount * 28;
}
property var categoryKeys: Object.keys(categories)
function distributeCategories(cols) {
const columns = [];
const heights = [];
for (let i = 0; i < cols; i++) {
columns.push([]);
heights.push(0);
}
const sorted = [...categoryKeys].sort((a, b) => estimateCategoryHeight(b) - estimateCategoryHeight(a));
for (const cat of sorted) {
let minIdx = 0;
for (let i = 1; i < cols; i++) {
if (heights[i] < heights[minIdx])
minIdx = i;
}
columns[minIdx].push(cat);
heights[minIdx] += estimateCategoryHeight(cat);
}
return columns;
}
Row {
id: rowLayout
width: mainFlickable.width
spacing: Theme.spacingM
property int numColumns: Math.max(1, Math.min(3, Math.floor(width / 350)))
property var columnCategories: mainFlickable.distributeCategories(numColumns)
Repeater {
model: rowLayout.numColumns
Column {
id: masonryColumn
width: (rowLayout.width - rowLayout.spacing * (rowLayout.numColumns - 1)) / rowLayout.numColumns
spacing: Theme.spacingXL
Repeater {
model: rowLayout.columnCategories[index] || []
Column {
id: categoryColumn
width: parent.width
spacing: Theme.spacingXS
property string catName: modelData
property var catData: mainFlickable.categories[catName]
StyledText {
text: categoryColumn.catName
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.primary
}
Rectangle {
width: parent.width
height: 1
color: Theme.primary
opacity: 0.3
}
Item {
width: 1
height: Theme.spacingXS
}
Column {
width: parent.width
spacing: Theme.spacingM
Repeater {
model: categoryColumn.catData?.subcatKeys || []
Column {
width: parent.width
spacing: Theme.spacingXS
property string subcatName: modelData
property var subcatBinds: categoryColumn.catData?.subcats?.[subcatName] || []
StyledText {
visible: parent.subcatName !== "_root"
text: parent.subcatName
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
color: Theme.primary
opacity: 0.7
}
Column {
width: parent.width
spacing: Theme.spacingXS
Repeater {
model: parent.parent.subcatBinds
Item {
width: parent.width
height: 24
StyledRect {
id: keyBadge
width: Math.min(keyText.implicitWidth + 12, 160)
height: 22
radius: 4
anchors.verticalCenter: parent.verticalCenter
StyledText {
id: keyText
anchors.centerIn: parent
color: Theme.secondary
text: (modelData.key || "").replace(/\+/g, " + ")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
isMonospace: true
elide: Text.ElideRight
width: Math.min(implicitWidth, 148)
}
}
StyledText {
anchors.left: parent.left
anchors.leftMargin: 170
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: modelData.desc || modelData.action || ""
font.pixelSize: Theme.fontSizeSmall
opacity: 0.9
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
}
}
}
}
}
}
}
}
}
}
}
} }
} }
} }
@@ -1,38 +0,0 @@
import QtQml
import QtQuick
import qs.Common
import qs.Modals
import qs.Modals.Common
import qs.Services
DankModal {
id: overlay
signal floatingToggleRequested
layerNamespace: "dms:keybinds"
useOverlayLayer: true
property real _maxW: Math.min(overlay.screenWidth * 0.92, 1200)
property real _maxH: Math.min(overlay.screenHeight * 0.92, 900)
modalWidth: _maxW
modalHeight: _maxH
onBackgroundClicked: close()
onOpened: {
Qt.callLater(() => {
modalFocusScope.forceActiveFocus();
if (contentLoader.item?.searchField)
contentLoader.item.searchField.forceActiveFocus();
});
if (!Object.keys(KeybindsService.cheatsheet).length && KeybindsService.cheatsheetAvailable)
KeybindsService.loadCheatsheet();
}
content: Component {
KeybindsContent {
showFloatingToggle: true
floating: false
onCloseRequested: overlay.close()
onFloatingToggleRequested: overlay.floatingToggleRequested()
}
}
}
-140
View File
@@ -1,140 +0,0 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Modals
import qs.Services
import qs.Widgets
FloatingWindow {
id: win
property bool disablePopupTransparency: true
property alias shouldBeVisible: win.visible
signal floatingToggleRequested
function show() {
visible = true;
}
function hide() {
visible = false;
}
function toggle() {
visible = !visible;
}
objectName: "keybindsModalWindow"
title: I18n.tr("Keybinds")
minimumSize: Qt.size(Math.min(560, Screen.width), Math.min(400, Screen.height))
implicitWidth: 1000
implicitHeight: screen ? Math.min(820, screen.height - 100) : 820
color: Theme.surfaceContainer
visible: false
onVisibleChanged: {
if (!visible)
return;
if (!Object.keys(KeybindsService.cheatsheet).length && KeybindsService.cheatsheetAvailable)
KeybindsService.loadCheatsheet();
Qt.callLater(() => {
keybindsContent.forceActiveFocus();
keybindsContent.searchField.forceActiveFocus();
});
}
onClosed: win.visible = false
Column {
anchors.fill: parent
spacing: 0
Item {
width: parent.width
height: 48
z: 10
MouseArea {
anchors.fill: parent
onPressed: windowControls.tryStartMove()
onDoubleClicked: windowControls.tryToggleMaximize()
}
Rectangle {
anchors.fill: parent
color: Theme.surfaceContainer
opacity: 0.5
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingL
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "keyboard"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: KeybindsService.cheatsheet.title || I18n.tr("Keybinds")
font.pixelSize: Theme.fontSizeXLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
DankActionButton {
circular: false
iconName: "close_fullscreen"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
tooltipText: I18n.tr("Dock window")
onClicked: win.floatingToggleRequested()
}
DankActionButton {
visible: windowControls.canMaximize
circular: false
iconName: win.maximized ? "fullscreen_exit" : "fullscreen"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: windowControls.tryToggleMaximize()
}
DankActionButton {
circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: win.hide()
}
}
}
KeybindsContent {
id: keybindsContent
width: parent.width
height: parent.height - 48
showFloatingToggle: false
floating: true
onCloseRequested: win.hide()
}
}
FloatingWindowControls {
id: windowControls
targetWindow: win
}
}
-1
View File
@@ -11,7 +11,6 @@ DankModal {
layerNamespace: "dms:power-menu" layerNamespace: "dms:power-menu"
keepPopoutsOpen: true keepPopoutsOpen: true
useOverlayLayer: true
property int selectedIndex: 0 property int selectedIndex: 0
property int selectedRow: 0 property int selectedRow: 0
+1 -62
View File
@@ -1,7 +1,6 @@
import QtQuick import QtQuick
import qs.Common import qs.Common
import qs.Modules.Settings import qs.Modules.Settings
import qs.Services
import qs.Widgets import qs.Widgets
FocusScope { FocusScope {
@@ -233,52 +232,7 @@ FocusScope {
visible: active visible: active
focus: active focus: active
sourceComponent: NetworkStatusTab {} sourceComponent: NetworkTab {}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
Loader {
id: networkEthernetLoader
anchors.fill: parent
active: root.currentIndex === 39
visible: active
focus: active
sourceComponent: NetworkEthernetTab {}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
Loader {
id: networkWifiLoader
anchors.fill: parent
active: root.currentIndex === 40
visible: active
focus: active
sourceComponent: NetworkWifiTab {}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
Loader {
id: networkVpnLoader
anchors.fill: parent
active: root.currentIndex === 41
visible: active
focus: active
sourceComponent: NetworkVpnTab {}
onActiveChanged: { onActiveChanged: {
if (active && item) if (active && item)
@@ -686,20 +640,5 @@ FocusScope {
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
} }
} }
Loader {
id: batteryLoader
anchors.fill: parent
active: root.currentIndex === 42
visible: active
focus: active
sourceComponent: BatteryTab {}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
} }
} }
+8 -9
View File
@@ -53,21 +53,20 @@ FloatingWindow {
visible = !visible; visible = !visible;
} }
function setTabIndex(tabIndex: int) {
if (tabIndex < 0)
return;
currentTabIndex = tabIndex;
sidebar.autoExpandForTab(tabIndex);
}
function showWithTab(tabIndex: int) { function showWithTab(tabIndex: int) {
setTabIndex(tabIndex); if (tabIndex >= 0) {
currentTabIndex = tabIndex;
sidebar.autoExpandForTab(tabIndex);
}
visible = true; visible = true;
} }
function showWithTabName(tabName: string) { function showWithTabName(tabName: string) {
var idx = sidebar.resolveTabIndex(tabName); var idx = sidebar.resolveTabIndex(tabName);
setTabIndex(idx); if (idx >= 0) {
currentTabIndex = idx;
sidebar.autoExpandForTab(idx);
}
visible = true; visible = true;
} }
+10 -41
View File
@@ -105,8 +105,8 @@ Rectangle {
}, },
{ {
"id": "compositor_layout", "id": "compositor_layout",
"text": CompositorService.isNiri ? "Niri" : (CompositorService.isHyprland ? "Hyprland" : "MangoWC"), "text": CompositorService.isNiri ? "niri" : (CompositorService.isHyprland ? "Hyprland" : "MangoWC"),
"icon": "layers", "icon": "crop_square",
"tabIndex": 37, "tabIndex": 37,
"layoutCapable": true "layoutCapable": true
} }
@@ -117,18 +117,18 @@ Rectangle {
"text": I18n.tr("Dank Bar"), "text": I18n.tr("Dank Bar"),
"icon": "toolbar", "icon": "toolbar",
"children": [ "children": [
{
"id": "dankbar_appearance",
"text": I18n.tr("Appearance"),
"icon": "palette",
"tabIndex": 6
},
{ {
"id": "dankbar_settings", "id": "dankbar_settings",
"text": I18n.tr("Settings"), "text": I18n.tr("Settings"),
"icon": "tune", "icon": "tune",
"tabIndex": 3 "tabIndex": 3
}, },
{
"id": "dankbar_appearance",
"text": I18n.tr("Appearance"),
"icon": "palette",
"tabIndex": 6
},
{ {
"id": "dankbar_widgets", "id": "dankbar_widgets",
"text": I18n.tr("Widgets"), "text": I18n.tr("Widgets"),
@@ -238,33 +238,8 @@ Rectangle {
"id": "network", "id": "network",
"text": I18n.tr("Network"), "text": I18n.tr("Network"),
"icon": "wifi", "icon": "wifi",
"dmsOnly": true, "tabIndex": 7,
"children": [ "dmsOnly": true
{
"id": "network_status",
"text": I18n.tr("Status"),
"icon": "lan",
"tabIndex": 7
},
{
"id": "network_ethernet",
"text": I18n.tr("Ethernet"),
"icon": "settings_ethernet",
"tabIndex": 39
},
{
"id": "network_wifi",
"text": I18n.tr("WiFi"),
"icon": "wifi",
"tabIndex": 40
},
{
"id": "network_vpn",
"text": I18n.tr("VPN"),
"icon": "vpn_key",
"tabIndex": 41
}
]
}, },
{ {
"id": "applications", "id": "applications",
@@ -377,12 +352,6 @@ Rectangle {
"text": I18n.tr("Power & Sleep"), "text": I18n.tr("Power & Sleep"),
"icon": "power_settings_new", "icon": "power_settings_new",
"tabIndex": 21 "tabIndex": 21
},
{
"id": "battery",
"text": I18n.tr("Battery"),
"icon": "battery_charging_full",
"tabIndex": 42
} }
] ]
}, },
+45 -28
View File
@@ -1,22 +1,12 @@
import QtQuick import QtQuick
import Quickshell
import qs.Common import qs.Common
import qs.Modals.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
DankModal { FloatingWindow {
id: root id: root
layerNamespace: "dms:wifi-password"
keepPopoutsOpen: true
allowStacking: true
shouldBeVisible: false
modalWidth: 420
modalHeight: calculatedHeight
enableShadow: true
onBackgroundClicked: clearAndClose()
directContent: contentFocusScope
property bool disablePopupTransparency: true property bool disablePopupTransparency: true
property string wifiPasswordSSID: "" property string wifiPasswordSSID: ""
property string wifiPasswordInput: "" property string wifiPasswordInput: ""
@@ -112,7 +102,7 @@ DankModal {
const network = NetworkService.wifiNetworks.find(n => n.ssid === ssid); const network = NetworkService.wifiNetworks.find(n => n.ssid === ssid);
requiresEnterprise = network?.enterprise || false; requiresEnterprise = network?.enterprise || false;
open(); visible = true;
Qt.callLater(focusFirstField); Qt.callLater(focusFirstField);
} }
@@ -136,7 +126,7 @@ DankModal {
secretValues = {}; secretValues = {};
requiresEnterprise = false; requiresEnterprise = false;
open(); visible = true;
Qt.callLater(focusFirstField); Qt.callLater(focusFirstField);
} }
@@ -154,7 +144,6 @@ DankModal {
isVpnPrompt = (connectionType === "vpn" || connectionType === "wireguard"); isVpnPrompt = (connectionType === "vpn" || connectionType === "wireguard");
wifiPasswordSSID = isVpnPrompt ? connectionName : ssid; wifiPasswordSSID = isVpnPrompt ? connectionName : ssid;
savePasswordCheckbox.checked = !isVpnPrompt;
requiresEnterprise = setting === "802-1x"; requiresEnterprise = setting === "802-1x";
@@ -163,7 +152,7 @@ DankModal {
wifiAnonymousIdentityInput = ""; wifiAnonymousIdentityInput = "";
wifiDomainInput = ""; wifiDomainInput = "";
open(); visible = true;
Qt.callLater(() => { Qt.callLater(() => {
if (reason === "wrong-password" && fieldsInfo.length === 0) { if (reason === "wrong-password" && fieldsInfo.length === 0) {
passwordInput.text = ""; passwordInput.text = "";
@@ -173,7 +162,7 @@ DankModal {
} }
function hide() { function hide() {
close(); visible = false;
} }
function getFieldLabel(fieldName) { function getFieldLabel(fieldName) {
@@ -253,8 +242,23 @@ DankModal {
secretValues = {}; secretValues = {};
} }
onShouldBeVisibleChanged: { objectName: "wifiPasswordModal"
if (shouldBeVisible) { title: {
if (promptReason === "pkcs11")
return I18n.tr("Smartcard PIN");
if (isVpnPrompt)
return I18n.tr("VPN Password");
if (isHiddenNetwork)
return I18n.tr("Hidden Network");
return I18n.tr("Wi-Fi Password");
}
minimumSize: Qt.size(420, calculatedHeight)
maximumSize: Qt.size(420, calculatedHeight)
color: Theme.surfaceContainer
visible: false
onVisibleChanged: {
if (visible) {
Qt.callLater(focusFirstField); Qt.callLater(focusFirstField);
return; return;
} }
@@ -283,7 +287,7 @@ DankModal {
return; return;
wifiPasswordSSID = NetworkService.connectingSSID; wifiPasswordSSID = NetworkService.connectingSSID;
wifiPasswordInput = ""; wifiPasswordInput = "";
open(); visible = true;
NetworkService.passwordDialogShouldReopen = false; NetworkService.passwordDialogShouldReopen = false;
} }
} }
@@ -292,7 +296,7 @@ DankModal {
id: contentFocusScope id: contentFocusScope
anchors.fill: parent anchors.fill: parent
focus: root.shouldBeVisible focus: true
Keys.onEscapePressed: event => { Keys.onEscapePressed: event => {
clearAndClose(); clearAndClose();
@@ -314,6 +318,8 @@ DankModal {
anchors.right: buttonRow.left anchors.right: buttonRow.left
anchors.rightMargin: Theme.spacingM anchors.rightMargin: Theme.spacingM
height: headerCol.height height: headerCol.height
onPressed: windowControls.tryStartMove()
onDoubleClicked: windowControls.tryToggleMaximize()
Column { Column {
id: headerCol id: headerCol
@@ -374,6 +380,14 @@ DankModal {
anchors.right: parent.right anchors.right: parent.right
spacing: Theme.spacingXS spacing: Theme.spacingXS
DankActionButton {
visible: windowControls.canMaximize
iconName: root.maximized ? "fullscreen_exit" : "fullscreen"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: windowControls.tryToggleMaximize()
}
DankActionButton { DankActionButton {
iconName: "close" iconName: "close"
iconSize: Theme.iconSize - 4 iconSize: Theme.iconSize - 4
@@ -405,7 +419,7 @@ DankModal {
textColor: Theme.surfaceText textColor: Theme.surfaceText
placeholderText: I18n.tr("Network Name (SSID)") placeholderText: I18n.tr("Network Name (SSID)")
backgroundColor: "transparent" backgroundColor: "transparent"
enabled: root.shouldBeVisible enabled: root.visible
keyNavigationTab: passwordInput keyNavigationTab: passwordInput
onAccepted: passwordInput.forceActiveFocus() onAccepted: passwordInput.forceActiveFocus()
} }
@@ -435,7 +449,7 @@ DankModal {
echoMode: modelData.isSecret && !passwordVisible ? TextInput.Password : TextInput.Normal echoMode: modelData.isSecret && !passwordVisible ? TextInput.Password : TextInput.Normal
placeholderText: getFieldLabel(modelData.name) placeholderText: getFieldLabel(modelData.name)
backgroundColor: "transparent" backgroundColor: "transparent"
enabled: root.shouldBeVisible enabled: root.visible
Keys.onTabPressed: event => { Keys.onTabPressed: event => {
if (index < fieldsInfo.length - 1) { if (index < fieldsInfo.length - 1) {
@@ -505,7 +519,7 @@ DankModal {
text: wifiUsernameInput text: wifiUsernameInput
placeholderText: I18n.tr("Username") placeholderText: I18n.tr("Username")
backgroundColor: "transparent" backgroundColor: "transparent"
enabled: root.shouldBeVisible enabled: root.visible
keyNavigationTab: passwordInput keyNavigationTab: passwordInput
keyNavigationBacktab: domainMatchInput keyNavigationBacktab: domainMatchInput
onTextEdited: wifiUsernameInput = text onTextEdited: wifiUsernameInput = text
@@ -538,7 +552,7 @@ DankModal {
echoMode: passwordVisible ? TextInput.Normal : TextInput.Password echoMode: passwordVisible ? TextInput.Normal : TextInput.Password
placeholderText: (requiresEnterprise && !isVpnPrompt) ? I18n.tr("Password") : "" placeholderText: (requiresEnterprise && !isVpnPrompt) ? I18n.tr("Password") : ""
backgroundColor: "transparent" backgroundColor: "transparent"
enabled: root.shouldBeVisible enabled: root.visible
keyNavigationTab: (requiresEnterprise && !isVpnPrompt) ? anonInput : null keyNavigationTab: (requiresEnterprise && !isVpnPrompt) ? anonInput : null
keyNavigationBacktab: (requiresEnterprise && !isVpnPrompt) ? usernameInput : null keyNavigationBacktab: (requiresEnterprise && !isVpnPrompt) ? usernameInput : null
onTextEdited: wifiPasswordInput = text onTextEdited: wifiPasswordInput = text
@@ -575,7 +589,7 @@ DankModal {
text: wifiAnonymousIdentityInput text: wifiAnonymousIdentityInput
placeholderText: I18n.tr("Anonymous Identity (optional)") placeholderText: I18n.tr("Anonymous Identity (optional)")
backgroundColor: "transparent" backgroundColor: "transparent"
enabled: root.shouldBeVisible enabled: root.visible
keyNavigationTab: domainMatchInput keyNavigationTab: domainMatchInput
keyNavigationBacktab: passwordInput keyNavigationBacktab: passwordInput
onTextEdited: wifiAnonymousIdentityInput = text onTextEdited: wifiAnonymousIdentityInput = text
@@ -606,7 +620,7 @@ DankModal {
text: wifiDomainInput text: wifiDomainInput
placeholderText: I18n.tr("Domain (optional)") placeholderText: I18n.tr("Domain (optional)")
backgroundColor: "transparent" backgroundColor: "transparent"
enabled: root.shouldBeVisible enabled: root.visible
keyNavigationTab: usernameInput keyNavigationTab: usernameInput
keyNavigationBacktab: anonInput keyNavigationBacktab: anonInput
onTextEdited: wifiDomainInput = text onTextEdited: wifiDomainInput = text
@@ -743,5 +757,8 @@ DankModal {
} }
} }
onOpened: Qt.callLater(() => contentFocusScope.forceActiveFocus()) FloatingWindowControls {
id: windowControls
targetWindow: root
}
} }
@@ -7,7 +7,6 @@ import qs.Widgets
import qs.Services import qs.Services
Variants { Variants {
readonly property var log: Log.scoped("BlurredWallpaperBackground")
model: { model: {
if (SessionData.isGreeterMode) { if (SessionData.isGreeterMode) {
return Quickshell.screens; return Quickshell.screens;
@@ -33,8 +32,6 @@ Variants {
color: "transparent" color: "transparent"
updatesEnabled: root.renderActive || root._settleFrames > 0
mask: Region { mask: Region {
item: Item {} item: Item {}
} }
@@ -88,6 +85,7 @@ Variants {
} }
Component.onCompleted: { Component.onCompleted: {
blurWallpaperWindow.updatesEnabled = Qt.binding(() => !root.source || root.effectActive || root._renderSettling || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading);
isInitialized = true; isInitialized = true;
} }
@@ -95,67 +93,51 @@ Variants {
property real transitionProgress: 0 property real transitionProgress: 0
readonly property bool transitioning: transitionAnimation.running readonly property bool transitioning: transitionAnimation.running
property bool effectActive: false property bool effectActive: false
property bool _renderSettling: true
property bool useNextForEffect: false property bool useNextForEffect: false
readonly property var backingWindow: Window.window
readonly property bool renderActive: !source || effectActive || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading
property int _settleFrames: 3
function invalidate() {
_settleFrames = 3;
backingWindow?.update();
}
onRenderActiveChanged: invalidate()
onBackingWindowChanged: invalidate()
Connections { Connections {
target: root.backingWindow target: currentWallpaper
function onFrameSwapped() { function onStatusChanged() {
if (root._settleFrames > 0) if (currentWallpaper.status !== Image.Ready && currentWallpaper.status !== Image.Error)
root._settleFrames--; return;
} root._renderSettling = true;
function onVisibleChanged() { renderSettleTimer.restart();
root.invalidate();
} }
}
Connections {
target: blurWallpaperWindow
function onWidthChanged() { function onWidthChanged() {
root.invalidate(); root._renderSettling = true;
renderSettleTimer.restart();
} }
function onHeightChanged() { function onHeightChanged() {
root.invalidate(); root._renderSettling = true;
renderSettleTimer.restart();
} }
} }
Connections { Connections {
target: Quickshell target: Quickshell
function onScreensChanged() { function onScreensChanged() {
root.invalidate(); root._renderSettling = true;
renderSettleTimer.restart();
} }
} }
Connections { Connections {
target: SettingsData target: SettingsData
function onWallpaperFillModeChanged() { function onWallpaperFillModeChanged() {
root.invalidate(); root._renderSettling = true;
renderSettleTimer.restart();
} }
} }
Connections { Timer {
target: IdleService id: renderSettleTimer
function onIsShellLockedChanged() { interval: 1000
if (IdleService.isShellLocked) onTriggered: root._renderSettling = false
return;
root.invalidate();
}
}
function handleTransitionLoadError(failedSource) {
log.warn("failed to load candidate wallpaper for", modelData.name + ":", failedSource);
transitionDelayTimer.stop();
transitionAnimation.stop();
root.useNextForEffect = false;
root.effectActive = false;
root.transitionProgress = 0.0;
nextWallpaper.source = "";
} }
onSourceChanged: { onSourceChanged: {
@@ -182,6 +164,8 @@ Variants {
transitionAnimation.stop(); transitionAnimation.stop();
root.transitionProgress = 0.0; root.transitionProgress = 0.0;
root.effectActive = false; root.effectActive = false;
root._renderSettling = true;
renderSettleTimer.restart();
currentWallpaper.source = newSource; currentWallpaper.source = newSource;
nextWallpaper.source = ""; nextWallpaper.source = "";
} }
@@ -210,6 +194,8 @@ Variants {
transitionAnimation.stop(); transitionAnimation.stop();
root.transitionProgress = 0; root.transitionProgress = 0;
root.effectActive = false; root.effectActive = false;
root._renderSettling = true;
renderSettleTimer.restart();
currentWallpaper.source = nextWallpaper.source; currentWallpaper.source = nextWallpaper.source;
nextWallpaper.source = ""; nextWallpaper.source = "";
} }
@@ -218,6 +204,9 @@ Variants {
return; return;
} }
root._renderSettling = true;
renderSettleTimer.restart();
nextWallpaper.source = newPath; nextWallpaper.source = newPath;
if (nextWallpaper.status === Image.Ready) if (nextWallpaper.status === Image.Ready)
@@ -226,7 +215,7 @@ Variants {
Loader { Loader {
anchors.fill: parent anchors.fill: parent
active: !root.source || root.isColorSource || currentWallpaper.status === Image.Error active: !root.source || root.isColorSource
asynchronous: true asynchronous: true
sourceComponent: DankBackdrop { sourceComponent: DankBackdrop {
@@ -249,12 +238,6 @@ Variants {
cache: true cache: true
sourceSize: Qt.size(root.textureWidth, root.textureHeight) sourceSize: Qt.size(root.textureWidth, root.textureHeight)
fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SessionData.getMonitorWallpaperFillMode(modelData.name)) fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SessionData.getMonitorWallpaperFillMode(modelData.name))
onStatusChanged: {
if (status === Image.Error) {
log.warn("failed to load active wallpaper for", modelData.name + ":", source);
}
}
} }
Image { Image {
@@ -270,10 +253,6 @@ Variants {
fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SessionData.getMonitorWallpaperFillMode(modelData.name)) fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SessionData.getMonitorWallpaperFillMode(modelData.name))
onStatusChanged: { onStatusChanged: {
if (status === Image.Error) {
root.handleTransitionLoadError(source);
return;
}
if (status !== Image.Ready) if (status !== Image.Ready)
return; return;
if (!root.transitioning) { if (!root.transitioning) {
@@ -350,6 +329,8 @@ Variants {
root.useNextForEffect = false; root.useNextForEffect = false;
nextWallpaper.source = ""; nextWallpaper.source = "";
root.transitionProgress = 0.0; root.transitionProgress = 0.0;
root._renderSettling = true;
renderSettleTimer.restart();
root.effectActive = false; root.effectActive = false;
} }
} }
@@ -25,14 +25,7 @@ PluginComponent {
} }
ccWidgetIsActive: TailscaleService.connected ccWidgetIsActive: TailscaleService.connected
onCcWidgetToggled: { onCcWidgetToggled: {}
if (!TailscaleService.available)
return;
if (TailscaleService.connected)
TailscaleService.disconnectTailscale(null);
else
TailscaleService.connectTailscale(null);
}
ccDetailContent: Component { ccDetailContent: Component {
Rectangle { Rectangle {
@@ -95,122 +88,6 @@ PluginComponent {
width: parent.width width: parent.width
spacing: Theme.spacingS spacing: Theme.spacingS
// Connection status + connect/disconnect. Always shown
// (when available) so the connection can be toggled from
// the detail, including while disconnected.
RowLayout {
width: parent.width
spacing: Theme.spacingS
Column {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: 1
StyledText {
text: TailscaleService.connected ? I18n.tr("Connected", "Tailscale connection status: connected") : I18n.tr("Disconnected", "Tailscale connection status: disconnected")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
visible: TailscaleService.connected && TailscaleService.tailnetName.length > 0
text: TailscaleService.tailnetName
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
elide: Text.ElideRight
}
}
Rectangle {
id: connButton
Layout.alignment: Qt.AlignVCenter
height: 28
radius: 14
width: connButtonRow.implicitWidth + Theme.spacingM * 2
readonly property bool isConnected: TailscaleService.connected
color: isConnected ? (connButtonArea.containsMouse ? Theme.errorHover : Theme.surfaceLight) : (connButtonArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight)
Row {
id: connButtonRow
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: connButton.isConnected ? "link_off" : "link"
size: Theme.fontSizeSmall
color: connButton.isConnected ? Theme.surfaceText : Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: connButton.isConnected ? I18n.tr("Disconnect", "Tailscale disconnect button") : I18n.tr("Connect", "Tailscale connect button")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: connButton.isConnected ? Theme.surfaceText : Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: connButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (TailscaleService.connected)
TailscaleService.disconnectTailscale(null);
else
TailscaleService.connectTailscale(null);
}
}
}
}
// Connection controls: exit node picker + LAN access.
// Only meaningful while the backend is connected.
Column {
id: controlsColumn
width: parent.width
spacing: Theme.spacingS
visible: TailscaleService.connected
readonly property string noneLabel: I18n.tr("None", "Tailscale exit node: none selected")
DankDropdown {
width: parent.width
text: I18n.tr("Exit node", "Tailscale exit node selector label")
currentValue: TailscaleService.currentExitNode ? TailscaleService.currentExitNode.hostname : controlsColumn.noneLabel
options: {
const opts = [controlsColumn.noneLabel];
for (const p of TailscaleService.exitNodeOptions)
opts.push(p.hostname);
return opts;
}
onValueChanged: value => {
if (value === controlsColumn.noneLabel) {
TailscaleService.clearExitNode(null);
return;
}
const peer = TailscaleService.exitNodeOptions.find(p => p.hostname === value);
if (peer)
TailscaleService.setExitNode(peer.id, null);
}
}
DankToggle {
width: parent.width
text: I18n.tr("Allow LAN access", "Tailscale allow LAN access toggle")
description: I18n.tr("Reach local network devices while using an exit node", "Tailscale allow LAN access description")
visible: TailscaleService.currentExitNode !== null
checked: TailscaleService.exitNodeAllowLanAccess
onToggled: value => TailscaleService.setAllowLanAccess(value, null)
}
}
// Search bar + refresh button // Search bar + refresh button
RowLayout { RowLayout {
width: parent.width width: parent.width
@@ -93,7 +93,7 @@ DankPopout {
shouldBeVisible: false shouldBeVisible: false
property bool credentialsPromptOpen: NetworkService.credentialsRequested property bool credentialsPromptOpen: NetworkService.credentialsRequested
property bool wifiPasswordModalOpen: PopoutService.wifiPasswordModal?.shouldBeVisible ?? false property bool wifiPasswordModalOpen: PopoutService.wifiPasswordModal?.visible ?? false
property bool polkitModalOpen: PopoutService.polkitAuthModal?.visible ?? false property bool polkitModalOpen: PopoutService.polkitAuthModal?.visible ?? false
property bool anyModalOpen: credentialsPromptOpen || wifiPasswordModalOpen || polkitModalOpen || powerMenuOpen property bool anyModalOpen: credentialsPromptOpen || wifiPasswordModalOpen || polkitModalOpen || powerMenuOpen
@@ -2,7 +2,6 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import Quickshell import Quickshell
import qs.Common import qs.Common
import qs.Modules.Network
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
import qs.Modals import qs.Modals
@@ -152,7 +151,7 @@ Rectangle {
iconColor: Theme.surfaceVariantText iconColor: Theme.surfaceVariantText
onClicked: { onClicked: {
PopoutService.closeControlCenter(); PopoutService.closeControlCenter();
PopoutService.openSettingsWithTab(currentPreferenceIndex === 0 ? "network_ethernet" : "network_wifi"); PopoutService.openSettingsWithTab("network");
} }
} }
} }
@@ -722,7 +721,7 @@ Rectangle {
DankActionButton { DankActionButton {
id: qrCodeButton id: qrCodeButton
visible: modelData.secured && modelData.saved && !(modelData.enterprise || false) visible: modelData.secured && modelData.saved
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: optionsButton.width + pinWifiRow.width + 3 * Theme.spacingM + Theme.spacingS anchors.rightMargin: optionsButton.width + pinWifiRow.width + 3 * Theme.spacingM + Theme.spacingS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -750,9 +749,11 @@ Rectangle {
event.accepted = true; event.accepted = true;
return; return;
} }
WifiConnectionActions.connectToNetwork(modelData, { if (modelData.secured && !modelData.saved && (DMSService.apiVersion < 7 || modelData.enterprise)) {
connected: wifiDelegate.isConnected PopoutService.showWifiPasswordModal(modelData.ssid);
}); } else {
NetworkService.connectToWifi(modelData.ssid);
}
event.accepted = true; event.accepted = true;
} }
} }
@@ -803,9 +804,15 @@ Rectangle {
} }
onTriggered: { onTriggered: {
WifiConnectionActions.connectToNetworkFromDetails(networkContextMenu.currentSSID, networkContextMenu.currentSecured, networkContextMenu.currentSaved, networkContextMenu.currentEnterprise, networkContextMenu.currentConnected, { if (networkContextMenu.currentConnected) {
disconnectWhenConnected: true NetworkService.disconnectWifi();
}); return;
}
if (networkContextMenu.currentSecured && !networkContextMenu.currentSaved && (DMSService.apiVersion < 7 || networkContextMenu.currentEnterprise)) {
PopoutService.showWifiPasswordModal(networkContextMenu.currentSSID);
return;
}
NetworkService.connectToWifi(networkContextMenu.currentSSID);
} }
} }
@@ -15,7 +15,6 @@ Item {
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null property var blurBarWindow: null
property real sectionAvailablePrimarySize: 0
property bool overrideAxisLayout: false property bool overrideAxisLayout: false
property bool forceVerticalLayout: false property bool forceVerticalLayout: false
@@ -360,7 +359,6 @@ Item {
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow blurBarWindow: root.blurBarWindow
sectionAvailablePrimarySize: root.sectionAvailablePrimarySize
isFirst: index === 0 isFirst: index === 0
isLast: index === centerRepeater.count - 1 isLast: index === centerRepeater.count - 1
sectionSpacing: parent.itemSpacing sectionSpacing: parent.itemSpacing
@@ -497,7 +497,6 @@ Item {
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
barSpacing: barConfig?.spacing ?? 4 barSpacing: barConfig?.spacing ?? 4
sectionAvailablePrimarySize: Math.max(1, hCenterSection.x > 0 ? hCenterSection.x : parent.width / 3)
} }
Binding { Binding {
@@ -530,7 +529,6 @@ Item {
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
barSpacing: barConfig?.spacing ?? 4 barSpacing: barConfig?.spacing ?? 4
sectionAvailablePrimarySize: Math.max(1, hCenterSection.x > 0 ? parent.width - (hCenterSection.x + hCenterSection.width) : parent.width / 3)
} }
Binding { Binding {
@@ -563,7 +561,6 @@ Item {
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
barSpacing: barConfig?.spacing ?? 4 barSpacing: barConfig?.spacing ?? 4
sectionAvailablePrimarySize: Math.max(1, hRightSection.x > 0 ? hRightSection.x - (hLeftSection.x + hLeftSection.width) : parent.width / 3)
} }
Binding { Binding {
@@ -603,7 +600,6 @@ Item {
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
barSpacing: barConfig?.spacing ?? 4 barSpacing: barConfig?.spacing ?? 4
sectionAvailablePrimarySize: Math.max(1, vCenterSection.y > 0 ? vCenterSection.y : parent.height / 3)
} }
Binding { Binding {
@@ -637,7 +633,6 @@ Item {
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
barSpacing: barConfig?.spacing ?? 4 barSpacing: barConfig?.spacing ?? 4
sectionAvailablePrimarySize: Math.max(1, vRightSection.y > 0 ? vRightSection.y - (vLeftSection.y + vLeftSection.height) : parent.height / 3)
} }
Binding { Binding {
@@ -672,7 +667,6 @@ Item {
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
barSpacing: barConfig?.spacing ?? 4 barSpacing: barConfig?.spacing ?? 4
sectionAvailablePrimarySize: Math.max(1, vCenterSection.y > 0 ? parent.height - (vCenterSection.y + vCenterSection.height) : parent.height / 3)
} }
Binding { Binding {
+14 -26
View File
@@ -150,9 +150,6 @@ PanelWindow {
function onUsesFrameBarChromeChanged() { function onUsesFrameBarChromeChanged() {
_blurRebuildTimer.restart(); _blurRebuildTimer.restart();
} }
function onBarRevealedChanged() {
_blurRebuildTimer.restart();
}
} }
Component { Component {
@@ -179,13 +176,6 @@ PanelWindow {
teardown(); teardown();
if (!BlurService.enabled || !BlurService.available) if (!BlurService.enabled || !BlurService.available)
return; return;
// When the bar is hidden (auto-hide, or config not visible) keep the blur
// region empty rather than sliding it off-surface. Some compositors (Hyprland)
// gate blur on a non-empty region and then blur the whole surface box when the
// clip degenerates to empty, leaving the bar strip blurred while the bar is
// hidden (issue #2656). A null region disables the effect cleanly.
if (!barWindow.barRevealed)
return;
// In frame mode, FrameWindow owns the blur region for the entire screen edge // In frame mode, FrameWindow owns the blur region for the entire screen edge
// (including the bar area). The bar must not set its own competing blur region // (including the bar area). The bar must not set its own competing blur region
// so that frameBlurEnabled acts as the single control for all blur in frame mode. // so that frameBlurEnabled acts as the single control for all blur in frame mode.
@@ -296,6 +286,9 @@ PanelWindow {
readonly property bool isVertical: axis.isVertical readonly property bool isVertical: axis.isVertical
property bool gothCornersEnabled: barConfig?.gothCornersEnabled ?? false
property real wingtipsRadius: barConfig?.gothCornerRadiusOverride ? (barConfig?.gothCornerRadiusValue ?? 12) : Theme.cornerRadius
readonly property real _wingR: Math.max(0, wingtipsRadius)
readonly property color _surfaceContainer: Theme.surfaceContainer readonly property color _surfaceContainer: Theme.surfaceContainer
readonly property string _barId: barConfig?.id ?? "default" readonly property string _barId: barConfig?.id ?? "default"
property real _backgroundAlpha: barConfig?.transparency ?? 1.0 property real _backgroundAlpha: barConfig?.transparency ?? 1.0
@@ -307,30 +300,25 @@ PanelWindow {
} }
readonly property real _dpr: CompositorService.getScreenScale(barWindow.screen) readonly property real _dpr: CompositorService.getScreenScale(barWindow.screen)
property string screenName: modelData.name
readonly property bool usesConnectedFrameChrome: CompositorService.usesConnectedFrameChromeForScreen(screenName)
readonly property bool usesFrameBarChrome: CompositorService.frameWindowVisibleForScreen(screenName)
readonly property var renderBarConfig: SettingsData.effectiveBarConfigForRender(barConfig, usesFrameBarChrome)
property bool gothCornersEnabled: renderBarConfig?.gothCornersEnabled ?? false
property real wingtipsRadius: renderBarConfig?.gothCornerRadiusOverride ? (renderBarConfig?.gothCornerRadiusValue ?? 12) : Theme.cornerRadius
readonly property real _wingR: Math.max(0, wingtipsRadius)
// Shadow buffer: extra window space for shadow to render beyond bar bounds // Shadow buffer: extra window space for shadow to render beyond bar bounds
readonly property bool _shadowActive: (Theme.elevationEnabled && (typeof SettingsData !== "undefined" ? (SettingsData.barElevationEnabled ?? true) : false)) || (renderBarConfig?.shadowIntensity ?? 0) > 0 readonly property bool _shadowActive: (Theme.elevationEnabled && (typeof SettingsData !== "undefined" ? (SettingsData.barElevationEnabled ?? true) : false)) || (barConfig?.shadowIntensity ?? 0) > 0
readonly property real _shadowBuffer: { readonly property real _shadowBuffer: {
if (!_shadowActive) if (!_shadowActive)
return 0; return 0;
const hasOverride = (renderBarConfig?.shadowIntensity ?? 0) > 0; const hasOverride = (barConfig?.shadowIntensity ?? 0) > 0;
if (hasOverride) { if (hasOverride) {
const blur = (renderBarConfig.shadowIntensity ?? 0) * 0.2; const blur = (barConfig.shadowIntensity ?? 0) * 0.2;
const offset = blur * 0.5; const offset = blur * 0.5;
return Theme.snap(Math.max(16, blur + offset + 8), _dpr); return Theme.snap(Math.max(16, blur + offset + 8), _dpr);
} }
return Theme.snap(Theme.elevationRenderPadding(Theme.elevationLevel2, "top", 4, 8, 16), _dpr); return Theme.snap(Theme.elevationRenderPadding(Theme.elevationLevel2, "top", 4, 8, 16), _dpr);
} }
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. // 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. // When the bar draws its own pill, keep rounded corners and spacing like the dock.
readonly property bool flattenForMaximizedWindow: !SettingsData.frameEnabled || usesFrameBarChrome readonly property bool flattenForMaximizedWindow: !SettingsData.frameEnabled || usesFrameBarChrome
@@ -566,8 +554,8 @@ PanelWindow {
} }
screen: modelData screen: modelData
implicitHeight: !isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((renderBarConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0 implicitHeight: !isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((barConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0
implicitWidth: isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((renderBarConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0 implicitWidth: isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((barConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0
color: "transparent" color: "transparent"
Component.onCompleted: { Component.onCompleted: {
@@ -964,7 +952,7 @@ PanelWindow {
id: barBackground id: barBackground
barWindow: barWindow barWindow: barWindow
axis: axis axis: axis
barConfig: barWindow.renderBarConfig barConfig: barWindow.barConfig
} }
MouseArea { MouseArea {
@@ -14,7 +14,6 @@ Item {
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null property var blurBarWindow: null
property real sectionAvailablePrimarySize: 0
property bool overrideAxisLayout: false property bool overrideAxisLayout: false
property bool forceVerticalLayout: false property bool forceVerticalLayout: false
@@ -62,7 +61,6 @@ Item {
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow blurBarWindow: root.blurBarWindow
sectionAvailablePrimarySize: root.sectionAvailablePrimarySize
isFirst: index === 0 isFirst: index === 0
isLast: index === rowRepeater.count - 1 isLast: index === rowRepeater.count - 1
sectionSpacing: parent.rowSpacing sectionSpacing: parent.rowSpacing
@@ -108,7 +106,6 @@ Item {
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow blurBarWindow: root.blurBarWindow
sectionAvailablePrimarySize: root.sectionAvailablePrimarySize
isFirst: index === 0 isFirst: index === 0
isLast: index === columnRepeater.count - 1 isLast: index === columnRepeater.count - 1
sectionSpacing: parent.columnSpacing sectionSpacing: parent.columnSpacing
@@ -14,7 +14,6 @@ Item {
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null property var blurBarWindow: null
property real sectionAvailablePrimarySize: 0
property bool overrideAxisLayout: false property bool overrideAxisLayout: false
property bool forceVerticalLayout: false property bool forceVerticalLayout: false
@@ -64,7 +63,6 @@ Item {
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow blurBarWindow: root.blurBarWindow
sectionAvailablePrimarySize: root.sectionAvailablePrimarySize
isFirst: index === 0 isFirst: index === 0
isLast: index === rowRepeater.count - 1 isLast: index === rowRepeater.count - 1
sectionSpacing: parent.rowSpacing sectionSpacing: parent.rowSpacing
@@ -110,7 +108,6 @@ Item {
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow blurBarWindow: root.blurBarWindow
sectionAvailablePrimarySize: root.sectionAvailablePrimarySize
isFirst: index === 0 isFirst: index === 0
isLast: index === columnRepeater.count - 1 isLast: index === columnRepeater.count - 1
sectionSpacing: parent.columnSpacing sectionSpacing: parent.columnSpacing
@@ -17,7 +17,6 @@ Loader {
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null property var blurBarWindow: null
property real sectionAvailablePrimarySize: 0
property bool isFirst: false property bool isFirst: false
property bool isLast: false property bool isLast: false
property real sectionSpacing: 0 property real sectionSpacing: 0
@@ -142,14 +141,6 @@ Loader {
restoreMode: Binding.RestoreNone restoreMode: Binding.RestoreNone
} }
Binding {
target: root.item
when: root.item && "sectionAvailablePrimarySize" in root.item
property: "sectionAvailablePrimarySize"
value: root.sectionAvailablePrimarySize
restoreMode: Binding.RestoreNone
}
Binding { Binding {
target: root.item target: root.item
when: root.item && "isLeftBarEdge" in root.item when: root.item && "isLeftBarEdge" in root.item
@@ -933,17 +933,19 @@ BasePill {
tooltipLoader.active = true; tooltipLoader.active = true;
if (tooltipLoader.item) { if (tooltipLoader.item) {
if (root.isVerticalOrientation) { if (root.isVerticalOrientation) {
const localPos = delegateItem.mapToItem(null, 0, delegateItem.height / 2); const globalPos = delegateItem.mapToGlobal(0, delegateItem.height / 2);
const screenX = root.parentScreen ? root.parentScreen.x : 0;
const screenY = root.parentScreen ? root.parentScreen.y : 0;
const isLeft = root.axis?.edge === "left"; const isLeft = root.axis?.edge === "left";
const tooltipX = isLeft ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (root.parentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS); const tooltipX = isLeft ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (root.parentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS);
const screenRelativeY = localPos.y + root.minTooltipY; const screenRelativeY = globalPos.y - screenY + root.minTooltipY;
tooltipLoader.item.show(appItem.tooltipText, tooltipX, screenRelativeY, root.parentScreen, isLeft, !isLeft); tooltipLoader.item.show(appItem.tooltipText, screenX + tooltipX, screenRelativeY, root.parentScreen, isLeft, !isLeft);
} else { } else {
const localPos = delegateItem.mapToItem(null, delegateItem.width / 2, delegateItem.height); const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height);
const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height; const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height;
const isBottom = root.axis?.edge === "bottom"; const isBottom = root.axis?.edge === "bottom";
const tooltipY = isBottom ? (screenHeight - root.barThickness - root.barSpacing - Theme.spacingXS - 35) : (root.barThickness + root.barSpacing + Theme.spacingXS); const tooltipY = isBottom ? (screenHeight - root.barThickness - root.barSpacing - Theme.spacingXS - 35) : (root.barThickness + root.barSpacing + Theme.spacingXS);
tooltipLoader.item.show(appItem.tooltipText, localPos.x, tooltipY, root.parentScreen, false, false); tooltipLoader.item.show(appItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false);
} }
} }
} }
@@ -965,12 +967,14 @@ BasePill {
contextMenuLoader.active = true; contextMenuLoader.active = true;
if (contextMenuLoader.item) { if (contextMenuLoader.item) {
const localPos = delegateItem.mapToItem(null, delegateItem.width / 2, delegateItem.height / 2); const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2);
const screenX = root.parentScreen ? root.parentScreen.x : 0;
const screenY = root.parentScreen ? root.parentScreen.y : 0;
const isBarVertical = root.axis?.isVertical ?? false; const isBarVertical = root.axis?.isVertical ?? false;
const barEdge = root.axis?.edge ?? "top"; const barEdge = root.axis?.edge ?? "top";
let x = localPos.x; let x = globalPos.x - screenX;
let y = localPos.y; let y = globalPos.y - screenY;
switch (barEdge) { switch (barEdge) {
case "bottom": case "bottom":
+20 -15
View File
@@ -118,18 +118,10 @@ BasePill {
width: battery.width + battery.leftMargin + battery.rightMargin width: battery.width + battery.leftMargin + battery.rightMargin
height: battery.height + battery.topMargin + battery.bottomMargin height: battery.height + battery.topMargin + battery.bottomMargin
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton
onPressed: mouse => { onPressed: mouse => {
battery.triggerRipple(this, mouse.x, mouse.y); battery.triggerRipple(this, mouse.x, mouse.y);
if (mouse.button === Qt.LeftButton) { toggleBatteryPopup();
toggleBatteryPopup();
} else if (mouse.button === Qt.RightButton) {
if (PowerProfileWatcher.available) {
PowerProfileWatcher.cycleProfile();
} else {
ToastService.showError(I18n.tr("power-profiles-daemon not available"));
}
}
} }
onWheel: wheel => { onWheel: wheel => {
var delta = wheel.angleDelta.y; var delta = wheel.angleDelta.y;
@@ -139,20 +131,33 @@ BasePill {
// Check if this is a touchpad // Check if this is a touchpad
if (delta !== 120 && delta !== -120) { if (delta !== 120 && delta !== -120) {
touchpadAccumulator += delta; touchpadAccumulator += delta;
log.info("Acc: " + touchpadAccumulator);
if (Math.abs(touchpadAccumulator) < 500) if (Math.abs(touchpadAccumulator) < 500)
return; return;
delta = touchpadAccumulator; delta = touchpadAccumulator;
touchpadAccumulator = 0; touchpadAccumulator = 0;
} }
log.info("Trigger! Delta: " + delta);
if (!DisplayService.brightnessAvailable) { // This is after the other delta checks so it only shows on valid Y scroll
if (!PowerProfileWatcher.available) {
ToastService.showError(I18n.tr("power-profiles-daemon not available"));
return; return;
} }
const step = 5; const profiles = PowerProfileWatcher.availableProfiles;
const change = delta > 0 ? step : -step; var index = profiles.findIndex(profile => PowerProfiles.profile === profile);
const newBrightness = Math.max(0, Math.min(100, DisplayService.brightnessLevel + change));
DisplayService.setBrightness(newBrightness, "", false); if (delta > 0)
index += 1;
else
index -= 1;
if (index < 0 || index >= profiles.length)
return;
if (!PowerProfileWatcher.applyProfile(profiles[index]))
ToastService.showError(I18n.tr("Failed to set power profile"));
} }
} }
} }
@@ -513,7 +513,7 @@ BasePill {
case "vpn": case "vpn":
return "vpn_lock"; return "vpn_lock";
case "bluetooth": case "bluetooth":
return BluetoothService.connected ? "bluetooth_connected" : "bluetooth"; return "bluetooth";
case "battery": case "battery":
return Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable); return Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable);
case "printer": case "printer":
@@ -698,7 +698,7 @@ BasePill {
case "vpn": case "vpn":
return "vpn_lock"; return "vpn_lock";
case "bluetooth": case "bluetooth":
return BluetoothService.connected ? "bluetooth_connected" : "bluetooth"; return "bluetooth";
case "battery": case "battery":
return Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable); return Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable);
case "printer": case "printer":
@@ -276,12 +276,15 @@ BasePill {
if (root.isVerticalOrientation && root.selectedMount) { if (root.isVerticalOrientation && root.selectedMount) {
tooltipLoader.active = true; tooltipLoader.active = true;
if (tooltipLoader.item) { if (tooltipLoader.item) {
const localPos = mapToItem(null, width / 2, height / 2); const globalPos = mapToGlobal(width / 2, height / 2);
const currentScreen = root.parentScreen || Screen; const currentScreen = root.parentScreen || Screen;
const adjustedY = localPos.y + root.minTooltipY; const screenX = currentScreen ? currentScreen.x : 0;
const screenY = currentScreen ? currentScreen.y : 0;
const relativeY = globalPos.y - screenY;
const adjustedY = relativeY + root.minTooltipY;
const tooltipX = root.axis?.edge === "left" ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (currentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS); const tooltipX = root.axis?.edge === "left" ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (currentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS);
const isLeft = root.axis?.edge === "left"; const isLeft = root.axis?.edge === "left";
tooltipLoader.item.show(root.selectedMount.mount, tooltipX, adjustedY, currentScreen, isLeft, !isLeft); tooltipLoader.item.show(root.selectedMount.mount, screenX + tooltipX, adjustedY, currentScreen, isLeft, !isLeft);
} }
} }
} }
@@ -304,9 +304,13 @@ BasePill {
if (root.isVerticalOrientation && activeWindow && activeWindow.appId && root.parentScreen) { if (root.isVerticalOrientation && activeWindow && activeWindow.appId && root.parentScreen) {
tooltipLoader.active = true; tooltipLoader.active = true;
if (tooltipLoader.item) { if (tooltipLoader.item) {
const localPos = mapToItem(null, width / 2, height / 2); const globalPos = mapToGlobal(width / 2, height / 2);
const currentScreen = root.parentScreen; const currentScreen = root.parentScreen;
const adjustedY = localPos.y + root.minTooltipY; const screenX = currentScreen ? currentScreen.x : 0;
const screenY = currentScreen ? currentScreen.y : 0;
const relativeY = globalPos.y - screenY;
// Add minTooltipY offset to account for top bar
const adjustedY = relativeY + root.minTooltipY;
const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + (barConfig?.spacing ?? 4) + Theme.spacingXS) : (currentScreen.width - Theme.barHeight - (barConfig?.spacing ?? 4) - Theme.spacingXS); const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + (barConfig?.spacing ?? 4) + Theme.spacingXS) : (currentScreen.width - Theme.barHeight - (barConfig?.spacing ?? 4) - Theme.spacingXS);
const appName = Paths.getAppName(activeWindow.appId, activeDesktopEntry); const appName = Paths.getAppName(activeWindow.appId, activeDesktopEntry);
@@ -314,7 +318,7 @@ BasePill {
const tooltipText = appName + (title ? " • " + title : ""); const tooltipText = appName + (title ? " • " + title : "");
const isLeft = root.axis?.edge === "left"; const isLeft = root.axis?.edge === "left";
tooltipLoader.item.show(tooltipText, tooltipX, adjustedY, currentScreen, isLeft, !isLeft); tooltipLoader.item.show(tooltipText, screenX + tooltipX, adjustedY, currentScreen, isLeft, !isLeft);
} }
} }
} }
@@ -32,20 +32,9 @@ BasePill {
} }
readonly property var notepadInstance: resolveNotepadInstance() readonly property var notepadInstance: resolveNotepadInstance()
readonly property bool popoutDefault: SettingsData.notepadDefaultMode === "popout" readonly property bool isActive: notepadInstance?.isVisible ?? false
readonly property bool isActive: popoutDefault ? (PopoutService.notepadPopout?.visible ?? false) : (notepadInstance?.isVisible ?? false)
property bool isAutoHideBar: false property bool isAutoHideBar: false
function showActiveSurface() {
if (root.popoutDefault) {
PopoutService.openNotepadPopout();
return;
}
const instance = prepareNotepadInstance(root.notepadInstance);
if (instance && typeof instance.show === "function")
instance.show();
}
function prepareNotepadInstance(instance) { function prepareNotepadInstance(instance) {
if (instance) if (instance)
instance.triggerUsesOverlayLayer = root.barUsesOverlayLayer; instance.triggerUsesOverlayLayer = root.barUsesOverlayLayer;
@@ -86,14 +75,20 @@ BasePill {
function openTabByIndex(tabIndex) { function openTabByIndex(tabIndex) {
if (tabIndex < 0) if (tabIndex < 0)
return; return;
showActiveSurface(); const instance = prepareNotepadInstance(root.notepadInstance);
if (instance && typeof instance.show === "function") {
instance.show();
}
Qt.callLater(() => { Qt.callLater(() => {
NotepadStorageService.switchToTab(tabIndex); NotepadStorageService.switchToTab(tabIndex);
}); });
} }
function openNewNote() { function openNewNote() {
showActiveSurface(); const instance = prepareNotepadInstance(root.notepadInstance);
if (instance && typeof instance.show === "function") {
instance.show();
}
Qt.callLater(() => { Qt.callLater(() => {
NotepadStorageService.createNewTab(); NotepadStorageService.createNewTab();
}); });
@@ -152,10 +147,6 @@ BasePill {
openContextMenu(); openContextMenu();
return; return;
} }
if (root.popoutDefault) {
PopoutService.toggleNotepadPopout();
return;
}
const inst = prepareNotepadInstance(root.notepadInstance); const inst = prepareNotepadInstance(root.notepadInstance);
if (inst) { if (inst) {
inst.toggle(); inst.toggle();
@@ -18,14 +18,6 @@ BasePill {
property var widgetData: null property var widgetData: null
property var hoveredItem: null property var hoveredItem: null
onHoveredItemChanged: {
if (hoveredItem)
return;
if (tooltipLoader.item)
tooltipLoader.item.hide();
tooltipLoader.active = false;
}
property var topBar: null property var topBar: null
property bool isAutoHideBar: false property bool isAutoHideBar: false
property Item windowRoot: (Window.window ? Window.window.contentItem : null) property Item windowRoot: (Window.window ? Window.window.contentItem : null)
@@ -244,11 +236,6 @@ BasePill {
delegate: Item { delegate: Item {
id: delegateItem id: delegateItem
Component.onDestruction: {
if (root.hoveredItem === delegateItem)
root.hoveredItem = null;
}
property bool isGrouped: root._groupByApp property bool isGrouped: root._groupByApp
property var groupData: isGrouped ? modelData : null property var groupData: isGrouped ? modelData : null
property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData
@@ -424,16 +411,22 @@ BasePill {
windowContextMenuLoader.item.triggerBarThickness = root.barThickness; windowContextMenuLoader.item.triggerBarThickness = root.barThickness;
windowContextMenuLoader.item.triggerBarSpacing = root.barSpacing; windowContextMenuLoader.item.triggerBarSpacing = root.barSpacing;
if (root.isVerticalOrientation) { if (root.isVerticalOrientation) {
const localPos = delegateItem.mapToItem(null, delegateItem.width / 2, delegateItem.height / 2); const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2);
const adjustedY = localPos.y + root.minTooltipY; const screenX = root.parentScreen ? root.parentScreen.x : 0;
const screenY = root.parentScreen ? root.parentScreen.y : 0;
const relativeY = globalPos.y - screenY;
// Add minTooltipY offset to account for top bar
const adjustedY = relativeY + root.minTooltipY;
const xPos = root.axis?.edge === "left" ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (root.parentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS); const xPos = root.axis?.edge === "left" ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (root.parentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS);
windowContextMenuLoader.item.showAt(xPos, adjustedY, true, root.axis?.edge); windowContextMenuLoader.item.showAt(xPos, adjustedY, true, root.axis?.edge);
} else { } else {
const localPos = delegateItem.mapToItem(null, delegateItem.width / 2, 0); const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, 0);
const screenX = root.parentScreen ? root.parentScreen.x : 0;
const relativeX = globalPos.x - screenX;
const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height; const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height;
const isBottom = root.axis?.edge === "bottom"; const isBottom = root.axis?.edge === "bottom";
const yPos = isBottom ? (screenHeight - root.barThickness - root.barSpacing - 32 - Theme.spacingXS) : (root.barThickness + root.barSpacing + Theme.spacingXS); const yPos = isBottom ? (screenHeight - root.barThickness - root.barSpacing - 32 - Theme.spacingXS) : (root.barThickness + root.barSpacing + Theme.spacingXS);
windowContextMenuLoader.item.showAt(localPos.x, yPos, false, root.axis?.edge); windowContextMenuLoader.item.showAt(relativeX, yPos, false, root.axis?.edge);
} }
} }
} else if (mouse.button === Qt.MiddleButton) { } else if (mouse.button === Qt.MiddleButton) {
@@ -449,23 +442,33 @@ BasePill {
tooltipLoader.active = true; tooltipLoader.active = true;
if (tooltipLoader.item) { if (tooltipLoader.item) {
if (root.isVerticalOrientation) { if (root.isVerticalOrientation) {
const localPos = delegateItem.mapToItem(null, delegateItem.width / 2, delegateItem.height / 2); const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2);
const screenX = root.parentScreen ? root.parentScreen.x : 0;
const screenY = root.parentScreen ? root.parentScreen.y : 0;
const relativeY = globalPos.y - screenY;
const tooltipX = root.axis?.edge === "left" ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (root.parentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS); const tooltipX = root.axis?.edge === "left" ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (root.parentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS);
const isLeft = root.axis?.edge === "left"; const isLeft = root.axis?.edge === "left";
const adjustedY = localPos.y + root.minTooltipY; const adjustedY = relativeY + root.minTooltipY;
tooltipLoader.item.show(delegateItem.tooltipText, tooltipX, adjustedY, root.parentScreen, isLeft, !isLeft); const finalX = screenX + tooltipX;
tooltipLoader.item.show(delegateItem.tooltipText, finalX, adjustedY, root.parentScreen, isLeft, !isLeft);
} else { } else {
const localPos = delegateItem.mapToItem(null, delegateItem.width / 2, delegateItem.height); const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height);
const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height; const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height;
const isBottom = root.axis?.edge === "bottom"; const isBottom = root.axis?.edge === "bottom";
const tooltipY = isBottom ? (screenHeight - root.barThickness - root.barSpacing - Theme.spacingXS - 35) : (root.barThickness + root.barSpacing + Theme.spacingXS); const tooltipY = isBottom ? (screenHeight - root.barThickness - root.barSpacing - Theme.spacingXS - 35) : (root.barThickness + root.barSpacing + Theme.spacingXS);
tooltipLoader.item.show(delegateItem.tooltipText, localPos.x, tooltipY, root.parentScreen, false, false); tooltipLoader.item.show(delegateItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false);
} }
} }
} }
onExited: { onExited: {
if (root.hoveredItem === delegateItem) if (root.hoveredItem === delegateItem) {
root.hoveredItem = null; root.hoveredItem = null;
if (tooltipLoader.item) {
tooltipLoader.item.hide();
}
tooltipLoader.active = false;
}
} }
} }
} }
@@ -488,11 +491,6 @@ BasePill {
delegate: Item { delegate: Item {
id: delegateItem id: delegateItem
Component.onDestruction: {
if (root.hoveredItem === delegateItem)
root.hoveredItem = null;
}
property bool isGrouped: root._groupByApp property bool isGrouped: root._groupByApp
property var groupData: isGrouped ? modelData : null property var groupData: isGrouped ? modelData : null
property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData
@@ -667,16 +665,22 @@ BasePill {
windowContextMenuLoader.item.triggerBarThickness = root.barThickness; windowContextMenuLoader.item.triggerBarThickness = root.barThickness;
windowContextMenuLoader.item.triggerBarSpacing = root.barSpacing; windowContextMenuLoader.item.triggerBarSpacing = root.barSpacing;
if (root.isVerticalOrientation) { if (root.isVerticalOrientation) {
const localPos = delegateItem.mapToItem(null, delegateItem.width / 2, delegateItem.height / 2); const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2);
const adjustedY = localPos.y + root.minTooltipY; const screenX = root.parentScreen ? root.parentScreen.x : 0;
const screenY = root.parentScreen ? root.parentScreen.y : 0;
const relativeY = globalPos.y - screenY;
// Add minTooltipY offset to account for top bar
const adjustedY = relativeY + root.minTooltipY;
const xPos = root.axis?.edge === "left" ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (root.parentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS); const xPos = root.axis?.edge === "left" ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (root.parentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS);
windowContextMenuLoader.item.showAt(xPos, adjustedY, true, root.axis?.edge); windowContextMenuLoader.item.showAt(xPos, adjustedY, true, root.axis?.edge);
} else { } else {
const localPos = delegateItem.mapToItem(null, delegateItem.width / 2, 0); const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, 0);
const screenX = root.parentScreen ? root.parentScreen.x : 0;
const relativeX = globalPos.x - screenX;
const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height; const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height;
const isBottom = root.axis?.edge === "bottom"; const isBottom = root.axis?.edge === "bottom";
const yPos = isBottom ? (screenHeight - root.barThickness - root.barSpacing - 32 - Theme.spacingXS) : (root.barThickness + root.barSpacing + Theme.spacingXS); const yPos = isBottom ? (screenHeight - root.barThickness - root.barSpacing - 32 - Theme.spacingXS) : (root.barThickness + root.barSpacing + Theme.spacingXS);
windowContextMenuLoader.item.showAt(localPos.x, yPos, false, root.axis?.edge); windowContextMenuLoader.item.showAt(relativeX, yPos, false, root.axis?.edge);
} }
} }
} else if (mouse.button === Qt.MiddleButton) { } else if (mouse.button === Qt.MiddleButton) {
@@ -692,23 +696,33 @@ BasePill {
tooltipLoader.active = true; tooltipLoader.active = true;
if (tooltipLoader.item) { if (tooltipLoader.item) {
if (root.isVerticalOrientation) { if (root.isVerticalOrientation) {
const localPos = delegateItem.mapToItem(null, delegateItem.width / 2, delegateItem.height / 2); const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2);
const screenX = root.parentScreen ? root.parentScreen.x : 0;
const screenY = root.parentScreen ? root.parentScreen.y : 0;
const relativeY = globalPos.y - screenY;
const tooltipX = root.axis?.edge === "left" ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (root.parentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS); const tooltipX = root.axis?.edge === "left" ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (root.parentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS);
const isLeft = root.axis?.edge === "left"; const isLeft = root.axis?.edge === "left";
const adjustedY = localPos.y + root.minTooltipY; const adjustedY = relativeY + root.minTooltipY;
tooltipLoader.item.show(delegateItem.tooltipText, tooltipX, adjustedY, root.parentScreen, isLeft, !isLeft); const finalX = screenX + tooltipX;
tooltipLoader.item.show(delegateItem.tooltipText, finalX, adjustedY, root.parentScreen, isLeft, !isLeft);
} else { } else {
const localPos = delegateItem.mapToItem(null, delegateItem.width / 2, delegateItem.height); const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height);
const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height; const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height;
const isBottom = root.axis?.edge === "bottom"; const isBottom = root.axis?.edge === "bottom";
const tooltipY = isBottom ? (screenHeight - root.barThickness - root.barSpacing - Theme.spacingXS - 35) : (root.barThickness + root.barSpacing + Theme.spacingXS); const tooltipY = isBottom ? (screenHeight - root.barThickness - root.barSpacing - Theme.spacingXS - 35) : (root.barThickness + root.barSpacing + Theme.spacingXS);
tooltipLoader.item.show(delegateItem.tooltipText, localPos.x, tooltipY, root.parentScreen, false, false); tooltipLoader.item.show(delegateItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false);
} }
} }
} }
onExited: { onExited: {
if (root.hoveredItem === delegateItem) if (root.hoveredItem === delegateItem) {
root.hoveredItem = null; root.hoveredItem = null;
if (tooltipLoader.item) {
tooltipLoader.item.hide();
}
tooltipLoader.active = false;
}
} }
} }
} }
@@ -22,10 +22,6 @@ BasePill {
property bool isAtBottom: false property bool isAtBottom: false
property bool isAutoHideBar: false property bool isAutoHideBar: false
property bool useOverflowPopup: !widgetData?.trayUseInlineExpansion property bool useOverflowPopup: !widgetData?.trayUseInlineExpansion
property bool useSingleLineOverflowPopup: widgetData?.trayPopupSingleLine ?? SettingsData.trayPopupSingleLine
property bool useAutomaticOverflow: widgetData?.trayAutoOverflow ?? SettingsData.trayAutoOverflow
property int configuredMaxVisibleItems: widgetData?.trayMaxVisibleItems ?? SettingsData.trayMaxVisibleItems
property real sectionAvailablePrimarySize: 0
readonly property var hiddenTrayIds: { readonly property var hiddenTrayIds: {
const envValue = Quickshell.env("DMS_HIDE_TRAYIDS") || ""; const envValue = Quickshell.env("DMS_HIDE_TRAYIDS") || "";
return envValue ? envValue.split(",").map(id => id.trim().toLowerCase()) : []; return envValue ? envValue.split(",").map(id => id.trim().toLowerCase()) : [];
@@ -150,32 +146,12 @@ BasePill {
readonly property var allSortedTrayItems: sortByPreferredOrder(allTrayItems, _trayOrderTrigger) readonly property var allSortedTrayItems: sortByPreferredOrder(allTrayItems, _trayOrderTrigger)
readonly property var allSortedTrayItemKeys: allSortedTrayItems.map(item => getTrayItemKey(item)) readonly property var allSortedTrayItemKeys: allSortedTrayItems.map(item => getTrayItemKey(item))
readonly property var visibleSortedTrayItems: allSortedTrayItems.filter(item => !SessionData.isHiddenTrayId(root.getTrayItemKey(item))) readonly property var mainBarItemsRaw: allSortedTrayItems.filter(item => !SessionData.isHiddenTrayId(root.getTrayItemKey(item)))
readonly property int automaticVisibleItemLimit: {
if (!root.useAutomaticOverflow)
return root.visibleSortedTrayItems.length;
const explicitLimit = Number(root.configuredMaxVisibleItems || 0);
if (explicitLimit > 0)
return Math.max(1, Math.min(root.visibleSortedTrayItems.length, explicitLimit));
const scale = (typeof CompositorService !== "undefined" && CompositorService.getScreenScale) ? Math.max(1, CompositorService.getScreenScale(root.parentScreen)) : 1;
const sectionPrimary = root.sectionAvailablePrimarySize > 0 ? root.sectionAvailablePrimarySize : (root.isVerticalOrientation ? (root.parentScreen?.height || 0) : (root.parentScreen?.width || 0));
const logicalPrimary = sectionPrimary > 0 ? (sectionPrimary / scale) : 640;
const maxTrayShare = root.isVerticalOrientation ? 0.55 : 0.50;
const itemSize = Math.max(1, root.trayItemSize);
const slots = Math.floor((logicalPrimary * maxTrayShare) / itemSize);
return Math.max(2, Math.min(10, Math.min(root.visibleSortedTrayItems.length, slots)));
}
readonly property var mainBarItemsRaw: visibleSortedTrayItems.slice(0, automaticVisibleItemLimit)
readonly property var mainBarItems: mainBarItemsRaw.map((item, idx) => ({ readonly property var mainBarItems: mainBarItemsRaw.map((item, idx) => ({
key: getTrayItemKey(item), key: getTrayItemKey(item),
item: item item: item
})) }))
readonly property var autoOverflowBarItems: visibleSortedTrayItems.slice(automaticVisibleItemLimit) readonly property var hiddenBarItems: allSortedTrayItems.filter(item => SessionData.isHiddenTrayId(root.getTrayItemKey(item)))
readonly property var manualHiddenBarItems: allSortedTrayItems.filter(item => SessionData.isHiddenTrayId(root.getTrayItemKey(item)))
readonly property var hiddenBarItemKeys: manualHiddenBarItems.concat(autoOverflowBarItems).map(item => root.getTrayItemKey(item))
readonly property var hiddenBarItems: allSortedTrayItems.filter(item => hiddenBarItemKeys.indexOf(root.getTrayItemKey(item)) !== -1)
readonly property string trayIconTintMode: { readonly property string trayIconTintMode: {
const configuredMode = SettingsData.systemTrayIconTintMode || "none"; const configuredMode = SettingsData.systemTrayIconTintMode || "none";
switch (configuredMode) { switch (configuredMode) {
@@ -243,10 +219,6 @@ BasePill {
const fromKey = mainBarItems[visibleFromIndex]?.key ?? null; const fromKey = mainBarItems[visibleFromIndex]?.key ?? null;
const toKey = mainBarItems[visibleToIndex]?.key ?? null; const toKey = mainBarItems[visibleToIndex]?.key ?? null;
moveTrayItemKeyInFullOrder(fromKey, toKey);
}
function moveTrayItemKeyInFullOrder(fromKey, toKey) {
if (!fromKey || !toKey) if (!fromKey || !toKey)
return; return;
@@ -261,103 +233,10 @@ BasePill {
SessionData.setTrayItemOrder(fullOrder); SessionData.setTrayItemOrder(fullOrder);
} }
function promoteTrayItemToBar(item) {
const itemKey = getTrayItemKey(item);
if (!itemKey)
return;
if (SessionData.isHiddenTrayId(itemKey)) {
SessionData.showTrayId(itemKey);
return;
}
const fullOrder = [...allSortedTrayItemKeys];
const fromIndex = fullOrder.indexOf(itemKey);
if (fromIndex < 0)
return;
const movedKey = fullOrder.splice(fromIndex, 1)[0];
const targetIndex = Math.max(0, Math.min(root.automaticVisibleItemLimit - 1, fullOrder.length));
fullOrder.splice(targetIndex, 0, movedKey);
SessionData.setTrayItemOrder(fullOrder);
}
function isManualHiddenTrayItem(item) {
return SessionData.isHiddenTrayId(getTrayItemKey(item));
}
function isAutoOverflowTrayItem(item) {
const key = getTrayItemKey(item);
return key && !isManualHiddenTrayItem(item) && root.autoOverflowBarItems.some(overflowItem => getTrayItemKey(overflowItem) === key);
}
function dragShiftOffset(index, draggedIndex, dropTargetIndex, shiftAmount) {
if (draggedIndex < 0 || index === draggedIndex || dropTargetIndex < 0)
return 0;
if (draggedIndex < dropTargetIndex && index > draggedIndex && index <= dropTargetIndex)
return -shiftAmount;
if (draggedIndex > dropTargetIndex && index >= dropTargetIndex && index < draggedIndex)
return shiftAmount;
return 0;
}
function beginMainDrag(visualIndex, reversed) {
root.draggedIndex = reversed ? (root.mainBarItems.length - 1 - visualIndex) : visualIndex;
root.dropTargetIndex = root.draggedIndex;
}
function updateMainDrag(axisOffset, visualIndex, reversed) {
const itemSize = root.trayItemSize;
const slotOffset = Math.round(axisOffset / itemSize);
const visualTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, visualIndex + slotOffset));
const newTargetIndex = reversed ? (root.mainBarItems.length - 1 - visualTargetIndex) : visualTargetIndex;
if (newTargetIndex !== root.dropTargetIndex)
root.dropTargetIndex = newTargetIndex;
}
function finishMainDrag() {
const didReorder = root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex;
if (didReorder) {
root.suppressShiftAnimation = true;
root.moveTrayItemInFullOrder(root.draggedIndex, root.dropTargetIndex);
Qt.callLater(() => root.suppressShiftAnimation = false);
}
root.draggedIndex = -1;
root.dropTargetIndex = -1;
return didReorder;
}
function beginPopupDrag(index) {
root.popupDraggedIndex = index;
root.popupDropTargetIndex = index;
}
function updatePopupDrag(axisOffset, index) {
const itemSize = root.trayItemSize + 6;
const slotOffset = Math.round(axisOffset / itemSize);
const newTargetIndex = Math.max(0, Math.min(root.hiddenBarItems.length - 1, index + slotOffset));
if (newTargetIndex !== root.popupDropTargetIndex)
root.popupDropTargetIndex = newTargetIndex;
}
function finishPopupDrag() {
const didReorder = root.popupDropTargetIndex >= 0 && root.popupDropTargetIndex !== root.popupDraggedIndex;
if (didReorder) {
const fromItem = root.hiddenBarItems[root.popupDraggedIndex];
const toItem = root.hiddenBarItems[root.popupDropTargetIndex];
root.suppressShiftAnimation = true;
root.moveTrayItemKeyInFullOrder(root.getTrayItemKey(fromItem), root.getTrayItemKey(toItem));
Qt.callLater(() => root.suppressShiftAnimation = false);
}
root.popupDraggedIndex = -1;
root.popupDropTargetIndex = -1;
return didReorder;
}
property int draggedIndex: -1 property int draggedIndex: -1
property int dropTargetIndex: -1 property int dropTargetIndex: -1
property int popupDraggedIndex: -1
property int popupDropTargetIndex: -1
property bool suppressShiftAnimation: false property bool suppressShiftAnimation: false
readonly property bool hasHiddenItems: hiddenBarItems.length > 0 readonly property bool hasHiddenItems: allTrayItems.length > mainBarItems.length
readonly property bool inlineExpanded: hasHiddenItems && !useOverflowPopup && menuOpen readonly property bool inlineExpanded: hasHiddenItems && !useOverflowPopup && menuOpen
visible: allTrayItems.length > 0 visible: allTrayItems.length > 0
opacity: allTrayItems.length > 0 ? 1 : 0 opacity: allTrayItems.length > 0 ? 1 : 0
@@ -472,7 +351,22 @@ BasePill {
height: root.barThickness height: root.barThickness
z: dragHandler.dragging ? 100 : 0 z: dragHandler.dragging ? 100 : 0
property real shiftOffset: root.dragShiftOffset(index, root.draggedIndex, root.dropTargetIndex, root.trayItemSize) property real shiftOffset: {
if (root.draggedIndex < 0)
return 0;
if (index === root.draggedIndex)
return 0;
const dragIdx = root.draggedIndex;
const dropIdx = root.dropTargetIndex;
const shiftAmount = root.trayItemSize;
if (dropIdx < 0)
return 0;
if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
return -shiftAmount;
if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx)
return shiftAmount;
return 0;
}
transform: Translate { transform: Translate {
x: delegateRoot.shiftOffset x: delegateRoot.shiftOffset
@@ -572,12 +466,19 @@ BasePill {
onReleased: mouse => { onReleased: mouse => {
longPressTimer.stop(); longPressTimer.stop();
const wasDragging = dragHandler.dragging; const wasDragging = dragHandler.dragging;
if (wasDragging) const didReorder = wasDragging && root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex;
root.finishMainDrag();
if (didReorder) {
root.suppressShiftAnimation = true;
root.moveTrayItemInFullOrder(root.draggedIndex, root.dropTargetIndex);
Qt.callLater(() => root.suppressShiftAnimation = false);
}
dragHandler.longPressing = false; dragHandler.longPressing = false;
dragHandler.dragging = false; dragHandler.dragging = false;
dragHandler.dragAxisOffset = 0; dragHandler.dragAxisOffset = 0;
root.draggedIndex = -1;
root.dropTargetIndex = -1;
if (wasDragging || mouse.button !== Qt.LeftButton) if (wasDragging || mouse.button !== Qt.LeftButton)
return; return;
@@ -600,7 +501,8 @@ BasePill {
const distance = Math.abs(mouse.x - dragHandler.dragStartPos.x); const distance = Math.abs(mouse.x - dragHandler.dragStartPos.x);
if (distance > 5) { if (distance > 5) {
dragHandler.dragging = true; dragHandler.dragging = true;
root.beginMainDrag(index, root.reverseInlineHorizontal); root.draggedIndex = root.reverseInlineHorizontal ? (root.mainBarItems.length - 1 - index) : index;
root.dropTargetIndex = root.draggedIndex;
} }
} }
if (!dragHandler.dragging) if (!dragHandler.dragging)
@@ -608,7 +510,13 @@ BasePill {
const axisOffset = mouse.x - dragHandler.dragStartPos.x; const axisOffset = mouse.x - dragHandler.dragStartPos.x;
dragHandler.dragAxisOffset = axisOffset; dragHandler.dragAxisOffset = axisOffset;
root.updateMainDrag(axisOffset, index, root.reverseInlineHorizontal); const itemSize = root.trayItemSize;
const slotOffset = Math.round(axisOffset / itemSize);
const visualTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
const newTargetIndex = root.reverseInlineHorizontal ? (root.mainBarItems.length - 1 - visualTargetIndex) : visualTargetIndex;
if (newTargetIndex !== root.dropTargetIndex) {
root.dropTargetIndex = newTargetIndex;
}
} }
onClicked: mouse => { onClicked: mouse => {
@@ -798,7 +706,22 @@ BasePill {
height: root.trayItemSize height: root.trayItemSize
z: dragHandler.dragging ? 100 : 0 z: dragHandler.dragging ? 100 : 0
property real shiftOffset: root.dragShiftOffset(index, root.draggedIndex, root.dropTargetIndex, root.trayItemSize) property real shiftOffset: {
if (root.draggedIndex < 0)
return 0;
if (index === root.draggedIndex)
return 0;
const dragIdx = root.draggedIndex;
const dropIdx = root.dropTargetIndex;
const shiftAmount = root.trayItemSize;
if (dropIdx < 0)
return 0;
if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
return -shiftAmount;
if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx)
return shiftAmount;
return 0;
}
transform: Translate { transform: Translate {
y: shiftOffset y: shiftOffset
@@ -898,12 +821,19 @@ BasePill {
onReleased: mouse => { onReleased: mouse => {
longPressTimer.stop(); longPressTimer.stop();
const wasDragging = dragHandler.dragging; const wasDragging = dragHandler.dragging;
if (wasDragging) const didReorder = wasDragging && root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex;
root.finishMainDrag();
if (didReorder) {
root.suppressShiftAnimation = true;
root.moveTrayItemInFullOrder(root.draggedIndex, root.dropTargetIndex);
Qt.callLater(() => root.suppressShiftAnimation = false);
}
dragHandler.longPressing = false; dragHandler.longPressing = false;
dragHandler.dragging = false; dragHandler.dragging = false;
dragHandler.dragAxisOffset = 0; dragHandler.dragAxisOffset = 0;
root.draggedIndex = -1;
root.dropTargetIndex = -1;
if (wasDragging || mouse.button !== Qt.LeftButton) if (wasDragging || mouse.button !== Qt.LeftButton)
return; return;
@@ -926,7 +856,8 @@ BasePill {
const distance = Math.abs(mouse.y - dragHandler.dragStartPos.y); const distance = Math.abs(mouse.y - dragHandler.dragStartPos.y);
if (distance > 5) { if (distance > 5) {
dragHandler.dragging = true; dragHandler.dragging = true;
root.beginMainDrag(index, false); root.draggedIndex = index;
root.dropTargetIndex = root.draggedIndex;
} }
} }
if (!dragHandler.dragging) if (!dragHandler.dragging)
@@ -934,7 +865,12 @@ BasePill {
const axisOffset = mouse.y - dragHandler.dragStartPos.y; const axisOffset = mouse.y - dragHandler.dragStartPos.y;
dragHandler.dragAxisOffset = axisOffset; dragHandler.dragAxisOffset = axisOffset;
root.updateMainDrag(axisOffset, index, false); const itemSize = root.trayItemSize;
const slotOffset = Math.round(axisOffset / itemSize);
const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
if (newTargetIndex !== root.dropTargetIndex) {
root.dropTargetIndex = newTargetIndex;
}
} }
onClicked: mouse => { onClicked: mouse => {
@@ -1179,12 +1115,11 @@ BasePill {
} }
function updatePosition() { function updatePosition() {
// Window-local maps directly to screen-local because the bar window spans the const globalPos = root.mapToGlobal(0, 0);
// full screen edge; this avoids mixing mapToGlobal with a separately-tracked const screenX = screen.x || 0;
// screen.x/.y origin, which desync on non-primary monitors and after DPMS/hotplug. const screenY = screen.y || 0;
const localPos = root.mapToItem(null, 0, 0); const relativeX = globalPos.x - screenX;
const relativeX = localPos.x; const relativeY = globalPos.y - screenY;
const relativeY = localPos.y;
if (root.isVerticalOrientation) { if (root.isVerticalOrientation) {
const edge = root.axis?.edge; const edge = root.axis?.edge;
@@ -1201,38 +1136,20 @@ BasePill {
id: menuContainer id: menuContainer
objectName: "overflowMenuContainer" objectName: "overflowMenuContainer"
readonly property bool popupUsesVerticalLine: root.useSingleLineOverflowPopup && root.isVerticalOrientation
readonly property real popupPadding: Theme.spacingS + (popupUsesVerticalLine ? 3 : 0)
readonly property real rawWidth: { readonly property real rawWidth: {
const itemCount = root.hiddenBarItems.length; const itemCount = root.hiddenBarItems.length;
if (itemCount === 0) const cols = Math.min(5, itemCount);
return 0;
if (popupUsesVerticalLine)
return root.trayItemSize + 4 + popupPadding * 2;
const cols = root.useSingleLineOverflowPopup ? itemCount : Math.min(5, itemCount);
const itemSize = root.trayItemSize + 4; const itemSize = root.trayItemSize + 4;
const spacing = 2; const spacing = 2;
const desiredWidth = cols * itemSize + (cols - 1) * spacing + popupPadding * 2; return cols * itemSize + (cols - 1) * spacing + Theme.spacingS * 2;
if (!root.useSingleLineOverflowPopup)
return desiredWidth;
const maxWidth = Math.max(itemSize + popupPadding * 2, overflowMenu.maskWidth - 20);
return Math.min(desiredWidth, maxWidth);
} }
readonly property real rawHeight: { readonly property real rawHeight: {
const itemCount = root.hiddenBarItems.length; const itemCount = root.hiddenBarItems.length;
if (itemCount === 0) const cols = Math.min(5, itemCount);
return 0; const rows = Math.ceil(itemCount / cols);
const itemSize = root.trayItemSize + 4; const itemSize = root.trayItemSize + 4;
const spacing = 2; const spacing = 2;
if (popupUsesVerticalLine) { return rows * itemSize + (rows - 1) * spacing + Theme.spacingS * 2;
const desiredHeight = itemCount * itemSize + (itemCount - 1) * spacing + popupPadding * 2;
const maxHeight = Math.max(itemSize + popupPadding * 2, overflowMenu.maskHeight - 20);
return Math.min(desiredHeight, maxHeight);
}
const cols = root.useSingleLineOverflowPopup ? itemCount : Math.min(5, itemCount);
const rows = Math.ceil(itemCount / cols);
return rows * itemSize + (rows - 1) * spacing + popupPadding * 2;
} }
readonly property real alignedWidth: Theme.px(rawWidth, overflowMenu.dpr) readonly property real alignedWidth: Theme.px(rawWidth, overflowMenu.dpr)
@@ -1313,161 +1230,76 @@ BasePill {
z: 100 z: 100
} }
Flickable { Grid {
id: menuGrid
anchors.centerIn: parent anchors.centerIn: parent
width: parent.width - menuContainer.popupPadding * 2 columns: Math.min(5, root.hiddenBarItems.length)
height: parent.height - menuContainer.popupPadding * 2 spacing: 2
contentWidth: menuGrid.implicitWidth rowSpacing: 2
contentHeight: menuGrid.implicitHeight
boundsBehavior: Flickable.StopAtBounds
clip: true
interactive: root.useSingleLineOverflowPopup && (menuContainer.popupUsesVerticalLine ? contentHeight > height : contentWidth > width)
Grid { Repeater {
id: menuGrid model: root.hiddenBarItems
anchors.verticalCenter: menuContainer.popupUsesVerticalLine ? undefined : parent.verticalCenter
anchors.horizontalCenter: menuContainer.popupUsesVerticalLine ? parent.horizontalCenter : undefined
columns: menuContainer.popupUsesVerticalLine ? 1 : (root.useSingleLineOverflowPopup ? root.hiddenBarItems.length : Math.min(5, root.hiddenBarItems.length))
spacing: 2
rowSpacing: 2
Repeater { delegate: Rectangle {
model: root.hiddenBarItems property var trayItem: modelData
property string iconSource: root.trayIconSourceFor(trayItem)
delegate: Rectangle { width: root.trayItemSize + 4
id: overflowItemRoot height: root.trayItemSize + 4
property var trayItem: modelData radius: Theme.cornerRadius
property string itemKey: root.getTrayItemKey(trayItem) color: itemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0)
property string iconSource: root.trayIconSourceFor(trayItem)
width: root.trayItemSize + 4 IconImage {
height: root.trayItemSize + 4 id: menuIconImg
z: popupDragHandler.dragging ? 100 : 0 anchors.centerIn: parent
radius: Theme.cornerRadius width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: itemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0) height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
border.width: popupDragHandler.dragging ? 2 : 0 source: parent.iconSource
border.color: Theme.primary asynchronous: true
opacity: popupDragHandler.dragging ? 0.8 : 1.0 smooth: true
mipmap: true
property real shiftOffset: root.dragShiftOffset(index, root.popupDraggedIndex, root.popupDropTargetIndex, root.trayItemSize + 6) visible: status === Image.Ready
layer.enabled: root.trayIconTintEnabled
transform: Translate { layer.effect: MultiEffect {
x: !menuContainer.popupUsesVerticalLine ? overflowItemRoot.shiftOffset + (popupDragHandler.dragging ? popupDragHandler.dragAxisOffset : 0) : 0 saturation: root.trayIconSaturation
y: menuContainer.popupUsesVerticalLine ? overflowItemRoot.shiftOffset + (popupDragHandler.dragging ? popupDragHandler.dragAxisOffset : 0) : 0 colorization: root.trayIconColorization
Behavior on x { colorizationColor: root.trayIconTintColor
enabled: !root.suppressShiftAnimation && !menuContainer.popupUsesVerticalLine
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
Behavior on y {
enabled: !root.suppressShiftAnimation && menuContainer.popupUsesVerticalLine
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
} }
}
Item { StyledText {
id: popupDragHandler anchors.centerIn: parent
anchors.fill: parent visible: !menuIconImg.visible
property bool dragging: false text: {
property point dragStartPos: Qt.point(0, 0) const itemId = trayItem?.id || "";
property real dragAxisOffset: 0 if (!itemId)
property bool longPressing: false return "?";
return itemId.charAt(0).toUpperCase();
Timer {
id: popupLongPressTimer
interval: 400
repeat: false
onTriggered: popupDragHandler.longPressing = true
}
} }
font.pixelSize: 10
color: Theme.widgetTextColor
}
IconImage { MouseArea {
id: menuIconImg id: itemArea
anchors.centerIn: parent anchors.fill: parent
width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) hoverEnabled: true
height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) acceptedButtons: Qt.LeftButton | Qt.RightButton
source: parent.iconSource cursorShape: Qt.PointingHandCursor
asynchronous: true onClicked: mouse => {
smooth: true if (!trayItem)
mipmap: true return;
visible: status === Image.Ready if (mouse.button === Qt.LeftButton && !trayItem.onlyMenu) {
layer.enabled: root.trayIconTintEnabled trayItem.activate();
layer.effect: MultiEffect { root.menuOpen = false;
saturation: root.trayIconSaturation return;
colorization: root.trayIconColorization
colorizationColor: root.trayIconTintColor
} }
} if (!trayItem.hasMenu) {
const gp = itemArea.mapToGlobal(mouse.x, mouse.y);
StyledText { root.callContextMenuFallback(trayItem.id, Math.round(gp.x), Math.round(gp.y));
anchors.centerIn: parent return;
visible: !menuIconImg.visible
text: {
const itemId = trayItem?.id || "";
if (!itemId)
return "?";
return itemId.charAt(0).toUpperCase();
}
font.pixelSize: 10
color: Theme.widgetTextColor
}
MouseArea {
id: itemArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: popupDragHandler.longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
onPressed: mouse => {
if (mouse.button === Qt.LeftButton) {
popupDragHandler.dragStartPos = Qt.point(mouse.x, mouse.y);
popupLongPressTimer.start();
}
}
onReleased: mouse => {
popupLongPressTimer.stop();
const wasDragging = popupDragHandler.dragging;
if (wasDragging)
root.finishPopupDrag();
popupDragHandler.longPressing = false;
popupDragHandler.dragging = false;
popupDragHandler.dragAxisOffset = 0;
}
onPositionChanged: mouse => {
const axisDelta = menuContainer.popupUsesVerticalLine ? (mouse.y - popupDragHandler.dragStartPos.y) : (mouse.x - popupDragHandler.dragStartPos.x);
if (popupDragHandler.longPressing && !popupDragHandler.dragging && Math.abs(axisDelta) > 5) {
popupDragHandler.dragging = true;
root.beginPopupDrag(index);
}
if (!popupDragHandler.dragging)
return;
popupDragHandler.dragAxisOffset = axisDelta;
root.updatePopupDrag(axisDelta, index);
}
onClicked: mouse => {
if (popupDragHandler.dragging)
return;
if (!trayItem)
return;
if (mouse.button === Qt.LeftButton && !trayItem.onlyMenu) {
trayItem.activate();
root.menuOpen = false;
return;
}
if (!trayItem.hasMenu) {
const gp = itemArea.mapToGlobal(mouse.x, mouse.y);
root.callContextMenuFallback(trayItem.id, Math.round(gp.x), Math.round(gp.y));
return;
}
root.showForTrayItem(trayItem, menuContainer, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
} }
root.showForTrayItem(trayItem, menuContainer, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
} }
} }
} }
@@ -1723,13 +1555,11 @@ BasePill {
anchorPos = Qt.point(targetX, targetY); anchorPos = Qt.point(targetX, targetY);
} }
} else { } else {
// Window-local maps directly to screen-local because the bar window spans const globalPos = targetItem.mapToGlobal(0, 0);
// the full screen edge; this avoids mixing mapToGlobal with a separately- const screenX = screen.x || 0;
// tracked screen.x/.y origin, which desync on non-primary monitors and after const screenY = screen.y || 0;
// DPMS/hotplug. const relativeX = globalPos.x - screenX;
const localPos = targetItem.mapToItem(null, 0, 0); const relativeY = globalPos.y - screenY;
const relativeX = localPos.x;
const relativeY = localPos.y;
if (menuRoot.isVertical) { if (menuRoot.isVertical) {
const edge = menuRoot.axis?.edge; const edge = menuRoot.axis?.edge;
@@ -1865,12 +1695,7 @@ BasePill {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Theme.spacingS anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: { text: menuRoot.trayItem?.id || "Unknown"
const itemId = menuRoot.trayItem?.id || "Unknown";
if (root.isAutoOverflowTrayItem(menuRoot.trayItem))
return itemId + " · " + I18n.tr("Keep in Bar");
return itemId;
}
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium color: Theme.surfaceTextMedium
elide: Text.ElideMiddle elide: Text.ElideMiddle
@@ -1881,11 +1706,7 @@ BasePill {
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: Theme.spacingS anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
name: { name: SessionData.isHiddenTrayId(root.getTrayItemKey(menuRoot.trayItem)) ? "visibility" : "visibility_off"
if (root.isAutoOverflowTrayItem(menuRoot.trayItem))
return "push_pin";
return root.isManualHiddenTrayItem(menuRoot.trayItem) ? "visibility" : "visibility_off";
}
size: 16 size: 16
color: Theme.widgetTextColor color: Theme.widgetTextColor
} }
@@ -1899,9 +1720,7 @@ BasePill {
const itemKey = root.getTrayItemKey(menuRoot.trayItem); const itemKey = root.getTrayItemKey(menuRoot.trayItem);
if (!itemKey) if (!itemKey)
return; return;
if (root.isAutoOverflowTrayItem(menuRoot.trayItem)) { if (SessionData.isHiddenTrayId(itemKey)) {
root.promoteTrayItemToBar(menuRoot.trayItem);
} else if (root.isManualHiddenTrayItem(menuRoot.trayItem)) {
SessionData.showTrayId(itemKey); SessionData.showTrayId(itemKey);
} else { } else {
SessionData.hideTrayId(itemKey); SessionData.hideTrayId(itemKey);
+8 -5
View File
@@ -106,15 +106,18 @@ BasePill {
} }
if (root.isVerticalOrientation) { if (root.isVerticalOrientation) {
const localPos = mapToItem(null, width / 2, height / 2); const globalPos = mapToGlobal(width / 2, height / 2);
const currentScreen = root.parentScreen || Screen; const currentScreen = root.parentScreen || Screen;
const adjustedY = localPos.y + root.minTooltipY; const screenX = currentScreen ? currentScreen.x : 0;
const screenY = currentScreen ? currentScreen.y : 0;
const relativeY = globalPos.y - screenY;
const adjustedY = relativeY + root.minTooltipY;
const tooltipX = root.axis?.edge === "left" ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (currentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS); const tooltipX = root.axis?.edge === "left" ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (currentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS);
const isLeft = root.axis?.edge === "left"; const isLeft = root.axis?.edge === "left";
tooltipLoader.item.show(tooltipText, tooltipX, adjustedY, currentScreen, isLeft, !isLeft); tooltipLoader.item.show(tooltipText, screenX + tooltipX, adjustedY, currentScreen, isLeft, !isLeft);
} else { } else {
const isBottom = root.axis?.edge === "bottom"; const isBottom = root.axis?.edge === "bottom";
const localPos = mapToItem(null, width / 2, 0); const globalPos = mapToGlobal(width / 2, 0);
const currentScreen = root.parentScreen || Screen; const currentScreen = root.parentScreen || Screen;
let tooltipY; let tooltipY;
@@ -125,7 +128,7 @@ BasePill {
tooltipY = root.barThickness + root.barSpacing + Theme.spacingXS; tooltipY = root.barThickness + root.barSpacing + Theme.spacingXS;
} }
tooltipLoader.item.show(tooltipText, localPos.x, tooltipY, currentScreen, false, false); tooltipLoader.item.show(tooltipText, globalPos.x, tooltipY, currentScreen, false, false);
} }
} }
onExited: { onExited: {
@@ -9,8 +9,9 @@ BasePill {
visible: SettingsData.weatherEnabled visible: SettingsData.weatherEnabled
Component.onCompleted: WeatherService.addRef() Ref {
Component.onDestruction: WeatherService.removeRef() service: WeatherService
}
content: Component { content: Component {
Item { Item {
@@ -1192,25 +1192,38 @@ Item {
return Math.max(baseHeight + iconsExtraHeight, contentImplicitHeight + padding); return Math.max(baseHeight + iconsExtraHeight, contentImplicitHeight + padding);
} }
function colorFromMode(mode, fallbackColor, customColor, customFallbackColor) { readonly property color unfocusedColor: {
switch (mode) { switch (SettingsData.workspaceUnfocusedColorMode) {
case "primary": case "s":
case "pri": return Theme.surface;
case "sc":
return Theme.surfaceContainer;
case "sch":
return Theme.surfaceContainerHigh;
default:
return Theme.surfaceTextAlpha;
}
}
readonly property color activeColor: {
switch (SettingsData.workspaceColorMode) {
case "s":
return Theme.surface;
case "sc":
return Theme.surfaceContainer;
case "sch":
return Theme.surfaceContainerHigh;
case "none":
return unfocusedColor;
default:
return Theme.primary; return Theme.primary;
case "primaryContainer": }
return Theme.primaryContainer; }
case "secondary":
readonly property color occupiedColor: {
switch (SettingsData.workspaceOccupiedColorMode) {
case "sec": case "sec":
return Theme.secondary; return Theme.secondary;
case "secondaryContainer":
return Theme.secondaryContainer;
case "tertiary":
case "ter":
return Theme.tertiary;
case "tertiaryContainer":
return Theme.tertiaryContainer;
case "surfaceText":
return Theme.surfaceText;
case "s": case "s":
return Theme.surface; return Theme.surface;
case "sc": case "sc":
@@ -1219,34 +1232,37 @@ Item {
return Theme.surfaceContainerHigh; return Theme.surfaceContainerHigh;
case "schh": case "schh":
return Theme.surfaceContainerHighest; return Theme.surfaceContainerHighest;
case "error":
case "err":
return Theme.error;
case "custom":
return Theme.safeColor(customColor, customFallbackColor);
default: default:
return fallbackColor; return unfocusedColor;
} }
} }
readonly property color unfocusedColor: colorFromMode(SettingsData.workspaceUnfocusedColorMode, Theme.surfaceTextAlpha, SettingsData.workspaceUnfocusedCustomColor, Theme.surfaceTextAlpha) readonly property color urgentColor: {
switch (SettingsData.workspaceUrgentColorMode) {
readonly property color activeColor: { case "primary":
if (SettingsData.workspaceColorMode === "none") return Theme.primary;
return unfocusedColor; case "secondary":
return colorFromMode(SettingsData.workspaceColorMode, Theme.primary, SettingsData.workspaceFocusedCustomColor, Theme.primary); return Theme.secondary;
case "s":
return Theme.surface;
case "sc":
return Theme.surfaceContainer;
default:
return Theme.error;
}
} }
readonly property color occupiedColor: { readonly property color focusedBorderColor: {
if (SettingsData.workspaceOccupiedColorMode === "none") switch (SettingsData.workspaceFocusedBorderColor) {
return unfocusedColor; case "surfaceText":
return colorFromMode(SettingsData.workspaceOccupiedColorMode, unfocusedColor, SettingsData.workspaceOccupiedCustomColor, Theme.secondary); return Theme.surfaceText;
case "secondary":
return Theme.secondary;
default:
return Theme.primary;
}
} }
readonly property color urgentColor: colorFromMode(SettingsData.workspaceUrgentColorMode, Theme.error, SettingsData.workspaceUrgentCustomColor, Theme.error)
readonly property color focusedBorderColor: colorFromMode(SettingsData.workspaceFocusedBorderColor, Theme.primary, SettingsData.workspaceFocusedBorderCustomColor, Theme.primary)
function getContrastingIconColor(bgColor) { function getContrastingIconColor(bgColor) {
const luminance = 0.299 * bgColor.r + 0.587 * bgColor.g + 0.114 * bgColor.b; const luminance = 0.299 * bgColor.r + 0.587 * bgColor.g + 0.114 * bgColor.b;
return luminance > 0.4 ? Qt.rgba(0.15, 0.15, 0.15, 1) : Qt.rgba(0.8, 0.8, 0.8, 1); return luminance > 0.4 ? Qt.rgba(0.15, 0.15, 0.15, 1) : Qt.rgba(0.8, 0.8, 0.8, 1);
@@ -108,6 +108,9 @@ DankPopout {
MprisController.setActivePlayer(player); MprisController.setActivePlayer(player);
root.__hideDropdowns(); root.__hideDropdowns();
} }
onDeviceSelected: device => {
root.__hideDropdowns();
}
} }
} }
@@ -227,13 +230,6 @@ DankPopout {
return; return;
} }
if (root.currentTabIndex === 0 && overviewLoader.item?.handleKeyEvent) {
if (overviewLoader.item.handleKeyEvent(event)) {
event.accepted = true;
return;
}
}
if (root.currentTabIndex === 1 && mediaLoader.item?.handleKeyEvent) { if (root.currentTabIndex === 1 && mediaLoader.item?.handleKeyEvent) {
if (mediaLoader.item.handleKeyEvent(event)) { if (mediaLoader.item.handleKeyEvent(event)) {
event.accepted = true; event.accepted = true;
@@ -363,7 +359,6 @@ DankPopout {
sourceComponent: Component { sourceComponent: Component {
OverviewTab { OverviewTab {
onCloseDash: root.dashVisible = false onCloseDash: root.dashVisible = false
onNavFocusRequested: mainContainer.forceActiveFocus()
onSwitchToWeatherTab: { onSwitchToWeatherTab: {
if (SettingsData.weatherEnabled) { if (SettingsData.weatherEnabled) {
root.currentTabIndex = 3; root.currentTabIndex = 3;
@@ -383,27 +383,7 @@ Item {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: {
onPressed: mouse => {
if (mouse.button === Qt.RightButton) {
mouse.accepted = true;
}
}
onWheel: wheelEvent => {
if (SettingsData.audioDeviceScrollVolumeEnabled && wheelEvent.x >= deviceMouseArea.width / 2) {
AudioService.handleNodeVolumeWheel(modelData, wheelEvent);
} else {
wheelEvent.accepted = false;
}
}
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
if (modelData && modelData.audio) {
SessionData.suppressOSDTemporarily();
modelData.audio.muted = !modelData.audio.muted;
}
return;
}
if (modelData && modelData.name) { if (modelData && modelData.name) {
AudioService.setDefaultSinkByName(modelData.name); AudioService.setDefaultSinkByName(modelData.name);
root.deviceSelected(modelData); root.deviceSelected(modelData);
+1 -21
View File
@@ -866,27 +866,7 @@ Item {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: {
onPressed: mouse => {
if (mouse.button === Qt.RightButton) {
mouse.accepted = true;
}
}
onWheel: wheelEvent => {
const delta = wheelEvent.angleDelta.y;
if (delta !== 0) {
AudioService.cycleAudioOutputDirection(delta < 0);
wheelEvent.accepted = true;
}
}
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
if (AudioService.sink?.audio) {
SessionData.suppressOSDTemporarily();
AudioService.sink.audio.muted = !AudioService.sink.audio.muted;
}
return;
}
if (devicesExpanded) { if (devicesExpanded) {
const sinks = AudioService.getAvailableSinks(); const sinks = AudioService.getAvailableSinks();
if (sinks && sinks.length > 1) { if (sinks && sinks.length > 1) {
@@ -1,311 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property var eventData: null
property bool canEdit: false
signal editRequested
signal deleteRequested
signal closeRequested
readonly property bool _descriptionIsHtml: /<[a-z][^>]*>/i.test((eventData && eventData.description) || "")
function _styleAnchors(html) {
return html.replace(/<a\s([^>]*)>/gi, (m, attrs) => {
const cleaned = attrs.replace(/style="[^"]*"/gi, "");
return "<a style=\"text-decoration:none; color:" + Theme.primary + ";\" " + cleaned + ">";
});
}
function _inlineMarkdown(line) {
let out = line.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
out = out.replace(/\\([\\`*_{}[\]()#+\-.!~>])/g, "$1");
out = out.replace(/(?:https?:\/\/|www\.)[^\s<>)\]]*[^\s<>)\].,;:!?"']/g, (m, offset, s) => {
const prev = offset > 0 ? s[offset - 1] : "";
if (prev === "(" || prev === "[" || prev === "\"" || prev === "'")
return m;
const href = m.startsWith("www.") ? "https://" + m : m;
return "<a href=\"" + href + "\">" + m + "</a>";
});
out = out.replace(/\[([^\]]+)\]\(([^()\s]+)\)/g, "<a href=\"$2\">$1</a>");
out = out.replace(/\*\*([^*]+)\*\*/g, "<b>$1</b>");
out = out.replace(/(^|[^*])\*([^*\s][^*]*)\*/g, "$1<i>$2</i>");
return out;
}
// Descriptions arrive as HTML (Google) or markdown/plain text; both render
// as RichText so links become clickable anchors recolored to the theme.
function _descriptionRichText() {
const raw = ((eventData && eventData.description) || "").trim();
if (raw === "")
return "";
if (_descriptionIsHtml)
return _styleAnchors(raw);
const parts = [];
let list = "";
const closeList = () => {
if (list === "")
return;
parts.push("</" + list + ">");
list = "";
};
const lines = raw.split("\n");
for (let i = 0; i < lines.length; i++) {
const ul = lines[i].match(/^\s*[-*+]\s+(.+)$/);
const ol = lines[i].match(/^\s*\d+[.)]\s+(.+)$/);
if (ul || ol) {
const tag = ul ? "ul" : "ol";
if (list !== tag) {
closeList();
parts.push("<" + tag + ">");
list = tag;
}
parts.push("<li>" + _inlineMarkdown((ul || ol)[1]) + "</li>");
continue;
}
closeList();
parts.push(_inlineMarkdown(lines[i]) + "<br/>");
}
closeList();
return _styleAnchors(parts.join("").replace(/<br\/>$/, ""));
}
function _timeText() {
if (!eventData)
return "";
const dateStr = Qt.formatDate(eventData.start, "ddd, MMM d");
if (eventData.allDay)
return I18n.tr("All day") + " · " + dateStr;
const fmt = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP";
const startStr = Qt.formatTime(eventData.start, fmt);
if (eventData.start.getTime() === eventData.end.getTime())
return dateStr + " · " + startStr;
return dateStr + " · " + startStr + " " + Qt.formatTime(eventData.end, fmt);
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Qt.rgba(0, 0, 0, 0.45)
MouseArea {
anchors.fill: parent
onClicked: root.closeRequested()
}
}
Rectangle {
anchors.centerIn: parent
width: Math.min(parent.width - Theme.spacingL * 2, 380)
height: Math.min(parent.height - Theme.spacingM * 2, body.implicitHeight + Theme.spacingL * 2)
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Theme.outlineMedium
border.width: 1
clip: true
MouseArea {
anchors.fill: parent
}
DankActionButton {
id: closeButton
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Theme.spacingXS
circular: false
iconName: "close"
iconSize: 16
z: 1
onClicked: root.closeRequested()
}
DankFlickable {
anchors.fill: parent
anchors.margins: Theme.spacingL
anchors.topMargin: Theme.spacingL
contentWidth: width
contentHeight: body.implicitHeight
clip: true
Column {
id: body
width: parent.width
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingS
Rectangle {
width: 4
height: titleText.implicitHeight
radius: 2
anchors.top: parent.top
color: (root.eventData && root.eventData.color) ? root.eventData.color : Theme.primary
}
StyledText {
id: titleText
width: parent.width - 4 - Theme.spacingS - closeButton.width
text: root.eventData ? root.eventData.title : ""
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
horizontalAlignment: Text.AlignLeft
wrapMode: Text.Wrap
maximumLineCount: 3
elide: Text.ElideRight
}
}
StyledText {
width: parent.width
text: root._timeText()
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.Wrap
}
Row {
width: parent.width
spacing: Theme.spacingXS
visible: root.eventData && root.eventData.calendar
DankIcon {
name: "calendar_month"
size: 14
color: Theme.surfaceVariantText
anchors.top: parent.top
anchors.topMargin: 2
}
StyledText {
width: parent.width - 14 - Theme.spacingXS
text: {
if (!root.eventData)
return "";
const acc = root.eventData.account || "";
return root.eventData.calendar + (acc ? " · " + acc : "");
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.Wrap
maximumLineCount: 2
elide: Text.ElideRight
}
}
Row {
width: parent.width
spacing: Theme.spacingXS
visible: root.eventData && root.eventData.location
DankIcon {
name: "place"
size: 14
color: Theme.surfaceVariantText
anchors.top: parent.top
anchors.topMargin: 2
}
StyledText {
width: parent.width - 14 - Theme.spacingXS
text: root.eventData ? root.eventData.location : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.Wrap
maximumLineCount: 2
elide: Text.ElideRight
}
}
Row {
width: parent.width
spacing: Theme.spacingXS
visible: root.eventData && root.eventData.url
DankIcon {
name: "link"
size: 14
color: Theme.primary
anchors.top: parent.top
anchors.topMargin: 2
}
StyledText {
width: parent.width - 14 - Theme.spacingXS
text: root.eventData ? root.eventData.url : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
wrapMode: Text.WrapAnywhere
maximumLineCount: 2
elide: Text.ElideRight
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.eventData && root.eventData.url)
Qt.openUrlExternally(root.eventData.url);
}
}
}
}
StyledText {
id: descriptionText
width: parent.width
text: root._descriptionRichText()
visible: root.eventData && root.eventData.description
textFormat: Text.RichText
linkColor: Theme.primary
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
horizontalAlignment: Text.AlignLeft
wrapMode: Text.Wrap
onLinkActivated: link => Qt.openUrlExternally(link)
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: descriptionText.hoveredLink !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
Row {
width: parent.width
spacing: Theme.spacingS
visible: root.canEdit
topPadding: Theme.spacingXS
DankButton {
text: I18n.tr("Edit")
iconName: "edit"
buttonHeight: 32
onClicked: root.editRequested()
}
DankButton {
text: I18n.tr("Delete")
iconName: "delete"
buttonHeight: 32
backgroundColor: Theme.withAlpha(Theme.error, 0.15)
textColor: Theme.error
onClicked: root.deleteRequested()
}
}
}
}
}
}
@@ -1,350 +0,0 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property var eventData: null
property date initialDate: new Date()
signal saved
signal closeRequested
property string fTitle: ""
property bool fAllDay: false
property date fDate: initialDate
property string fStart: "10:00"
property string fEnd: "11:00"
property string fLocation: ""
property string fDescription: ""
property string fCalendarId: ""
property int fReminder: -1
property string errorText: ""
property bool saving: false
readonly property var _cals: CalendarService.writableCalendars()
readonly property var _remLabels: [I18n.tr("No reminder"), I18n.tr("At start"), I18n.tr("5 min before"), I18n.tr("10 min before"), I18n.tr("15 min before"), I18n.tr("30 min before"), I18n.tr("1 hour before"), I18n.tr("1 day before")]
readonly property var _remMins: [-1, 0, 5, 10, 15, 30, 60, 1440]
function _parseTime(value) {
const m = value.trim().match(/^(\d{1,2}):(\d{2})$/);
if (!m)
return null;
const h = parseInt(m[1]);
const min = parseInt(m[2]);
if (h > 23 || min > 59)
return null;
return {
"h": h,
"m": min
};
}
function _isoFromDateTime(dateObj, h, m) {
const d = new Date(dateObj);
d.setHours(h, m, 0, 0);
return d.toISOString();
}
function _allDayIso(dateObj, dayOffset) {
return new Date(Date.UTC(dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate() + dayOffset)).toISOString();
}
function _calendarName(id) {
for (let i = 0; i < _cals.length; i++) {
if (_cals[i].id === id)
return _cals[i].name;
}
return _cals.length > 0 ? _cals[0].name : "";
}
function save() {
const title = fTitle.trim();
if (!title) {
errorText = I18n.tr("Title is required");
return;
}
let calId = fCalendarId;
if (!calId) {
const def = CalendarService.defaultCalendar();
calId = def ? def.id : "";
}
if (!calId) {
errorText = I18n.tr("No writable calendar available");
return;
}
let startIso, endIso;
if (fAllDay) {
startIso = _allDayIso(fDate, 0);
endIso = _allDayIso(fDate, 1);
} else {
const s = _parseTime(fStart);
const e = _parseTime(fEnd);
if (!s || !e) {
errorText = I18n.tr("Use HH:MM time format");
return;
}
startIso = _isoFromDateTime(fDate, s.h, s.m);
endIso = _isoFromDateTime(fDate, e.h, e.m);
if (new Date(endIso).getTime() <= new Date(startIso).getTime()) {
errorText = I18n.tr("End must be after start");
return;
}
}
const fields = {
"calendarId": calId,
"summary": title,
"description": fDescription,
"location": fLocation,
"start": startIso,
"end": endIso,
"allDay": fAllDay,
"reminders": fReminder >= 0 ? [
{
"method": "popup",
"minutes": fReminder
}
] : []
};
saving = true;
errorText = "";
const cb = response => {
saving = false;
if (response.error) {
errorText = response.error;
return;
}
root.saved();
};
if (eventData && eventData.id)
CalendarService.updateEvent(eventData.id, fields, cb);
else
CalendarService.createEvent(fields, cb);
}
Component.onCompleted: {
if (!eventData) {
fCalendarId = CalendarService.defaultCalendar() ? CalendarService.defaultCalendar().id : "";
return;
}
fTitle = eventData.title || "";
fAllDay = !!eventData.allDay;
fDate = eventData.start;
const fmt = "HH:mm";
fStart = Qt.formatTime(eventData.start, fmt);
fEnd = Qt.formatTime(eventData.end, fmt);
fLocation = eventData.location || "";
fDescription = eventData.description || "";
fCalendarId = eventData.calendarId || "";
if (eventData.reminders && eventData.reminders.length > 0)
fReminder = eventData.reminders[0].minutes;
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Qt.rgba(0, 0, 0, 0.45)
MouseArea {
anchors.fill: parent
onClicked: root.closeRequested()
}
}
Rectangle {
anchors.centerIn: parent
width: Math.min(parent.width - Theme.spacingL * 2, 400)
height: Math.min(parent.height - Theme.spacingM, 300)
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
border.color: Theme.outlineMedium
border.width: 1
MouseArea {
anchors.fill: parent
}
DankFlickable {
anchors.fill: parent
anchors.margins: Theme.spacingM
contentWidth: width
contentHeight: form.implicitHeight
clip: true
Column {
id: form
width: parent.width
spacing: Theme.spacingS
StyledText {
width: parent.width
text: root.eventData ? I18n.tr("Edit event") : I18n.tr("New event")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
horizontalAlignment: Text.AlignLeft
}
DankTextField {
width: parent.width
labelText: I18n.tr("Title")
leftIconName: "title"
leftIconSize: Theme.iconSize - 6
placeholderText: I18n.tr("Event title")
text: root.fTitle
onTextChanged: root.fTitle = text
}
DankToggle {
width: parent.width
text: I18n.tr("All day")
checked: root.fAllDay
onToggled: checked => root.fAllDay = checked
}
Row {
width: parent.width
spacing: Theme.spacingXS
DankActionButton {
circular: false
iconName: "chevron_left"
iconSize: 16
onClicked: {
let d = new Date(root.fDate);
d.setDate(d.getDate() - 1);
root.fDate = d;
}
}
StyledText {
width: parent.width - 72
text: Qt.formatDate(root.fDate, "ddd, MMM d yyyy")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
height: 32
}
DankActionButton {
circular: false
iconName: "chevron_right"
iconSize: 16
onClicked: {
let d = new Date(root.fDate);
d.setDate(d.getDate() + 1);
root.fDate = d;
}
}
}
Row {
width: parent.width
spacing: Theme.spacingS
visible: !root.fAllDay
DankTextField {
width: (parent.width - Theme.spacingS) / 2
labelText: I18n.tr("Start")
leftIconName: "schedule"
leftIconSize: Theme.iconSize - 6
placeholderText: "HH:MM"
text: root.fStart
onTextChanged: root.fStart = text
}
DankTextField {
width: (parent.width - Theme.spacingS) / 2
labelText: I18n.tr("End")
placeholderText: "HH:MM"
text: root.fEnd
onTextChanged: root.fEnd = text
}
}
DankDropdown {
width: parent.width
text: I18n.tr("Calendar")
options: root._cals.map(c => c.name)
currentValue: root._calendarName(root.fCalendarId)
onValueChanged: value => {
for (let i = 0; i < root._cals.length; i++) {
if (root._cals[i].name === value) {
root.fCalendarId = root._cals[i].id;
return;
}
}
}
}
DankDropdown {
width: parent.width
text: I18n.tr("Reminder")
options: root._remLabels
currentValue: root._remLabels[Math.max(0, root._remMins.indexOf(root.fReminder))]
onValueChanged: value => {
const idx = root._remLabels.indexOf(value);
if (idx >= 0)
root.fReminder = root._remMins[idx];
}
}
DankTextField {
width: parent.width
labelText: I18n.tr("Location")
leftIconName: "place"
leftIconSize: Theme.iconSize - 6
placeholderText: I18n.tr("Add location")
text: root.fLocation
onTextChanged: root.fLocation = text
}
DankTextField {
width: parent.width
labelText: I18n.tr("Notes")
leftIconName: "notes"
leftIconSize: Theme.iconSize - 6
placeholderText: I18n.tr("Add notes")
text: root.fDescription
onTextChanged: root.fDescription = text
}
StyledText {
width: parent.width
text: root.errorText
visible: root.errorText !== ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
wrapMode: Text.WordWrap
}
Row {
width: parent.width
spacing: Theme.spacingS
DankButton {
text: root.saving ? I18n.tr("Saving…") : I18n.tr("Save")
iconName: "check"
buttonHeight: 32
backgroundColor: Theme.primary
textColor: Theme.primaryText
enabled: !root.saving
onClicked: root.save()
}
DankButton {
text: I18n.tr("Cancel")
buttonHeight: 32
onClicked: root.closeRequested()
}
}
}
}
}
}
@@ -8,21 +8,14 @@ Rectangle {
id: root id: root
readonly property var log: Log.scoped("CalendarOverviewCard") readonly property var log: Log.scoped("CalendarOverviewCard")
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
implicitWidth: SettingsData.showWeekNumber ? 736 : 700 implicitWidth: SettingsData.showWeekNumber ? 736 : 700
property bool showEventDetails: false property bool showEventDetails: false
property date selectedDate: systemClock.date property date selectedDate: systemClock.date
property var selectedDateEvents: [] property var selectedDateEvents: []
property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0 property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0
property var detailEvent: null
property bool showEditor: false
property var editorEvent: null
signal closeDash signal closeDash
signal navFocusRequested
function weekStartQt() { function weekStartQt() {
if (SettingsData.firstDayOfWeek >= 7 || SettingsData.firstDayOfWeek < 0) { if (SettingsData.firstDayOfWeek >= 7 || SettingsData.firstDayOfWeek < 0) {
@@ -86,7 +79,7 @@ Rectangle {
} }
function updateSelectedDateEvents() { function updateSelectedDateEvents() {
if (CalendarService && CalendarService.calendarAvailable) { if (CalendarService && CalendarService.khalAvailable) {
const events = CalendarService.getEventsForDate(selectedDate); const events = CalendarService.getEventsForDate(selectedDate);
selectedDateEvents = events; selectedDateEvents = events;
} else { } else {
@@ -95,7 +88,7 @@ Rectangle {
} }
function loadEventsForMonth() { function loadEventsForMonth() {
if (!CalendarService || !CalendarService.calendarAvailable) { if (!CalendarService || !CalendarService.khalAvailable) {
return; return;
} }
@@ -111,83 +104,11 @@ Rectangle {
CalendarService.loadEvents(startDate, endDate); CalendarService.loadEvents(startDate, endDate);
} }
function goToToday() {
const now = systemClock.date;
calendarGrid.selectedDate = now;
calendarGrid.displayDate = now;
root.selectedDate = now;
loadEventsForMonth();
}
function moveSelection(days) {
let d = new Date(calendarGrid.selectedDate);
d.setDate(d.getDate() + days);
calendarGrid.selectedDate = d;
root.selectedDate = d;
if (d.getMonth() !== calendarGrid.displayDate.getMonth() || d.getFullYear() !== calendarGrid.displayDate.getFullYear()) {
calendarGrid.displayDate = d;
loadEventsForMonth();
}
}
function shiftMonth(delta) {
let d = new Date(calendarGrid.displayDate);
d.setMonth(d.getMonth() + delta);
calendarGrid.displayDate = d;
loadEventsForMonth();
}
function handleKeyEvent(event) {
if (showEventDetails) {
if (event.key === Qt.Key_Escape) {
showEventDetails = false;
return true;
}
return false;
}
switch (event.key) {
case Qt.Key_Left:
case Qt.Key_H:
moveSelection(I18n.isRtl ? 1 : -1);
return true;
case Qt.Key_Right:
case Qt.Key_L:
moveSelection(I18n.isRtl ? -1 : 1);
return true;
case Qt.Key_Up:
case Qt.Key_K:
moveSelection(-7);
return true;
case Qt.Key_Down:
case Qt.Key_J:
moveSelection(7);
return true;
case Qt.Key_PageUp:
shiftMonth(-1);
return true;
case Qt.Key_PageDown:
shiftMonth(1);
return true;
case Qt.Key_T:
goToToday();
return true;
case Qt.Key_Return:
case Qt.Key_Enter:
case Qt.Key_Space:
root.selectedDate = calendarGrid.selectedDate;
showEventDetails = true;
return true;
}
return false;
}
onSelectedDateChanged: updateSelectedDateEvents() onSelectedDateChanged: updateSelectedDateEvents()
onShowEventDetailsChanged: { onShowEventDetailsChanged: {
if (showEventDetails) { if (showEventDetails) {
taskInput.forceActiveFocus(); taskInput.forceActiveFocus();
} else {
navFocusRequested();
} }
} }
@@ -201,8 +122,8 @@ Rectangle {
updateSelectedDateEvents(); updateSelectedDateEvents();
} }
function onCalendarAvailableChanged() { function onKhalAvailableChanged() {
if (CalendarService && CalendarService.calendarAvailable) { if (CalendarService && CalendarService.khalAvailable) {
loadEventsForMonth(); loadEventsForMonth();
} }
updateSelectedDateEvents(); updateSelectedDateEvents();
@@ -222,55 +143,6 @@ Rectangle {
anchors.margins: Theme.spacingM anchors.margins: Theme.spacingM
spacing: Theme.spacingS spacing: Theme.spacingS
Rectangle {
id: dankWarning
width: parent.width
visible: CalendarService && CalendarService.dankNeedsLaunch
height: visible ? Math.max(28, warningRow.implicitHeight) + Theme.spacingS : 0
radius: Theme.cornerRadius
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12)
border.color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.35)
border.width: 1
Row {
id: warningRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingS
DankIcon {
name: "warning"
size: 16
color: Theme.warning
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
width: parent.width - 16 - Theme.spacingS - (launchButton.visible ? launchButton.width + Theme.spacingS : 0)
anchors.verticalCenter: parent.verticalCenter
text: (CalendarService && CalendarService.dankBinaryExists) ? I18n.tr("DankCalendar isn't running") : I18n.tr("DankCalendar isn't installed")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
horizontalAlignment: Text.AlignLeft
wrapMode: Text.Wrap
}
DankButton {
id: launchButton
anchors.verticalCenter: parent.verticalCenter
visible: CalendarService && CalendarService.dankBinaryExists
text: I18n.tr("Launch")
buttonHeight: 26
backgroundColor: Theme.primary
textColor: Theme.primaryText
onClicked: CalendarService.launchDankCalendar()
}
}
}
Item { Item {
width: parent.width width: parent.width
height: 40 height: 40
@@ -301,40 +173,11 @@ Rectangle {
} }
} }
Rectangle {
width: 32
height: 32
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
radius: Theme.cornerRadius
visible: CalendarService && CalendarService.canCreateEvents
color: addEventArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "event"
size: 16
color: Theme.primary
}
MouseArea {
id: addEventArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.editorEvent = null;
root.showEditor = true;
}
}
}
StyledText { StyledText {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.leftMargin: 32 + Theme.spacingS * 2 anchors.leftMargin: 32 + Theme.spacingS * 2
anchors.rightMargin: (CalendarService && CalendarService.canCreateEvents) ? 32 + Theme.spacingS * 2 : Theme.spacingS anchors.rightMargin: Theme.spacingS
height: 40 height: 40
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: { text: {
@@ -386,7 +229,7 @@ Rectangle {
} }
StyledText { StyledText {
width: parent.width - 84 width: parent.width - 56
height: 28 height: 28
text: calendarGrid.displayDate.toLocaleDateString(I18n.locale(), "MMMM yyyy") text: calendarGrid.displayDate.toLocaleDateString(I18n.locale(), "MMMM yyyy")
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
@@ -396,28 +239,6 @@ Rectangle {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }
Rectangle {
width: 28
height: 28
radius: Theme.cornerRadius
color: todayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "today"
size: 14
color: Theme.primary
}
MouseArea {
id: todayArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.goToToday()
}
}
Rectangle { Rectangle {
width: 28 width: 28
height: 28 height: 28
@@ -567,8 +388,6 @@ Rectangle {
height: width height: width
color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent" color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.color: (isSelected && !isToday) ? Theme.primary : "transparent"
border.width: (isSelected && !isToday) ? 1 : 0
StyledText { StyledText {
anchors.centerIn: parent anchors.centerIn: parent
@@ -578,31 +397,21 @@ Rectangle {
font.weight: isToday ? Font.Medium : Font.Normal font.weight: isToday ? Font.Medium : Font.Normal
} }
Row { Rectangle {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: 3 anchors.bottomMargin: 4
spacing: 2 width: 12
visible: CalendarService && CalendarService.calendarAvailable && CalendarService.hasEventsForDate(dayDate) height: 2
radius: Theme.cornerRadius
visible: CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)
color: isToday ? Qt.lighter(Theme.primary, 1.3) : Theme.primary
opacity: isToday ? 0.9 : 0.7
Repeater { Behavior on opacity {
model: { NumberAnimation {
const evs = CalendarService.getEventsForDate(dayDate); duration: Theme.shortDuration
const seen = []; easing.type: Theme.standardEasing
for (let i = 0; i < evs.length && seen.length < 3; i++) {
const c = (evs[i].color && evs[i].color.length) ? evs[i].color : "primary";
if (seen.indexOf(c) === -1)
seen.push(c);
}
return seen;
}
Rectangle {
width: 5
height: 5
radius: 2.5
color: modelData === "primary" ? (isToday ? Qt.lighter(Theme.primary, 1.3) : Theme.primary) : modelData
opacity: isToday ? 0.95 : 0.8
} }
} }
} }
@@ -614,7 +423,6 @@ Rectangle {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
calendarGrid.selectedDate = dayDate;
root.selectedDate = dayDate; root.selectedDate = dayDate;
root.showEventDetails = true; root.showEventDetails = true;
} }
@@ -814,15 +622,7 @@ Rectangle {
} }
} }
readonly property bool isTask: modelData && modelData.id && modelData.id.startsWith("task_") color: isDragging ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : (eventMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : Theme.nestedSurface)
readonly property color accentColor: {
if (isTask)
return modelData.completed ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Theme.primary;
return (modelData && modelData.color && modelData.color.length) ? modelData.color : Theme.primary;
}
readonly property color surfaceColor: isDragging ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : (eventMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : Theme.nestedSurface)
color: surfaceColor
border.color: isDragging ? Theme.primary : (eventMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : Theme.outlineMedium) border.color: isDragging ? Theme.primary : (eventMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : Theme.outlineMedium)
border.width: (isDragging || eventMouseArea.containsMouse) ? 1 : Theme.layerOutlineWidth border.width: (isDragging || eventMouseArea.containsMouse) ? 1 : Theme.layerOutlineWidth
@@ -860,22 +660,15 @@ Rectangle {
} }
} }
Item { Rectangle {
id: accentClip width: 3
width: 4 height: parent.height - 6
clip: true
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 3
Rectangle { anchors.verticalCenter: parent.verticalCenter
width: taskItem.width radius: Theme.cornerRadius
height: taskItem.height color: (modelData && modelData.id && modelData.id.startsWith("task_")) ? (modelData.completed ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Theme.primary) : Theme.primary
radius: taskItem.radius opacity: 0.8
color: taskItem.accentColor
anchors.top: parent.top
anchors.left: parent.left
}
} }
// Drag Handle // Drag Handle
@@ -974,7 +767,6 @@ Rectangle {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: (modelData && modelData.id && modelData.id.startsWith("task_") && modelData.completed) ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) : Theme.surfaceText color: (modelData && modelData.id && modelData.id.startsWith("task_") && modelData.completed) ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) : Theme.surfaceText
font.weight: Font.Medium font.weight: Font.Medium
horizontalAlignment: Text.AlignLeft
elide: Text.ElideRight elide: Text.ElideRight
maximumLineCount: 1 maximumLineCount: 1
} }
@@ -982,24 +774,21 @@ Rectangle {
StyledText { StyledText {
width: parent.width width: parent.width
text: { text: {
if (!modelData) if (!modelData || modelData.allDay) {
return ""; return I18n.tr("All day", "calendar task with no specific time");
const cal = (modelData.calendar && modelData.calendar.length) ? " · " + modelData.calendar : ""; } else if (modelData.start && modelData.end) {
if (modelData.allDay)
return I18n.tr("All day", "calendar task with no specific time") + cal;
if (modelData.start && modelData.end) {
const timeFormat = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP"; const timeFormat = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP";
const startTime = Qt.formatTime(modelData.start, timeFormat); const startTime = Qt.formatTime(modelData.start, timeFormat);
if (modelData.start.toDateString() !== modelData.end.toDateString() || modelData.start.getTime() !== modelData.end.getTime()) if (modelData.start.toDateString() !== modelData.end.toDateString() || modelData.start.getTime() !== modelData.end.getTime()) {
return startTime + " " + Qt.formatTime(modelData.end, timeFormat) + cal; return startTime + " " + Qt.formatTime(modelData.end, timeFormat);
return startTime + cal; }
return startTime;
} }
return ""; return "";
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
font.weight: Font.Normal font.weight: Font.Normal
horizontalAlignment: Text.AlignLeft
visible: text !== "" && modelData && modelData.id && !modelData.id.startsWith("task_") visible: text !== "" && modelData && modelData.id && !modelData.id.startsWith("task_")
} }
} }
@@ -1035,9 +824,8 @@ Rectangle {
taskItem.isEditing = false; taskItem.isEditing = false;
} }
Keys.onEscapePressed: event => { Keys.onEscapePressed: {
taskItem.isEditing = false; taskItem.isEditing = false;
event.accepted = true;
} }
} }
} }
@@ -1050,15 +838,18 @@ Rectangle {
anchors.leftMargin: (modelData && modelData.id && modelData.id.startsWith("task_")) ? 32 : 6 anchors.leftMargin: (modelData && modelData.id && modelData.id.startsWith("task_")) ? 32 : 6
anchors.rightMargin: (modelData && modelData.id && modelData.id.startsWith("task_")) ? 64 : 0 anchors.rightMargin: (modelData && modelData.id && modelData.id.startsWith("task_")) ? 64 : 0
hoverEnabled: true hoverEnabled: true
cursorShape: modelData ? Qt.PointingHandCursor : Qt.ArrowCursor cursorShape: (modelData && (modelData.url || (modelData.id && modelData.id.startsWith("task_")))) ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: modelData && !taskItem.isEditing enabled: modelData && (modelData.url !== "" || (modelData.id && modelData.id.startsWith("task_"))) && !taskItem.isEditing
onClicked: { onClicked: {
if (modelData && modelData.id && modelData.id.startsWith("task_")) { if (modelData && modelData.id && modelData.id.startsWith("task_")) {
CalendarService.toggleTask(modelData.id); CalendarService.toggleTask(modelData.id);
return; } else if (modelData && modelData.url && modelData.url !== "") {
if (Qt.openUrlExternally(modelData.url) === false) {
log.warn("Failed to open URL: " + modelData.url);
} else {
root.closeDash();
}
} }
if (modelData)
root.detailEvent = modelData;
} }
} }
@@ -1162,7 +953,7 @@ Rectangle {
Text { Text {
text: I18n.tr("Add a task...", "placeholder in the new-task input field") text: I18n.tr("Add a task...", "placeholder in the new-task input field")
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
visible: taskInput.text.length === 0 visible: !taskInput.text && !taskInput.activeFocus
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
@@ -1174,52 +965,6 @@ Rectangle {
text = ""; text = "";
} }
} }
Keys.onEscapePressed: event => {
root.showEventDetails = false;
event.accepted = true;
}
}
}
}
Loader {
anchors.fill: parent
z: 1000
active: root.detailEvent !== null
sourceComponent: CalendarEventDetail {
eventData: root.detailEvent
canEdit: CalendarService && CalendarService.canCreateEvents && root.detailEvent && !root.detailEvent.readOnly && !(root.detailEvent.id && root.detailEvent.id.startsWith("task_"))
onCloseRequested: root.detailEvent = null
onEditRequested: {
root.editorEvent = root.detailEvent;
root.detailEvent = null;
root.showEditor = true;
}
onDeleteRequested: {
if (root.detailEvent && root.detailEvent.id)
CalendarService.deleteEvent(root.detailEvent.id, null);
root.detailEvent = null;
}
}
}
Loader {
anchors.fill: parent
z: 1000
active: root.showEditor
sourceComponent: CalendarEventEditor {
eventData: root.editorEvent
initialDate: root.selectedDate
onCloseRequested: {
root.showEditor = false;
root.editorEvent = null;
}
onSaved: {
root.showEditor = false;
root.editorEvent = null;
} }
} }
} }
@@ -14,11 +14,6 @@ Item {
signal switchToWeatherTab signal switchToWeatherTab
signal switchToMediaTab signal switchToMediaTab
signal closeDash signal closeDash
signal navFocusRequested
function handleKeyEvent(event) {
return calendarCard.handleKeyEvent(event);
}
Item { Item {
anchors.fill: parent anchors.fill: parent
@@ -59,14 +54,12 @@ Item {
// Calendar - bottom middle (wider and taller) // Calendar - bottom middle (wider and taller)
CalendarOverviewCard { CalendarOverviewCard {
id: calendarCard
x: parent.width * 0.2 - Theme.spacingM x: parent.width * 0.2 - Theme.spacingM
y: 100 + Theme.spacingM y: 100 + Theme.spacingM
width: parent.width * 0.6 width: parent.width * 0.6
height: 300 height: 300
onCloseDash: root.closeDash() onCloseDash: root.closeDash()
onNavFocusRequested: root.navFocusRequested()
} }
// Media - bottom right (narrow and taller) // Media - bottom right (narrow and taller)
@@ -18,9 +18,6 @@ Item {
property bool showHourly: false property bool showHourly: false
property bool available: WeatherService.weather.available property bool available: WeatherService.weather.available
Component.onCompleted: WeatherService.addRef()
Component.onDestruction: WeatherService.removeRef()
function syncFrom(type) { function syncFrom(type) {
if (!dailyLoader.item || !hourlyLoader.item) if (!dailyLoader.item || !hourlyLoader.item)
return; return;
+7 -5
View File
@@ -511,11 +511,13 @@ Variants {
if (!dock.hoveredButton || !dock.reveal || slideXAnimation.running || slideYAnimation.running) if (!dock.hoveredButton || !dock.reveal || slideXAnimation.running || slideYAnimation.running)
return; return;
const buttonLocalPos = dock.hoveredButton.mapToItem(null, 0, 0); const buttonGlobalPos = dock.hoveredButton.mapToGlobal(0, 0);
const tooltipText = dock.hoveredButton.tooltipText || ""; const tooltipText = dock.hoveredButton.tooltipText || "";
if (!tooltipText) if (!tooltipText)
return; return;
const screenX = dock.screen ? (dock.screen.x || 0) : 0;
const screenY = dock.screen ? (dock.screen.y || 0) : 0;
const screenHeight = dock.screen ? dock.screen.height : 0; const screenHeight = dock.screen ? dock.screen.height : 0;
const gap = Theme.spacingS; const gap = Theme.spacingS;
@@ -525,19 +527,19 @@ Variants {
if (!dock.isVertical) { if (!dock.isVertical) {
const isBottom = SettingsData.dockPosition === SettingsData.Position.Bottom; const isBottom = SettingsData.dockPosition === SettingsData.Position.Bottom;
const tooltipX = buttonLocalPos.x + btnW / 2 + adjacentLeftBarWidth; const globalX = buttonGlobalPos.x + btnW / 2 + adjacentLeftBarWidth;
const tooltipHeight = 32; const tooltipHeight = 32;
const totalFromEdge = bgMargin + dockBackground.height + dock.borderThickness + gap; const totalFromEdge = bgMargin + dockBackground.height + dock.borderThickness + gap;
const screenRelativeY = isBottom ? (screenHeight - totalFromEdge - tooltipHeight) : totalFromEdge; const screenRelativeY = isBottom ? (screenHeight - totalFromEdge - tooltipHeight) : totalFromEdge;
dockTooltip.show(tooltipText, tooltipX, screenRelativeY, dock.screen, false, false); dockTooltip.show(tooltipText, globalX, screenRelativeY, dock.screen, false, false);
return; return;
} }
const isLeft = SettingsData.dockPosition === SettingsData.Position.Left; const isLeft = SettingsData.dockPosition === SettingsData.Position.Left;
const screenWidth = dock.screen ? dock.screen.width : 0; const screenWidth = dock.screen ? dock.screen.width : 0;
const totalFromEdge = bgMargin + dockBackground.width + dock.borderThickness + gap; const totalFromEdge = bgMargin + dockBackground.width + dock.borderThickness + gap;
const tooltipX = isLeft ? totalFromEdge : (screenWidth - totalFromEdge); const tooltipX = isLeft ? (screenX + totalFromEdge) : (screenX + screenWidth - totalFromEdge);
const screenRelativeY = buttonLocalPos.y + btnH / 2 + adjacentTopBarHeight; const screenRelativeY = buttonGlobalPos.y - screenY + btnH / 2 + adjacentTopBarHeight;
dockTooltip.show(tooltipText, tooltipX, screenRelativeY, dock.screen, isLeft, !isLeft); dockTooltip.show(tooltipText, tooltipX, screenRelativeY, dock.screen, isLeft, !isLeft);
} }
-4
View File
@@ -60,10 +60,6 @@ Scope {
function lock() { function lock() {
if (SettingsData.customPowerActionLock?.length > 0) { if (SettingsData.customPowerActionLock?.length > 0) {
Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionLock]); Quickshell.execDetached(["sh", "-c", SettingsData.customPowerActionLock]);
// The custom locker manages its own surface; DMS never engages
// WlSessionLock here, so isShellLocked stays false and the fade
// overlay would never be dismissed. Hand off by dismissing it now.
IdleService.dismissFadeToLock();
return; return;
} }
if (shouldLock || pendingLock) if (shouldLock || pendingLock)
+7 -28
View File
@@ -23,9 +23,6 @@ Scope {
property string u2fPendingMode property string u2fPendingMode
property string buffer property string buffer
property var attemptInfoMessages: []
property bool lockoutAnnouncedThisAttempt: false
signal flashMsg signal flashMsg
signal unlockRequested signal unlockRequested
@@ -121,37 +118,23 @@ Scope {
configDirectory: (dankshellConfigWatcher.loaded || nixosMarker.loaded || root.runningFromNixStore) ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam" configDirectory: (dankshellConfigWatcher.loaded || nixosMarker.loaded || root.runningFromNixStore) ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
onMessageChanged: { onMessageChanged: {
// collected by position, not text, so it works in any locale if (message.startsWith("The account is locked")) {
if (message.length > 0 && !responseRequired) root.lockMessage = message;
root.attemptInfoMessages = root.attemptInfoMessages.concat([message]); } else if (root.lockMessage && message.endsWith(" left to unlock)")) {
root.lockMessage += "\n" + message;
} else if (root.lockMessage && message && message.length > 0) {
root.lockMessage = "";
}
} }
onResponseRequiredChanged: { onResponseRequiredChanged: {
if (!responseRequired) if (!responseRequired)
return; return;
const notice = root.attemptInfoMessages.filter(m => m !== message);
if (notice.length > 0) {
root.lockMessage = notice.join("\n");
root.lockoutAnnouncedThisAttempt = true;
}
root.attemptInfoMessages = [];
respond(root.buffer); respond(root.buffer);
} }
onCompleted: res => { onCompleted: res => {
// requisite preauth can lock without ever prompting; surface it here too
if (!root.lockoutAnnouncedThisAttempt) {
if (root.attemptInfoMessages.length > 0) {
root.lockMessage = root.attemptInfoMessages.join("\n");
root.lockoutAnnouncedThisAttempt = true;
} else {
root.lockMessage = "";
}
root.attemptInfoMessages = [];
}
if (res === PamResult.Success) { if (res === PamResult.Success) {
if (!root.unlockInProgress) { if (!root.unlockInProgress) {
fprint.abort(); fprint.abort();
@@ -185,8 +168,6 @@ Scope {
function onActiveChanged() { function onActiveChanged() {
if (passwd.active) { if (passwd.active) {
root.attemptInfoMessages = [];
root.lockoutAnnouncedThisAttempt = false;
passwdActiveTimeout.restart(); passwdActiveTimeout.restart();
} else { } else {
passwdActiveTimeout.running = false; passwdActiveTimeout.running = false;
@@ -412,8 +393,6 @@ Scope {
root.u2fPending = false; root.u2fPending = false;
root.u2fPendingMode = ""; root.u2fPendingMode = "";
root.lockMessage = ""; root.lockMessage = "";
root.attemptInfoMessages = [];
root.lockoutAnnouncedThisAttempt = false;
root.resetAuthFlows(); root.resetAuthFlows();
fprint.checkAvail(); fprint.checkAvail();
u2f.checkAvail(); u2f.checkAvail();
@@ -1,47 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import Quickshell
import qs.Services
Singleton {
id: root
function connectToNetwork(network, options) {
if (!network)
return;
const actionOptions = options || {};
const ssid = network.ssid || "";
if (!ssid)
return;
const connected = actionOptions.connected ?? network.connected ?? (ssid === NetworkService.currentWifiSSID);
if (connected) {
if (actionOptions.disconnectWhenConnected ?? false)
NetworkService.disconnectWifi();
return;
}
if (shouldPromptForCredentials(network)) {
PopoutService.showWifiPasswordModal(ssid);
return;
}
NetworkService.connectToWifi(ssid);
}
function connectToNetworkFromDetails(ssid, secured, saved, enterprise, connected, options) {
connectToNetwork({
ssid: ssid,
secured: secured,
saved: saved,
enterprise: enterprise,
connected: connected
}, options);
}
function shouldPromptForCredentials(network) {
return (network.secured ?? false) && !(network.saved ?? false);
}
}
+9 -278
View File
@@ -1,6 +1,5 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
@@ -22,71 +21,21 @@ Item {
property var currentTab: NotepadStorageService.tabs.length > NotepadStorageService.currentTabIndex ? NotepadStorageService.tabs[NotepadStorageService.currentTabIndex] : null property var currentTab: NotepadStorageService.tabs.length > NotepadStorageService.currentTabIndex ? NotepadStorageService.tabs[NotepadStorageService.currentTabIndex] : null
property bool showSettingsMenu: false property bool showSettingsMenu: false
property string pendingSaveContent: "" property string pendingSaveContent: ""
readonly property bool conflictBannerVisible: currentTab !== null && NotepadStorageService.conflictTabId === currentTab.id
property var slideout: null property var slideout: null
property bool inPopout: false
property bool surfaceVisible: slideout ? slideout.isVisible : true
signal hideRequested signal hideRequested
signal popoutRequested
signal dockRequested
signal previewRequested(string content) signal previewRequested(string content)
function externalSync() {
textEditor.syncFromDisk();
}
function flushAutoSave() {
textEditor.autoSaveToSession();
}
Ref { Ref {
service: NotepadStorageService service: NotepadStorageService
} }
// In connected frame mode the slideout sits on the Overlay layer
onFileDialogOpenChanged: {
if (slideout)
slideout.suppressOverlayLayer = fileDialogOpen;
}
Connections { Connections {
target: slideout target: slideout
enabled: slideout !== null enabled: slideout !== null
function onAboutToHide() { function onAboutToHide() {
textEditor.autoSaveToSession(); textEditor.autoSaveToSession();
} }
function onRevealed() {
textEditor.syncFromDisk();
}
}
function showConflictBanner(diskContent) {
if (!currentTab)
return;
NotepadStorageService.flagConflict(currentTab.id, diskContent);
}
function resolveConflictKeepEdits() {
if (!root.conflictBannerVisible)
return;
NotepadStorageService.clearConflict();
if (currentTab && currentTab.filePath && !currentTab.isTemporary) {
root.saveToFile("file://" + currentTab.filePath);
}
}
function resolveConflictReload() {
if (!root.conflictBannerVisible)
return;
const diskContent = NotepadStorageService.conflictDiskContent;
NotepadStorageService.clearConflict();
textEditor.reloadFromDisk(diskContent);
}
function dismissConflictBanner() {
if (root.conflictBannerVisible)
NotepadStorageService.clearConflict();
} }
function hasUnsavedChanges() { function hasUnsavedChanges() {
@@ -102,14 +51,10 @@ Item {
} }
function performCreateNewTab() { function performCreateNewTab() {
textEditor.commitLiveBuffer();
NotepadStorageService.createNewTab(); NotepadStorageService.createNewTab();
textEditor.applyingShared = true;
textEditor.text = ""; textEditor.text = "";
textEditor.lastSavedContent = ""; textEditor.lastSavedContent = "";
textEditor.loadedTabId = -1;
textEditor.contentLoaded = true; textEditor.contentLoaded = true;
textEditor.applyingShared = false;
textEditor.textArea.forceActiveFocus(); textEditor.textArea.forceActiveFocus();
} }
@@ -141,6 +86,7 @@ Item {
NotepadStorageService.switchToTab(tabIndex); NotepadStorageService.switchToTab(tabIndex);
Qt.callLater(() => { Qt.callLater(() => {
textEditor.loadCurrentTabContent();
if (currentTab) { if (currentTab) {
root.currentFileName = currentTab.fileName || ""; root.currentFileName = currentTab.fileName || "";
root.currentFileUrl = currentTab.fileUrl || ""; root.currentFileUrl = currentTab.fileUrl || "";
@@ -154,7 +100,6 @@ Item {
var content = textEditor.text; var content = textEditor.text;
var filePath = fileUrl.toString().replace(/^file:\/\//, ''); var filePath = fileUrl.toString().replace(/^file:\/\//, '');
textEditor.externalWatchPaused = true;
saveFileView.path = ""; saveFileView.path = "";
pendingSaveContent = content; pendingSaveContent = content;
saveFileView.path = filePath; saveFileView.path = filePath;
@@ -164,53 +109,6 @@ Item {
}); });
} }
function saveExternalWithFreshnessCheck() {
if (!currentTab || currentTab.isTemporary || !currentTab.filePath)
return;
const filePath = currentTab.filePath;
loadFileView.path = "";
loadFileView.path = filePath;
if (!loadFileView.waitForJob()) {
saveToFile("file://" + filePath);
return;
}
Qt.callLater(() => {
if (!currentTab || currentTab.isTemporary || currentTab.filePath !== filePath)
return;
const diskContent = loadFileView.text();
if (diskContent !== undefined && diskContent !== null && diskContent !== textEditor.text && diskContent !== textEditor.lastSavedContent) {
root.showConflictBanner(diskContent);
return;
}
saveToFile("file://" + filePath);
});
}
function autoSaveExternal() {
if (!SettingsData.notepadAutoSave)
return;
if (!currentTab || currentTab.isTemporary || !currentTab.filePath)
return;
if (!textEditor.hasUnsavedChanges())
return;
const filePath = currentTab.filePath;
loadFileView.path = "";
loadFileView.path = filePath;
if (!loadFileView.waitForJob())
return;
Qt.callLater(() => {
if (!currentTab || currentTab.isTemporary || currentTab.filePath !== filePath)
return;
const diskContent = loadFileView.text();
if (diskContent === undefined || diskContent === null)
return;
if (diskContent !== textEditor.lastSavedContent)
return;
saveToFile("file://" + filePath);
});
}
function loadFromFile(fileUrl) { function loadFromFile(fileUrl) {
if (hasUnsavedTemporaryContent()) { if (hasUnsavedTemporaryContent()) {
root.pendingFileUrl = fileUrl; root.pendingFileUrl = fileUrl;
@@ -248,155 +146,14 @@ Item {
root.currentFileName = fileName; root.currentFileName = fileName;
root.currentFileUrl = fileUrl; root.currentFileUrl = fileUrl;
textEditor.loadedTabId = currentTab.id; textEditor.saveCurrentTabContent();
NotepadStorageService.clearSessionBuffer(currentTab.id);
if (root.conflictBannerVisible)
NotepadStorageService.clearConflict();
} }
}); });
} }
} }
Item {
id: conflictBanner
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: root.conflictBannerVisible ? bannerRect.implicitHeight : 0
visible: height > 0
clip: true
z: 5
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
StyledRect {
id: bannerRect
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
implicitHeight: bannerLayout.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.warning, 0.12)
border.color: Theme.withAlpha(Theme.warning, 0.5)
border.width: 1
ColumnLayout {
id: bannerLayout
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacingM
DankIcon {
Layout.alignment: Qt.AlignVCenter
name: "sync_problem"
size: Theme.iconSize - 2
color: Theme.warning
}
StyledText {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
text: I18n.tr("File changed on disk")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
wrapMode: Text.NoWrap
elide: Text.ElideRight
}
DankActionButton {
Layout.alignment: Qt.AlignVCenter
iconName: "close"
iconSize: Theme.iconSizeSmall
iconColor: Theme.surfaceText
buttonSize: 28
onClicked: root.dismissConflictBanner()
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: 32
Row {
id: bannerActions
anchors.right: parent.right
spacing: Theme.spacingS
readonly property real available: parent.width
StyledRect {
width: Math.min(keepText.implicitWidth + Theme.spacingM * 2, Math.max(104, (bannerActions.available - bannerActions.spacing) / 2))
height: 32
radius: Theme.cornerRadius
color: "transparent"
border.color: Theme.outlineMedium
border.width: 1
StateLayer {
anchors.fill: parent
cornerRadius: parent.radius
stateColor: Theme.surfaceText
onClicked: root.resolveConflictKeepEdits()
}
StyledText {
id: keepText
anchors.centerIn: parent
width: parent.width - Theme.spacingM
text: I18n.tr("Keep My Edits")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
}
}
StyledRect {
width: Math.min(reloadText.implicitWidth + Theme.spacingM * 2, Math.max(116, (bannerActions.available - bannerActions.spacing) / 2))
height: 32
radius: Theme.cornerRadius
color: Theme.primary
StateLayer {
anchors.fill: parent
cornerRadius: parent.radius
stateColor: Theme.background
onClicked: root.resolveConflictReload()
}
StyledText {
id: reloadText
anchors.centerIn: parent
width: parent.width - Theme.spacingM
text: I18n.tr("Reload From Disk")
font.pixelSize: Theme.fontSizeSmall
color: Theme.background
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
}
}
}
}
}
}
}
Column { Column {
anchors.top: conflictBanner.bottom anchors.fill: parent
anchors.topMargin: root.conflictBannerVisible ? Theme.spacingM : 0
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
spacing: Theme.spacingM spacing: Theme.spacingM
NotepadTabs { NotepadTabs {
@@ -421,12 +178,11 @@ Item {
id: textEditor id: textEditor
width: parent.width width: parent.width
height: parent.height - tabBar.height - Theme.spacingM * 2 height: parent.height - tabBar.height - Theme.spacingM * 2
inPopout: root.inPopout
surfaceVisible: root.surfaceVisible
onSaveRequested: { onSaveRequested: {
if (currentTab && !currentTab.isTemporary && currentTab.filePath) { if (currentTab && !currentTab.isTemporary && currentTab.filePath) {
root.saveExternalWithFreshnessCheck(); var fileUrl = "file://" + currentTab.filePath;
saveToFile(fileUrl);
} else { } else {
root.fileDialogOpen = true; root.fileDialogOpen = true;
saveBrowserLoader.active = true; saveBrowserLoader.active = true;
@@ -458,28 +214,12 @@ Item {
onEscapePressed: { onEscapePressed: {
textEditor.autoSaveToSession(); textEditor.autoSaveToSession();
if (showSettingsMenu) { root.hideRequested();
showSettingsMenu = false;
return;
}
if (!root.inPopout) {
root.hideRequested();
}
} }
onSettingsRequested: { onSettingsRequested: {
showSettingsMenu = !showSettingsMenu; showSettingsMenu = !showSettingsMenu;
} }
onPopoutRequested: root.popoutRequested()
onDockRequested: root.dockRequested()
onConflictDetected: diskContent => {
root.showConflictBanner(diskContent);
}
onAutoSaveRequested: root.autoSaveExternal()
} }
} }
@@ -502,24 +242,17 @@ Item {
printErrors: true printErrors: true
onSaved: { onSaved: {
if (currentTab && saveFileView.path) { if (currentTab && saveFileView.path && pendingSaveContent) {
NotepadStorageService.updateTabMetadata(NotepadStorageService.currentTabIndex, { NotepadStorageService.updateTabMetadata(NotepadStorageService.currentTabIndex, {
hasUnsavedChanges: false, hasUnsavedChanges: false,
lastSavedContent: pendingSaveContent lastSavedContent: pendingSaveContent
}); });
root.lastSavedFileContent = pendingSaveContent; root.lastSavedFileContent = pendingSaveContent;
textEditor.lastSavedContent = pendingSaveContent; pendingSaveContent = "";
textEditor.ignoreNextExternalChange = true;
textEditor.commitLiveBuffer();
if (root.conflictBannerVisible)
NotepadStorageService.clearConflict();
} }
textEditor.externalWatchPaused = false;
pendingSaveContent = "";
} }
onSaveFailed: error => { onSaveFailed: error => {
textEditor.externalWatchPaused = false;
pendingSaveContent = ""; pendingSaveContent = "";
} }
} }
@@ -565,7 +298,6 @@ Item {
root.currentFileName = fileName; root.currentFileName = fileName;
root.currentFileUrl = fileUrl; root.currentFileUrl = fileUrl;
textEditor.externalWatchPaused = true;
if (currentTab) { if (currentTab) {
NotepadStorageService.saveTabAs(NotepadStorageService.currentTabIndex, cleanPath); NotepadStorageService.saveTabAs(NotepadStorageService.currentTabIndex, cleanPath);
@@ -611,7 +343,7 @@ Item {
browserTitle: I18n.tr("Open Notepad File") browserTitle: I18n.tr("Open Notepad File")
browserIcon: "folder_open" browserIcon: "folder_open"
browserType: "notepad_load" browserType: "notepad_load"
fileExtensions: ["*"] fileExtensions: ["*.txt", "*.md", "*.*"]
allowStacking: true allowStacking: true
onFileSelected: path => { onFileSelected: path => {
@@ -644,7 +376,6 @@ Item {
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 180 modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 180
shouldBeVisible: false shouldBeVisible: false
allowStacking: true allowStacking: true
useOverlayLayer: true
onBackgroundClicked: { onBackgroundClicked: {
close(); close();
@@ -1,137 +0,0 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Notepad
FloatingWindow {
id: win
property alias shouldBeVisible: win.visible
function show() {
visible = true;
}
function hide() {
visible = false;
}
function toggle() {
visible = !visible;
}
title: I18n.tr("Notepad")
minimumSize: Qt.size(360, 320)
implicitWidth: 640
implicitHeight: 760
color: Theme.surfaceContainer
visible: false
onVisibleChanged: {
if (visible) {
Qt.callLater(notepad.externalSync);
} else {
notepad.flushAutoSave();
}
}
// A compositor close (e.g. niri close-window)
onClosed: win.visible = false
Item {
anchors.fill: parent
Item {
id: titleBar
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: 44
z: 10
MouseArea {
anchors.fill: parent
onPressed: windowControls.tryStartMove()
onDoubleClicked: windowControls.tryToggleMaximize()
}
Rectangle {
anchors.fill: parent
color: Theme.surfaceContainerHigh
opacity: 0.5
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "edit_note"
size: Theme.iconSize - 2
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Notepad")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
DankActionButton {
visible: windowControls.canMaximize
circular: false
iconName: win.maximized ? "fullscreen_exit" : "fullscreen"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: windowControls.tryToggleMaximize()
}
DankActionButton {
circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: win.hide()
}
}
}
Notepad {
id: notepad
anchors.top: titleBar.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.topMargin: Theme.spacingM
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.bottomMargin: Theme.spacingM
inPopout: true
surfaceVisible: win.visible
onHideRequested: win.hide()
onDockRequested: {
win.hide();
PopoutService.openNotepadSlideout();
}
}
}
FloatingWindowControls {
id: windowControls
targetWindow: win
}
}
+253 -450
View File
@@ -10,7 +10,6 @@ Item {
property var cachedFontFamilies: [] property var cachedFontFamilies: []
property var cachedMonoFamilies: [] property var cachedMonoFamilies: []
property bool fontsEnumerated: false property bool fontsEnumerated: false
property bool shortcutsExpanded: false
signal settingsRequested signal settingsRequested
signal findRequested signal findRequested
@@ -63,23 +62,11 @@ Item {
} }
} }
Rectangle { MouseArea {
anchors.fill: parent anchors.fill: parent
visible: root.isVisible visible: root.isVisible
onClicked: root.settingsRequested()
z: 50 z: 50
color: Qt.rgba(Theme.surface.r, Theme.surface.g, Theme.surface.b, 0.85)
WheelHandler {
// Hold scroll so the editor beneath doesn't move while settings are open.
onWheel: event => {
event.accepted = true;
}
}
MouseArea {
anchors.fill: parent
onClicked: root.settingsRequested()
}
} }
Rectangle { Rectangle {
@@ -87,8 +74,8 @@ Item {
visible: root.isVisible visible: root.isVisible
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: Math.min(360, root.width - Theme.spacingL * 2) width: 360
height: Math.min(settingsColumn.implicitHeight + Theme.spacingXL * 2, root.height - Theme.spacingL * 2) height: settingsColumn.implicitHeight + Theme.spacingXL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, Theme.notepadTransparency) color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, Theme.notepadTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
@@ -106,459 +93,275 @@ Item {
z: parent.z - 1 z: parent.z - 1
} }
DankFlickable { Column {
id: settingsFlickable id: settingsColumn
anchors.fill: parent width: parent.width - Theme.spacingXL * 2
clip: true anchors.horizontalCenter: parent.horizontalCenter
contentWidth: width anchors.top: parent.top
contentHeight: settingsColumn.implicitHeight + Theme.spacingXL * 2 anchors.topMargin: Theme.spacingXL
spacing: Theme.spacingS
Column { Rectangle {
id: settingsColumn width: parent.width
x: Theme.spacingXL height: 36
y: Theme.spacingXL color: "transparent"
width: settingsFlickable.width - Theme.spacingXL * 2
spacing: Theme.spacingS
Rectangle {
width: parent.width
height: 36
color: "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: -Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Notepad Settings")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
DankToggle {
anchors.left: parent.left
anchors.leftMargin: -Theme.spacingM
width: parent.width + Theme.spacingM
text: I18n.tr("Use Monospace Font")
description: I18n.tr("Toggle fonts")
checked: SettingsData.notepadUseMonospace
onToggled: checked => {
SettingsData.notepadUseMonospace = checked;
}
}
DankToggle {
anchors.left: parent.left
anchors.leftMargin: -Theme.spacingM
width: parent.width + Theme.spacingM
text: I18n.tr("Show Line Numbers")
description: I18n.tr("Display line numbers in editor")
checked: SettingsData.notepadShowLineNumbers
onToggled: checked => {
SettingsData.notepadShowLineNumbers = checked;
}
}
DankToggle {
anchors.left: parent.left
anchors.leftMargin: -Theme.spacingM
width: parent.width + Theme.spacingM
text: I18n.tr("Auto-save to disk")
description: I18n.tr("Automatically save changes to opened files as you type")
checked: SettingsData.notepadAutoSave
onToggled: checked => {
SettingsData.notepadAutoSave = checked;
}
}
StyledRect {
width: parent.width
height: 60
radius: Theme.cornerRadius
color: "transparent"
StateLayer {
anchors.fill: parent
anchors.leftMargin: -Theme.spacingM
width: parent.width + Theme.spacingM
stateColor: Theme.primary
cornerRadius: parent.radius
onClicked: root.findRequested()
}
Row {
anchors.left: parent.left
anchors.leftMargin: -Theme.spacingM
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "search"
size: Theme.iconSize - 2
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Find in Text")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Open search bar to find text")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
}
Rectangle {
width: parent.width
height: visible ? (fontDropdown.height + Theme.spacingS) : 0
color: "transparent"
visible: !SettingsData.notepadUseMonospace
DankDropdown {
id: fontDropdown
anchors.left: parent.left
anchors.leftMargin: -Theme.spacingM
width: parent.width + Theme.spacingM
text: I18n.tr("Font Family")
options: cachedFontFamilies
currentValue: {
if (!SettingsData.notepadFontFamily || SettingsData.notepadFontFamily === "")
return I18n.tr("Default (Global)");
else
return SettingsData.notepadFontFamily;
}
enableFuzzySearch: true
onValueChanged: value => {
if (value && (value.startsWith("Default") || value === "Default (Global)")) {
SettingsData.notepadFontFamily = "";
} else {
SettingsData.notepadFontFamily = value;
}
}
}
}
Rectangle {
width: parent.width
height: fontSizeRow.height + Theme.spacingS
color: "transparent"
Row {
id: fontSizeRow
width: parent.width
spacing: Theme.spacingS
Column {
width: parent.width - fontSizeControls.width - Theme.spacingM
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Font Size")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: SettingsData.notepadFontSize + "px"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
}
}
Row {
id: fontSizeControls
spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
DankActionButton {
buttonSize: 32
iconName: "remove"
iconSize: Theme.iconSizeSmall
enabled: SettingsData.notepadFontSize > 8
backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
iconColor: Theme.surfaceText
onClicked: {
var newSize = Math.max(8, SettingsData.notepadFontSize - 1);
SettingsData.notepadFontSize = newSize;
}
}
Rectangle {
width: 60
height: 32
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1
StyledText {
anchors.centerIn: parent
text: SettingsData.notepadFontSize + "px"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
}
DankActionButton {
buttonSize: 32
iconName: "add"
iconSize: Theme.iconSizeSmall
enabled: SettingsData.notepadFontSize < 48
backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
iconColor: Theme.surfaceText
onClicked: {
var newSize = Math.min(48, SettingsData.notepadFontSize + 1);
SettingsData.notepadFontSize = newSize;
}
}
}
}
}
Rectangle {
width: parent.width
height: transparencySliderColumn.height + Theme.spacingS
color: "transparent"
Column {
id: transparencySliderColumn
width: parent.width
spacing: Theme.spacingS
DankToggle {
anchors.left: parent.left
anchors.leftMargin: -Theme.spacingM
width: parent.width + Theme.spacingM
text: I18n.tr("Surface Opacity")
description: I18n.tr("Override global transparency for Notepad")
checked: SettingsData.notepadTransparencyOverride >= 0
onToggled: checked => {
if (checked) {
SettingsData.notepadTransparencyOverride = SettingsData.notepadLastCustomTransparency;
} else {
SettingsData.notepadTransparencyOverride = -1;
}
}
}
DankSlider {
anchors.left: parent.left
anchors.leftMargin: -Theme.spacingM
width: parent.width + Theme.spacingM
height: 24
visible: SettingsData.notepadTransparencyOverride >= 0
value: Math.round((SettingsData.notepadTransparencyOverride >= 0 ? SettingsData.notepadTransparencyOverride : SettingsData.popupTransparency) * 100)
minimum: 0
maximum: 100
unit: ""
showValue: true
wheelEnabled: false
onSliderValueChanged: newValue => {
if (SettingsData.notepadTransparencyOverride >= 0) {
SettingsData.notepadTransparencyOverride = newValue / 100;
}
}
}
}
}
Rectangle {
width: parent.width
height: gapColumn.height + Theme.spacingS
color: "transparent"
Column {
id: gapColumn
width: parent.width
spacing: Theme.spacingS
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Default Mode")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
DankButtonGroup {
model: [I18n.tr("Slideout"), I18n.tr("Popout")]
size: "small"
currentIndex: SettingsData.notepadDefaultMode === "popout" ? 1 : 0
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.notepadDefaultMode = index === 1 ? "popout" : "slideout";
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
visible: SettingsData.notepadDefaultMode !== "popout"
StyledText {
text: I18n.tr("Open From")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
DankButtonGroup {
model: [I18n.tr("Right"), I18n.tr("Left")]
size: "small"
currentIndex: SettingsData.notepadSlideoutSide === "left" ? 1 : 0
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.notepadSlideoutSide = index === 1 ? "left" : "right";
}
}
}
DankToggle {
anchors.left: parent.left
anchors.leftMargin: -Theme.spacingM
width: parent.width + Theme.spacingM
text: I18n.tr("Auto Compositor Gaps")
description: I18n.tr("Inset the Notepad from screen edges using the compositor's configured gaps")
checked: SettingsData.notepadUseCompositorGap
onToggled: checked => {
SettingsData.notepadUseCompositorGap = checked;
}
}
StyledText {
visible: !SettingsData.notepadUseCompositorGap
text: I18n.tr("Manual Gaps")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
DankSlider {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingXS
width: parent.width - Theme.spacingXS * 2
height: 24
visible: !SettingsData.notepadUseCompositorGap
value: SettingsData.notepadEdgeGap
minimum: 0
maximum: 64
unit: "px"
showValue: true
wheelEnabled: false
onSliderValueChanged: newValue => {
SettingsData.notepadEdgeGap = newValue;
}
}
}
}
StyledText { StyledText {
width: parent.width anchors.left: parent.left
text: SettingsData.notepadUseMonospace ? I18n.tr("Using global monospace font from Settings → Personalization") : I18n.tr("Global fonts can be configured in Settings → Personalization") anchors.leftMargin: -Theme.spacingXS
font.pixelSize: Theme.fontSizeSmall anchors.verticalCenter: parent.verticalCenter
color: Theme.surfaceTextMedium text: I18n.tr("Notepad Font Settings")
wrapMode: Text.WordWrap font.pixelSize: Theme.fontSizeMedium
opacity: 0.8 font.weight: Font.Medium
color: Theme.surfaceText
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
DankToggle {
anchors.left: parent.left
anchors.leftMargin: -Theme.spacingM
width: parent.width + Theme.spacingM
text: I18n.tr("Use Monospace Font")
description: I18n.tr("Toggle fonts")
checked: SettingsData.notepadUseMonospace
onToggled: checked => {
SettingsData.notepadUseMonospace = checked;
}
}
DankToggle {
anchors.left: parent.left
anchors.leftMargin: -Theme.spacingM
width: parent.width + Theme.spacingM
text: I18n.tr("Show Line Numbers")
description: I18n.tr("Display line numbers in editor")
checked: SettingsData.notepadShowLineNumbers
onToggled: checked => {
SettingsData.notepadShowLineNumbers = checked;
}
}
StyledRect {
width: parent.width
height: 60
radius: Theme.cornerRadius
color: "transparent"
StateLayer {
anchors.fill: parent
anchors.leftMargin: -Theme.spacingM
width: parent.width + Theme.spacingM
stateColor: Theme.primary
cornerRadius: parent.radius
onClicked: root.findRequested()
} }
StyledRect { Row {
width: parent.width anchors.left: parent.left
implicitHeight: shortcutsHeader.height + (root.shortcutsExpanded ? shortcutsColumn.implicitHeight + Theme.spacingM : 0) anchors.leftMargin: -Theme.spacingM
radius: Theme.cornerRadius anchors.right: parent.right
color: root.shortcutsExpanded ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : "transparent" anchors.rightMargin: Theme.spacingM
border.color: root.shortcutsExpanded ? Theme.primary : Theme.outlineMedium anchors.verticalCenter: parent.verticalCenter
border.width: root.shortcutsExpanded ? 2 : 1 spacing: Theme.spacingM
StateLayer { DankIcon {
anchors.fill: parent name: "search"
stateColor: Theme.primary size: Theme.iconSize - 2
cornerRadius: parent.radius color: Theme.primary
onClicked: root.shortcutsExpanded = !root.shortcutsExpanded anchors.verticalCenter: parent.verticalCenter
}
Row {
id: shortcutsHeader
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingS
height: 36
spacing: Theme.spacingS
DankIcon {
name: root.shortcutsExpanded ? "expand_less" : "expand_more"
size: Theme.iconSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Keyboard Shortcuts")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
} }
Column { Column {
id: shortcutsColumn anchors.verticalCenter: parent.verticalCenter
visible: root.shortcutsExpanded spacing: Theme.spacingXS
width: parent.width - Theme.spacingL * 2
anchors.top: shortcutsHeader.bottom
anchors.horizontalCenter: parent.horizontalCenter
spacing: 2
StyledText { StyledText {
width: parent.width text: I18n.tr("Find in Text")
text: I18n.tr("Ctrl+S: Save • Ctrl+O: Open • Ctrl+N: New • Ctrl+F: Find") font.pixelSize: Theme.fontSizeMedium
font.pixelSize: Theme.fontSizeSmall font.weight: Font.Medium
color: Theme.surfaceText color: Theme.surfaceText
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
} }
StyledText { StyledText {
width: parent.width text: I18n.tr("Open search bar to find text")
text: I18n.tr("Ctrl+A: Select All • Ctrl+P: Preview • Enter/Shift+Enter: Find Next/Previous • Esc: Close")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
} }
} }
} }
} }
Rectangle {
width: parent.width
height: visible ? (fontDropdown.height + Theme.spacingS) : 0
color: "transparent"
visible: !SettingsData.notepadUseMonospace
DankDropdown {
id: fontDropdown
anchors.left: parent.left
anchors.leftMargin: -Theme.spacingM
width: parent.width + Theme.spacingM
text: I18n.tr("Font Family")
options: cachedFontFamilies
currentValue: {
if (!SettingsData.notepadFontFamily || SettingsData.notepadFontFamily === "")
return I18n.tr("Default (Global)");
else
return SettingsData.notepadFontFamily;
}
enableFuzzySearch: true
onValueChanged: value => {
if (value && (value.startsWith("Default") || value === "Default (Global)")) {
SettingsData.notepadFontFamily = "";
} else {
SettingsData.notepadFontFamily = value;
}
}
}
}
Rectangle {
width: parent.width
height: fontSizeRow.height + Theme.spacingS
color: "transparent"
Row {
id: fontSizeRow
width: parent.width
spacing: Theme.spacingS
Column {
width: parent.width - fontSizeControls.width - Theme.spacingM
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Font Size")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: SettingsData.notepadFontSize + "px"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
}
}
Row {
id: fontSizeControls
spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
DankActionButton {
buttonSize: 32
iconName: "remove"
iconSize: Theme.iconSizeSmall
enabled: SettingsData.notepadFontSize > 8
backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
iconColor: Theme.surfaceText
onClicked: {
var newSize = Math.max(8, SettingsData.notepadFontSize - 1);
SettingsData.notepadFontSize = newSize;
}
}
Rectangle {
width: 60
height: 32
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1
StyledText {
anchors.centerIn: parent
text: SettingsData.notepadFontSize + "px"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
}
DankActionButton {
buttonSize: 32
iconName: "add"
iconSize: Theme.iconSizeSmall
enabled: SettingsData.notepadFontSize < 48
backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
iconColor: Theme.surfaceText
onClicked: {
var newSize = Math.min(48, SettingsData.notepadFontSize + 1);
SettingsData.notepadFontSize = newSize;
}
}
}
}
}
Rectangle {
width: parent.width
height: transparencySliderColumn.height + Theme.spacingS
color: "transparent"
Column {
id: transparencySliderColumn
width: parent.width
spacing: Theme.spacingS
DankToggle {
anchors.left: parent.left
anchors.leftMargin: -Theme.spacingM
width: parent.width + Theme.spacingM
text: I18n.tr("Custom Transparency")
description: I18n.tr("Override global transparency for Notepad")
checked: SettingsData.notepadTransparencyOverride >= 0
onToggled: checked => {
if (checked) {
SettingsData.notepadTransparencyOverride = SettingsData.notepadLastCustomTransparency;
} else {
SettingsData.notepadTransparencyOverride = -1;
}
}
}
DankSlider {
anchors.left: parent.left
anchors.leftMargin: -Theme.spacingM
width: parent.width + Theme.spacingM
height: 24
visible: SettingsData.notepadTransparencyOverride >= 0
value: Math.round((SettingsData.notepadTransparencyOverride >= 0 ? SettingsData.notepadTransparencyOverride : SettingsData.popupTransparency) * 100)
minimum: 0
maximum: 100
unit: ""
showValue: true
wheelEnabled: false
onSliderValueChanged: newValue => {
if (SettingsData.notepadTransparencyOverride >= 0) {
SettingsData.notepadTransparencyOverride = newValue / 100;
}
}
}
}
}
StyledText {
width: parent.width
text: SettingsData.notepadUseMonospace ? I18n.tr("Using global monospace font from Settings → Personalization") : I18n.tr("Global fonts can be configured in Settings → Personalization")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
wrapMode: Text.WordWrap
opacity: 0.8
}
} }
} }
} }

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