1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-06 05:28:29 -04:00

Compare commits

...

23 Commits

Author SHA1 Message Date
purian23 82a9824175 feat(theme): global floating window styling & transparency settings 2026-08-05 18:00:31 -04:00
lankong0522 2baf048293 feat(tray): add IPC action to open tray menus (#2701)
* feat(tray): add IPC action to open tray menus

* tray: open IPC-requested menus on the focused screen only

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-08-05 17:31:25 -04:00
bbedward cffc33e14a display config/niri: fix nested kdl blocks as being attributed to output
state

fixes #2966
port 1.5
2026-08-05 17:02:38 -04:00
bbedward 0c811e3417 gamma: validate config before write and publish state every scheduler
wakeup

fixes #2967
port 1.5
2026-08-05 16:55:34 -04:00
bbedward 6ad46cf2c2 bar: fix shadow from tinting goth corners
fixes #2975
port 1.5
2026-08-04 22:15:34 -04:00
Thomas Kroll 34626070af fix(tray): don't dismiss click-opened tray menus from hover controller (#2979)
closeHoverSurfaces() called TrayMenuManager.closeAllMenus(), tearing down
every registered tray menu including ones opened by right-click. With
hoverPopouts enabled, moving the pointer after a right-click re-ran the
hit test and closed the menu before it could be reached.

Tag each menu with how it was opened and add closeHoverMenus(), which
only closes hover-opened menus. The hover controller now uses that.

Click-driven callers pass no argument, so byHover stays undefined and
`byHover === true` evaluates to false.

Fixes #2978


Claude-Session: https://claude.ai/code/session_01DYF9vdsaToU4Usu7kxPCNB

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 21:25:22 -04:00
bbedward 80b27b9a6a settings: remove dead keys
fixes #2977
2026-08-04 21:25:06 -04:00
jack-mil 49f968d26b fix(mpris): remove required non-null artist/title from idle condition (#2993)
The current isIdle detection only works if the player has a title _and_ artist metadata,
which is not appropriate for all cases, e.g. MPV playing a video does not set artist.

In the current implementation, after a video stops playing in MPV, and the window closes
(but MPV and the MPRIS plugin server is still running as a background process), the playback controls remain
shown on the bar, and have non-functional controls.

It is sufficient to only check for the "Stopped" playing state. The MPRIS specification
states that Playback_Status==Stopped should be used to indicate that *NO* track is currently
playing. A compliant player should set that, and should also set the CanPlay/CanPause boolean
to false is not track is being played.

See: https://specifications.freedesktop.org/mpris/latest/Player_Interface.html#Enum:Playback_Status
2026-08-04 21:22:22 -04:00
Huỳnh Thiện Lộc 99b0dc596d feat(wallpaper): support random cycling order (#2936) 2026-08-04 21:21:52 -04:00
rredlip 158c0c12d8 Changed margin of the rectangle so the pixel of the half circle are completely covered. (#2989) 2026-08-04 21:20:53 -04:00
bbedward e089948225 powermenu: add soft reboot option for systemd systems
fixes #2986
port 1.5
2026-08-04 18:04:19 -04:00
bbedward 7f2ba56e06 dank16: correct DPS lab lightness scale
fixes #2987
port 1.5
2026-08-04 17:55:53 -04:00
nozsh 6de5593216 feat(keybinds): add keybind for PAM U2F on lock screen (#2930)
* feat(keybinds): add keybind for PAM U2F on lock screen

* add: tooltip, settings json & error handling

---------
2026-08-03 22:09:53 -04:00
bbedward 365474b0d9 launcher: dispatch plugin-category items as plugin, not app
fixes #2988
port 1.5
2026-08-03 20:05:23 -04:00
bbedward dc8a47644a evdev: resolve capslock state from any available keyboard
fixes #2991
port 1.5
2026-08-03 19:59:12 -04:00
bbedward 27483e68dc dankinstall/gentoo: perform autounmask on install
fixes #2992
port 1.5
2026-08-03 19:40:00 -04:00
purian23 400a18a8ed fix(battery): reliably apply configured power profiles at shell startup
Port 1.5
2026-08-02 19:01:39 -04:00
purian23 32ddf614c3 refactor(media OSD): add previous/next UI controls
Port 1.5
2026-08-02 18:10:12 -04:00
purian23 19d919ed5c refactor(mpris): consolidate equivalent-player resolution & update track matching
Port 1.5
2026-08-02 18:09:18 -04:00
Rubén García 594a2cde19 refactor(mpris): enrich metadata across equivalent players (#2928)
* fix(mpris): enrich metadata across equivalent players

* fix(mpris): address review feedback on source stealing and title stripping

- _bestPlayingPlayer: when the active player is playing but not
  controllable, only hand ownership to a controllable peer that passes
  isSameTrack; unrelated players can no longer steal the active source.
- displayTrackTitle: keep the full trackTitle for display. Suffix
  stripping now only applies to the isSameTrack matching key, and only
  for a known app-name allowlist (YouTube, SoundCloud, browsers, etc.)
  so legitimate titles containing ' | ' are never mangled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Port 1.5
2026-08-02 17:34:19 -04:00
Connor Welsh 11287459c3 popout: re-apply surface geometry after shouldBeVisible (#2983)
Popouts sized from their content sometimes open too short and clip what
they render, then come back correct on the next open.

open() snapshots the surface geometry before setting shouldBeVisible,
and the onAligned{X,Y,Width,Height}Changed handlers are all gated on
that flag. Mapping contentWindow happens in between, and that's what
finally runs the layout Qt deferred while the window was hidden.
Content-sized popouts pick up their real height in that gap, so the
correction is dropped and the surface stays short. Re-snapshot once the
flag is set.
2026-08-02 01:30:21 -04:00
bbedward ef191babb7 tailscale: add missing ID
related #2969
port 1.5
2026-07-31 09:37:01 -04:00
bbedward fe64a342f9 network: rescan when network panel is open 2026-07-31 09:33:17 -04:00
51 changed files with 1210 additions and 265 deletions
+22 -7
View File
@@ -313,6 +313,7 @@ func EnsureContrastDPSLstar(hexColor, hexBg string, minLc float64, isLightMode b
fg := HexToRGB(hexColor) fg := HexToRGB(hexColor)
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B} cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
Lf, af, bf := cf.Lab() Lf, af, bf := cf.Lab()
Lf *= 100.0
dir := 1.0 dir := 1.0
if isLightMode { if isLightMode {
@@ -341,6 +342,7 @@ func EnsureContrastDPSBidirectional(hexColor, hexBg string, minLc float64, isLig
fg := HexToRGB(hexColor) fg := HexToRGB(hexColor)
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B} cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
origL, af, bf := cf.Lab() origL, af, bf := cf.Lab()
origL *= 100.0
var darkerResult, lighterResult string var darkerResult, lighterResult string
darkerL, lighterL := origL, origL darkerL, lighterL := origL, origL
@@ -420,6 +422,24 @@ func blendHue(base, target, factor float64) float64 {
return result return result
} }
// color8 sits a fixed L* offset from the background so it keeps its dim role
// regardless of primary brightness (conventional palettes put ANSI bright
// black ~2-2.5:1 from the background, e.g. catppuccin-mocha #585b70 on #1e1e2e)
func DeriveDim(bgHex string, hue, sat float64, isLight bool) string {
offset := 22.0
if isLight {
offset = -offset
}
bgL := getLstar(bgHex)
targetL := math.Max(0, math.Min(100, bgL+offset))
tint := HSVToRGB(HSV{H: hue, S: sat, V: 0.5})
c := colorful.Color{R: tint.R, G: tint.G, B: tint.B}
_, af, bf := c.Lab()
return labToHex(targetL, af, bf)
}
func DeriveContainer(primary string, isLight bool) string { func DeriveContainer(primary string, isLight bool) string {
rgb := HexToRGB(primary) rgb := HexToRGB(primary)
hsv := RGBToHSV(rgb) hsv := RGBToHSV(rgb)
@@ -500,10 +520,7 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
gray7V := baseVal * 0.28 gray7V := baseVal * 0.28
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts)) palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
gray8S := baseSat * 0.05 palette.Color8 = NewColorInfo(DeriveDim(bgColor, hsv.H, baseSat*0.05, true))
gray8V := baseVal * 0.85
dimTarget := secondaryTarget * 0.5
palette.Color8 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray8S, V: gray8V})), bgColor, dimTarget, opts))
brightRedS := math.Min(baseSat*1.0, 1.0) brightRedS := math.Min(baseSat*1.0, 1.0)
brightRedV := math.Min(baseVal*1.2, 1.0) brightRedV := math.Min(baseVal*1.2, 1.0)
@@ -559,9 +576,7 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
gray7V := math.Min(baseVal*1.05, 1.0) gray7V := math.Min(baseVal*1.05, 1.0)
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts)) palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
gray8S := baseSat * 0.15 palette.Color8 = NewColorInfo(DeriveDim(bgColor, hsv.H, baseSat*0.15, false))
gray8V := baseVal * 0.65
palette.Color8 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray8S, V: gray8V})), bgColor, secondaryTarget, opts))
brightRedS := math.Min(baseSat*0.75, 1.0) brightRedS := math.Min(baseSat*0.75, 1.0)
brightRedV := math.Min(baseVal*1.35, 1.0) brightRedV := math.Min(baseVal*1.35, 1.0)
+70
View File
@@ -679,3 +679,73 @@ func TestContrastAlgorithmComparison(t *testing.T) {
t.Logf("WCAG and DPS palettes differ in %d/16 colors", differentCount) t.Logf("WCAG and DPS palettes differ in %d/16 colors", differentCount)
} }
func TestEnsureContrastDPSLightModeStaysLight(t *testing.T) {
tests := []struct {
name string
result string
bg string
minLstar float64
}{
{
name: "bidirectional adjustment",
result: EnsureContrastDPSBidirectional("#d0ccc6", "#f8f8f8", 17.5, true),
bg: "#f8f8f8",
minLstar: 20.0,
},
{
name: "lstar adjustment",
result: EnsureContrastDPSLstar("#c0c0c0", "#f8f8f8", 30.0, true),
bg: "#f8f8f8",
minLstar: 20.0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
lstar := getLstar(tt.result)
if lstar < tt.minLstar {
t.Errorf("result %s has L* %.2f on light bg %s, expected >= %.2f (collapsed to near-black)",
tt.result, lstar, tt.bg, tt.minLstar)
}
})
}
}
func TestGeneratePaletteColor8Dim(t *testing.T) {
hues := []string{"#e91e63", "#f59e0b", "#22c55e", "#06b6d4", "#8b5cf6", "#ef4444"}
for _, base := range hues {
t.Run(base, func(t *testing.T) {
palette := GeneratePalette(base, PaletteOptions{IsLight: false, UseDPS: true})
bgRatio := ContrastRatio(palette.Color8.Hex, palette.Color0.Hex)
if bgRatio < 1.5 || bgRatio > 3.0 {
t.Errorf("Color8 %s vs bg %s ratio %.2f, expected 1.5-3.0 (bright black stays near bg)",
palette.Color8.Hex, palette.Color0.Hex, bgRatio)
}
sepRatio := ContrastRatio(palette.Color4.Hex, palette.Color8.Hex)
if sepRatio < 2.0 {
t.Errorf("Color4 %s vs Color8 %s ratio %.2f, expected >= 2.0 (blue must not collide with bright black)",
palette.Color4.Hex, palette.Color8.Hex, sepRatio)
}
})
}
}
func TestGeneratePaletteLightColor8StaysLight(t *testing.T) {
palette := GeneratePalette("#f59e0b", PaletteOptions{IsLight: true, UseDPS: true})
lstar := getLstar(palette.Color8.Hex)
if lstar < 60.0 {
t.Errorf("light mode Color8 %s has L* %.2f, expected >= 60 (dim grey, not near-black)",
palette.Color8.Hex, lstar)
}
bgRatio := ContrastRatio(palette.Color8.Hex, palette.Color0.Hex)
if bgRatio > 3.0 {
t.Errorf("light mode Color8 %s vs bg %s ratio %.2f, expected <= 3.0",
palette.Color8.Hex, palette.Color0.Hex, bgRatio)
}
}
+9 -7
View File
@@ -53,6 +53,11 @@ func NewGentooDistribution(config DistroConfig, logChan chan<- string) *GentooDi
} }
} }
func emergeInstallArgs(packages []string) []string {
args := []string{"emerge", "--ask=n", "--quiet", "--autounmask-continue=y", "--autounmask-keep-keywords=y", "--autounmask-keep-masks=y"}
return append(args, packages...)
}
func (g *GentooDistribution) getArchKeyword() string { func (g *GentooDistribution) getArchKeyword() string {
arch := runtime.GOARCH arch := runtime.GOARCH
switch arch { switch arch {
@@ -319,6 +324,7 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
} }
g.log("Portage tree synced successfully") g.log("Portage tree synced successfully")
args := emergeInstallArgs(missingPkgs)
g.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", "))) g.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")))
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites, Phase: PhasePrerequisites,
@@ -326,12 +332,10 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)), Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)),
IsComplete: false, IsComplete: false,
NeedsSudo: true, NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo emerge --ask=n %s", strings.Join(missingPkgs, " ")), CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
LogOutput: fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")), LogOutput: fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")),
} }
args := []string{"emerge", "--ask=n", "--quiet"}
args = append(args, missingPkgs...)
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " ")) cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
@@ -521,8 +525,7 @@ func (g *GentooDistribution) installPortagePackages(ctx context.Context, package
} }
} }
args := []string{"emerge", "--ask=n", "--quiet"} args := emergeInstallArgs(packageNames)
args = append(args, packageNames...)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages, Phase: PhaseSystemPackages,
@@ -713,8 +716,7 @@ func (g *GentooDistribution) installGURUPackages(ctx context.Context, packages [
guruPackages[i] = pkg + "::guru" guruPackages[i] = pkg + "::guru"
} }
args := []string{"emerge", "--ask=n", "--quiet"} args := emergeInstallArgs(guruPackages)
args = append(args, guruPackages...)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages, Phase: PhaseAURPackages,
+25 -21
View File
@@ -48,7 +48,7 @@ func NewManager() (*Manager, error) {
return nil, fmt.Errorf("failed to find keyboards: %w", err) return nil, fmt.Errorf("failed to find keyboards: %w", err)
} }
initialCapsLock := readInitialCapsLockState(devices[0]) initialCapsLock, _ := capsLockFromDevices(devices)
watcher, err := fsnotify.NewWatcher() watcher, err := fsnotify.NewWatcher()
if err != nil { if err != nil {
@@ -85,14 +85,21 @@ func NewManager() (*Manager, error) {
return m, nil return m, nil
} }
func readInitialCapsLockState(device EvdevDevice) bool { func capsLockFromDevices(devices []EvdevDevice) (bool, bool) {
ledStates, err := device.State(evLedType) for _, device := range devices {
if err != nil { if device == nil {
log.Debugf("Could not read LED state: %v", err) continue
return false }
ledStates, err := device.State(evLedType)
if err != nil || len(ledStates) == 0 {
continue
}
return ledStates[ledCapslockKey], true
} }
return ledStates[ledCapslockKey] return false, false
} }
func findKeyboards() ([]EvdevDevice, error) { func findKeyboards() ([]EvdevDevice, error) {
@@ -297,25 +304,22 @@ func (m *Manager) readAndUpdateCapsLockState(deviceIndex int) {
m.devicesMutex.RUnlock() m.devicesMutex.RUnlock()
return return
} }
device := m.devices[deviceIndex] ordered := make([]EvdevDevice, 0, len(m.devices))
ordered = append(ordered, m.devices[deviceIndex])
for i, device := range m.devices {
if i == deviceIndex {
continue
}
ordered = append(ordered, device)
}
m.devicesMutex.RUnlock() m.devicesMutex.RUnlock()
ledStates, err := device.State(evLedType) capsLockState, ok := capsLockFromDevices(ordered)
if err != nil { if !ok {
log.Warnf("Failed to read LED state: %v", err) log.Debug("No LED-capable device available for caps lock state")
return return
} }
if len(ledStates) == 0 {
log.Debug("No LED state available (empty map)")
// This means the device either:
// - doesn't support LED reporting at all, or
// - the kernel returned an empty state
return
}
capsLockState := ledStates[ledCapslockKey]
m.updateCapsLockStateDirect(capsLockState) m.updateCapsLockStateDirect(capsLockState)
} }
+22 -4
View File
@@ -306,7 +306,7 @@ func TestNotifySubscribers(t *testing.T) {
m.Close() m.Close()
} }
func TestReadInitialCapsLockState(t *testing.T) { func TestCapsLockFromDevices(t *testing.T) {
t.Run("caps lock is on", func(t *testing.T) { t.Run("caps lock is on", func(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t) mockDevice := mocks.NewMockEvdevDevice(t)
ledState := evdev.StateMap{ ledState := evdev.StateMap{
@@ -314,7 +314,8 @@ func TestReadInitialCapsLockState(t *testing.T) {
} }
mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once() mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once()
result := readInitialCapsLockState(mockDevice) result, ok := capsLockFromDevices([]EvdevDevice{mockDevice})
assert.True(t, ok)
assert.True(t, result) assert.True(t, result)
}) })
@@ -325,7 +326,8 @@ func TestReadInitialCapsLockState(t *testing.T) {
} }
mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once() mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once()
result := readInitialCapsLockState(mockDevice) result, ok := capsLockFromDevices([]EvdevDevice{mockDevice})
assert.True(t, ok)
assert.False(t, result) assert.False(t, result)
}) })
@@ -333,9 +335,25 @@ func TestReadInitialCapsLockState(t *testing.T) {
mockDevice := mocks.NewMockEvdevDevice(t) mockDevice := mocks.NewMockEvdevDevice(t)
mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(nil, errors.New("read error")).Once() mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(nil, errors.New("read error")).Once()
result := readInitialCapsLockState(mockDevice) result, ok := capsLockFromDevices([]EvdevDevice{mockDevice})
assert.False(t, ok)
assert.False(t, result) assert.False(t, result)
}) })
t.Run("falls back past device without LED state", func(t *testing.T) {
noLedDevice := mocks.NewMockEvdevDevice(t)
noLedDevice.EXPECT().State(evdev.EvType(evLedType)).Return(evdev.StateMap{}, nil).Once()
ledDevice := mocks.NewMockEvdevDevice(t)
ledState := evdev.StateMap{
ledCapslockKey: true,
}
ledDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once()
result, ok := capsLockFromDevices([]EvdevDevice{noLedDevice, nil, ledDevice})
assert.True(t, ok)
assert.True(t, result)
})
} }
func TestHasInputGroupAccess(t *testing.T) { func TestHasInputGroupAccess(t *testing.T) {
+31 -20
View File
@@ -759,6 +759,9 @@ func (m *Manager) schedulerLoop() {
now := time.Now() now := time.Now()
m.recalcSchedule(now) m.recalcSchedule(now)
// publish independent of output readiness so night status never
// presents a stale schedule while applies are blocked (#2967)
m.updateStateFromSchedule()
waitDur := 24 * time.Hour waitDur := 24 * time.Hour
if enabled { if enabled {
@@ -1104,13 +1107,15 @@ func (m *Manager) SetTemperature(low, high int) error {
m.configMutex.Unlock() m.configMutex.Unlock()
return nil return nil
} }
m.config.LowTemp = low updated := m.config
m.config.HighTemp = high updated.LowTemp = low
err := m.config.Validate() updated.HighTemp = high
m.configMutex.Unlock() if err := updated.Validate(); err != nil {
if err != nil { m.configMutex.Unlock()
return err return err
} }
m.config = updated
m.configMutex.Unlock()
m.triggerUpdate() m.triggerUpdate()
return nil return nil
} }
@@ -1122,14 +1127,16 @@ func (m *Manager) SetLocation(lat, lon float64) error {
m.configMutex.Unlock() m.configMutex.Unlock()
return nil return nil
} }
m.config.Latitude = &lat updated := m.config
m.config.Longitude = &lon updated.Latitude = &lat
m.config.UseIPLocation = false updated.Longitude = &lon
err := m.config.Validate() updated.UseIPLocation = false
m.configMutex.Unlock() if err := updated.Validate(); err != nil {
if err != nil { m.configMutex.Unlock()
return err return err
} }
m.config = updated
m.configMutex.Unlock()
m.triggerUpdate() m.triggerUpdate()
return nil return nil
} }
@@ -1164,13 +1171,15 @@ func (m *Manager) SetManualTimes(sunrise, sunset time.Time) error {
m.configMutex.Unlock() m.configMutex.Unlock()
return nil return nil
} }
m.config.ManualSunrise = &sunrise updated := m.config
m.config.ManualSunset = &sunset updated.ManualSunrise = &sunrise
err := m.config.Validate() updated.ManualSunset = &sunset
m.configMutex.Unlock() if err := updated.Validate(); err != nil {
if err != nil { m.configMutex.Unlock()
return err return err
} }
m.config = updated
m.configMutex.Unlock()
m.triggerUpdate() m.triggerUpdate()
return nil return nil
} }
@@ -1193,12 +1202,14 @@ func (m *Manager) SetGamma(gamma float64) error {
m.configMutex.Unlock() m.configMutex.Unlock()
return nil return nil
} }
m.config.Gamma = gamma updated := m.config
err := m.config.Validate() updated.Gamma = gamma
m.configMutex.Unlock() if err := updated.Validate(); err != nil {
if err != nil { m.configMutex.Unlock()
return err return err
} }
m.config = updated
m.configMutex.Unlock()
m.triggerUpdate() m.triggerUpdate()
return nil return nil
} }
@@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient" mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_gamma_control"
) )
func TestManager_ActorSerializesOutputStateAccess(t *testing.T) { func TestManager_ActorSerializesOutputStateAccess(t *testing.T) {
@@ -412,3 +413,75 @@ func TestNewManager_InvalidConfig(t *testing.T) {
_, err := NewManager(mockDisplay, config) _, err := NewManager(mockDisplay, config)
assert.Error(t, err) assert.Error(t, err)
} }
func TestSetters_RejectedValuesLeaveConfigUntouched(t *testing.T) {
newManager := func() *Manager {
return &Manager{
config: DefaultConfig(),
updateTrigger: make(chan struct{}, 1),
}
}
t.Run("SetTemperature", func(t *testing.T) {
m := newManager()
before := m.config
err := m.SetTemperature(3200, 2500)
assert.Error(t, err)
assert.Equal(t, before, m.config)
assert.Empty(t, m.updateTrigger)
})
t.Run("SetLocation", func(t *testing.T) {
m := newManager()
before := m.config
err := m.SetLocation(120.0, 10.0)
assert.Error(t, err)
assert.Equal(t, before, m.config)
assert.Empty(t, m.updateTrigger)
})
t.Run("SetGamma", func(t *testing.T) {
m := newManager()
before := m.config
err := m.SetGamma(-1.0)
assert.Error(t, err)
assert.Equal(t, before, m.config)
assert.Empty(t, m.updateTrigger)
})
}
func TestSetters_ValidValuesCommitAndTrigger(t *testing.T) {
m := &Manager{
config: DefaultConfig(),
updateTrigger: make(chan struct{}, 1),
}
err := m.SetTemperature(3000, 6000)
assert.NoError(t, err)
assert.Equal(t, 3000, m.config.LowTemp)
assert.Equal(t, 6000, m.config.HighTemp)
assert.Len(t, m.updateTrigger, 1)
}
func TestApplyGamma_SkipsUnchangedTempAndGamma(t *testing.T) {
m := &Manager{config: DefaultConfig()}
m.controlsInitialized = true
out := &outputState{
id: 1,
rampSize: 256,
gammaControl: &wlr_gamma_control.ZwlrGammaControlV1{},
lastTemp: 5000,
lastGamma: m.config.Gamma,
}
m.outputs.Store(out.id, out)
m.applyGamma(5000)
assert.False(t, out.failed, "unchanged temp must not reach the compositor write path")
assert.Equal(t, 5000, out.lastTemp)
assert.Equal(t, uint32(256), out.rampSize)
}
+58
View File
@@ -271,3 +271,61 @@ function getConflictingBinds(keyCombo, currentAction, allBinds, modKey) {
} }
return conflicts; return conflicts;
} }
function qtKeyFromName(name) {
var n = (name || "").toUpperCase();
if (n.length === 1 && n >= "A" && n <= "Z")
return Qt.Key_A + (n.charCodeAt(0) - 65);
if (n.length === 1 && n >= "0" && n <= "9")
return Qt.Key_0 + (n.charCodeAt(0) - 48);
if (n.length >= 2 && n[0] === "F") {
var f = parseInt(n.slice(1), 10);
if (f >= 1 && f <= 12)
return Qt.Key_F1 + (f - 1);
}
var named = {
"SPACE": Qt.Key_Space,
"TAB": Qt.Key_Tab,
"RETURN": Qt.Key_Return,
"ENTER": Qt.Key_Enter,
"BACKSPACE": Qt.Key_Backspace,
"DELETE": Qt.Key_Delete,
"HOME": Qt.Key_Home,
"END": Qt.Key_End,
"UP": Qt.Key_Up,
"DOWN": Qt.Key_Down,
"LEFT": Qt.Key_Left,
"RIGHT": Qt.Key_Right
};
return named[n] || 0;
}
function isModifierKey(qk) {
return qk === Qt.Key_Control || qk === Qt.Key_Shift || qk === Qt.Key_Alt || qk === Qt.Key_Meta
|| qk === Qt.Key_NumLock || qk === Qt.Key_CapsLock || qk === Qt.Key_ScrollLock;
}
function eventMatchesCombo(event, combo) {
if (!combo)
return false;
var parts = combo.split("+");
var keyName = parts[parts.length - 1].trim().toUpperCase();
var wantsShift = false;
var hasCtrl = false;
for (var i = 0; i < parts.length - 1; i++) {
var mod = parts[i].trim().toLowerCase();
if (mod === "shift")
wantsShift = true;
else if (mod === "ctrl" || mod === "control")
hasCtrl = true;
else
return false;
}
if (hasCtrl && !(event.modifiers & Qt.ControlModifier))
return false;
if (event.modifiers & (Qt.AltModifier | Qt.MetaModifier))
return false;
if (((event.modifiers & Qt.ShiftModifier) !== 0) !== wantsShift)
return false;
return event.key === qtKeyFromName(keyName);
}
+38
View File
@@ -148,6 +148,7 @@ Singleton {
property var includedTransitions: availableWallpaperTransitions.filter(t => t !== "none") property var includedTransitions: availableWallpaperTransitions.filter(t => t !== "none")
property bool wallpaperCyclingEnabled: false property bool wallpaperCyclingEnabled: false
property bool wallpaperCyclingRandom: false
property string wallpaperCyclingMode: "interval" property string wallpaperCyclingMode: "interval"
property int wallpaperCyclingInterval: 300 property int wallpaperCyclingInterval: 300
property string wallpaperCyclingTime: "06:00" property string wallpaperCyclingTime: "06:00"
@@ -646,6 +647,11 @@ Singleton {
saveSettings(); saveSettings();
} }
function setWallpaperCyclingRandom(random) {
wallpaperCyclingRandom = random;
saveSettings();
}
function setWallpaperCyclingMode(mode) { function setWallpaperCyclingMode(mode) {
wallpaperCyclingMode = mode; wallpaperCyclingMode = mode;
saveSettings(); saveSettings();
@@ -692,6 +698,37 @@ Singleton {
saveSettings(); saveSettings();
} }
function setMonitorCyclingRandom(screenName, random) {
var screen = null;
var screens = Quickshell.screens;
for (var i = 0; i < screens.length; i++) {
if (screens[i].name === screenName) {
screen = screens[i];
break;
}
}
if (!screen) {
log.warn("Screen not found");
return;
}
var identifier = typeof SettingsData !== "undefined" ? SettingsData.getScreenDisplayName(screen) : screen.name;
var newSettings = {};
for (var key in monitorCyclingSettings) {
var isThisScreen = key === screen.name || (screen.model && key === screen.model);
if (!isThisScreen) {
newSettings[key] = monitorCyclingSettings[key];
}
}
newSettings[identifier] = getMonitorCyclingSettings(screenName);
newSettings[identifier].random = random;
monitorCyclingSettings = newSettings;
saveSettings();
}
function setMonitorCyclingMode(screenName, mode) { function setMonitorCyclingMode(screenName, mode) {
var screen = null; var screen = null;
var screens = Quickshell.screens; var screens = Quickshell.screens;
@@ -1322,6 +1359,7 @@ Singleton {
function getMonitorCyclingSettings(screenName) { function getMonitorCyclingSettings(screenName) {
var defaults = { var defaults = {
"enabled": false, "enabled": false,
"random": false,
"mode": "interval", "mode": "interval",
"interval": 300, "interval": 300,
"time": "06:00" "time": "06:00"
+2 -6
View File
@@ -659,9 +659,6 @@ Singleton {
readonly property string iconTheme: resolveIconTheme() 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 qt6ctAvailable: false
property bool gtkAvailable: false
property var cursorSettings: ({ property var cursorSettings: ({
"theme": "System Default", "theme": "System Default",
@@ -798,8 +795,6 @@ Singleton {
property int fadeToDpmsGracePeriod: 5 property int fadeToDpmsGracePeriod: 5
property string launchPrefix: "" property string launchPrefix: ""
property bool gtkThemingEnabled: false
property bool qtThemingEnabled: false
property bool syncModeWithPortal: true property bool syncModeWithPortal: true
property bool terminalsAlwaysDark: false property bool terminalsAlwaysDark: false
@@ -922,6 +917,8 @@ Singleton {
property bool lockPamInlineU2f: false property bool lockPamInlineU2f: false
property bool lockPamExternallyManaged: false property bool lockPamExternallyManaged: false
property string lockU2fPamPath: "" property string lockU2fPamPath: ""
property string lockScreenSecurityKeyShortcut: "Ctrl+Q"
property bool lockScreenSecurityKeyShortcutEnabled: false
property bool greeterPamExternallyManaged: false property bool greeterPamExternallyManaged: false
property string lockScreenInactiveColor: "#000000" property string lockScreenInactiveColor: "#000000"
property int lockScreenNotificationMode: 0 property int lockScreenNotificationMode: 0
@@ -1683,7 +1680,6 @@ Singleton {
_hasLoaded = true; _hasLoaded = true;
applyStoredTheme(); applyStoredTheme();
updateCompositorCursor(); updateCompositorCursor();
Processes.detectQtTools();
Qt.callLater(checkIconThemeDrift); Qt.callLater(checkIconThemeDrift);
_checkSettingsWritable(); _checkSettingsWritable();
+2 -2
View File
@@ -96,8 +96,6 @@ Singleton {
} }
property bool matugenAvailable: false property bool matugenAvailable: false
property bool gtkThemingEnabled: typeof SettingsData !== "undefined" ? SettingsData.gtkAvailable : false
property bool qtThemingEnabled: typeof SettingsData !== "undefined" ? (SettingsData.qt5ctAvailable || SettingsData.qt6ctAvailable) : false
property var workerRunning: false property var workerRunning: false
property var pendingThemeRequest: null property var pendingThemeRequest: null
@@ -352,6 +350,8 @@ Singleton {
readonly property color readableSurfaceHigh: withAlpha(surfaceContainerHigh, popupTransparency) readonly property color readableSurfaceHigh: withAlpha(surfaceContainerHigh, popupTransparency)
readonly property color floatingSurface: foregroundLayers ? readableSurface : withAlpha(readableSurface, 0) readonly property color floatingSurface: foregroundLayers ? readableSurface : withAlpha(readableSurface, 0)
readonly property color floatingSurfaceHigh: foregroundLayers ? readableSurfaceHigh : withAlpha(readableSurfaceHigh, 0) readonly property color floatingSurfaceHigh: foregroundLayers ? readableSurfaceHigh : withAlpha(readableSurfaceHigh, 0)
readonly property color floatingWindowSurface: readableSurface
readonly property color notepadWindowSurface: withAlpha(surfaceContainer, notepadTransparency)
readonly property color nestedSurface: floatingSurfaceHigh readonly property color nestedSurface: floatingSurfaceHigh
readonly property color notificationFloatingSurface: notificationForegroundLayers ? readableSurface : withAlpha(readableSurface, 0) readonly property color notificationFloatingSurface: notificationForegroundLayers ? readableSurface : withAlpha(readableSurface, 0)
readonly property color notificationFloatingSurfaceHigh: notificationForegroundLayers ? readableSurfaceHigh : withAlpha(readableSurfaceHigh, 0) readonly property color notificationFloatingSurfaceHigh: notificationForegroundLayers ? readableSurfaceHigh : withAlpha(readableSurfaceHigh, 0)
+61 -10
View File
@@ -1,31 +1,69 @@
pragma Singleton pragma Singleton
import Quickshell import Quickshell
import Quickshell.Services.SystemTray
import QtQuick import QtQuick
Singleton { Singleton {
id: root id: root
property var activeTrayMenus: ({}) property var activeTrayMenus: ({})
property var _pendingMenuRequest: null
signal openTrayMenuRequested
function requestOpenMenu(itemId, screenName) {
_pendingMenuRequest = {
"itemId": itemId,
"screenName": screenName
};
openTrayMenuRequested();
}
// Every SystemTrayBar instance receives the signal; the claim ensures
// exactly one opens the menu, preferring the requested screen
function claimMenuRequest(instanceScreenName) {
if (!_pendingMenuRequest)
return null;
if (_pendingMenuRequest.screenName && _pendingMenuRequest.screenName !== instanceScreenName)
return null;
const request = _pendingMenuRequest;
_pendingMenuRequest = null;
return request;
}
function findTrayItem(itemId: string): var {
if (!itemId)
return null;
return SystemTray.items.values.find(item => {
const id = item?.id || "";
const title = item?.tooltipTitle || "";
const fullKey = title ? `${id}::${title}` : id;
return fullKey === itemId || id === itemId;
});
}
function registerMenu(screenName, menu) { function registerMenu(screenName, menu) {
if (!screenName || !menu) return if (!screenName || !menu)
const newMenus = Object.assign({}, activeTrayMenus) return;
newMenus[screenName] = menu const newMenus = Object.assign({}, activeTrayMenus);
activeTrayMenus = newMenus newMenus[screenName] = menu;
activeTrayMenus = newMenus;
} }
function unregisterMenu(screenName) { function unregisterMenu(screenName) {
if (!screenName) return if (!screenName)
const newMenus = Object.assign({}, activeTrayMenus) return;
delete newMenus[screenName] const newMenus = Object.assign({}, activeTrayMenus);
activeTrayMenus = newMenus delete newMenus[screenName];
activeTrayMenus = newMenus;
} }
function closeAllMenus() { function closeHoverMenus() {
for (const screenName in activeTrayMenus) { for (const screenName in activeTrayMenus) {
const menu = activeTrayMenus[screenName] const menu = activeTrayMenus[screenName]
if (!menu) continue if (!menu || menu.openedByHover !== true) continue
if (typeof menu.close === "function") { if (typeof menu.close === "function") {
menu.close() menu.close()
} else if (menu.showMenu !== undefined) { } else if (menu.showMenu !== undefined) {
@@ -33,4 +71,17 @@ Singleton {
} }
} }
} }
function closeAllMenus() {
for (const screenName in activeTrayMenus) {
const menu = activeTrayMenus[screenName];
if (!menu)
continue;
if (typeof menu.close === "function") {
menu.close();
} else if (menu.showMenu !== undefined) {
menu.showMenu = false;
}
}
}
} }
-31
View File
@@ -487,41 +487,10 @@ Singleton {
return pamFprintDetected ? "probe_failed" : "missing_pam_support"; return pamFprintDetected ? "probe_failed" : "missing_pam_support";
} }
// --- Qt tools detection ---
function detectQtTools() {
qtToolsDetectionProcess.running = true;
}
function checkPluginSettings() { function checkPluginSettings() {
pluginSettingsCheckProcess.running = true; pluginSettingsCheckProcess.running = true;
} }
property var qtToolsDetectionProcess: Process {
command: ["sh", "-c", "echo -n 'qt5ct:'; command -v qt5ct >/dev/null && echo 'true' || echo 'false'; echo -n 'qt6ct:'; command -v qt6ct >/dev/null && echo 'true' || echo 'false'; echo -n 'gtk:'; (command -v gsettings >/dev/null || command -v dconf >/dev/null) && echo 'true' || echo 'false'"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (!settingsRoot)
return;
if (text && text.trim()) {
const lines = text.trim().split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.startsWith("qt5ct:")) {
settingsRoot.qt5ctAvailable = line.split(":")[1] === "true";
} else if (line.startsWith("qt6ct:")) {
settingsRoot.qt6ctAvailable = line.split(":")[1] === "true";
} else if (line.startsWith("gtk:")) {
settingsRoot.gtkAvailable = line.split(":")[1] === "true";
}
}
}
}
}
}
Timer { Timer {
id: authApplyDebounce id: authApplyDebounce
interval: 300 interval: 300
@@ -19,6 +19,7 @@ var SPEC = {
includedTransitions: { def: ["fade", "wipe", "disc", "stripes", "iris bloom", "pixelate", "portal"] }, includedTransitions: { def: ["fade", "wipe", "disc", "stripes", "iris bloom", "pixelate", "portal"] },
wallpaperCyclingEnabled: { def: false }, wallpaperCyclingEnabled: { def: false },
wallpaperCyclingRandom: { def: false },
wallpaperCyclingMode: { def: "interval" }, wallpaperCyclingMode: { def: "interval" },
wallpaperCyclingInterval: { def: 300 }, wallpaperCyclingInterval: { def: 300 },
wallpaperCyclingTime: { def: "06:00" }, wallpaperCyclingTime: { def: "06:00" },
+2 -5
View File
@@ -298,9 +298,6 @@ var SPEC = {
lastAppliedIconTheme: { def: "" }, 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 },
qt6ctAvailable: { def: false, persist: false },
gtkAvailable: { def: false, persist: false },
cursorSettings: { def: { theme: "System Default", size: 24, niri: { hideWhenTyping: false, hideAfterInactiveMs: 0 }, hyprland: { hideOnKeyPress: false, hideOnTouch: false, inactiveTimeout: 0 }, dwl: { cursorHideTimeout: 0 }, mango: { cursorHideTimeout: 0 } }, onChange: "updateCompositorCursor" }, cursorSettings: { def: { theme: "System Default", size: 24, niri: { hideWhenTyping: false, hideAfterInactiveMs: 0 }, hyprland: { hideOnKeyPress: false, hideOnTouch: false, inactiveTimeout: 0 }, dwl: { cursorHideTimeout: 0 }, mango: { cursorHideTimeout: 0 } }, onChange: "updateCompositorCursor" },
availableCursorThemes: { def: ["System Default"], persist: false }, availableCursorThemes: { def: ["System Default"], persist: false },
@@ -374,8 +371,6 @@ var SPEC = {
fadeToDpmsGracePeriod: { def: 5 }, fadeToDpmsGracePeriod: { def: 5 },
launchPrefix: { def: "" }, launchPrefix: { def: "" },
gtkThemingEnabled: { def: false, onChange: "regenSystemThemes" },
qtThemingEnabled: { def: false, onChange: "regenSystemThemes" },
syncModeWithPortal: { def: true }, syncModeWithPortal: { def: true },
terminalsAlwaysDark: { def: false, onChange: "regenSystemThemes" }, terminalsAlwaysDark: { def: false, onChange: "regenSystemThemes" },
@@ -473,6 +468,8 @@ var SPEC = {
enableU2f: { def: false, onChange: "scheduleAuthApply" }, enableU2f: { def: false, onChange: "scheduleAuthApply" },
u2fMode: { def: "or" }, u2fMode: { def: "or" },
lockPamPath: { def: "" }, lockPamPath: { def: "" },
lockScreenSecurityKeyShortcut: { def: "Ctrl+Q" },
lockScreenSecurityKeyShortcutEnabled: { def: false },
lockPamInlineFprint: { def: false }, lockPamInlineFprint: { def: false },
lockPamInlineU2f: { def: false }, lockPamInlineU2f: { def: false },
lockPamExternallyManaged: { def: false }, lockPamExternallyManaged: { def: false },
+3
View File
@@ -1015,6 +1015,9 @@ Item {
case "reboot": case "reboot":
SessionService.reboot(); SessionService.reboot();
break; break;
case "softreboot":
SessionService.softReboot();
break;
case "poweroff": case "poweroff":
SessionService.poweroff(); SessionService.poweroff();
break; break;
+14 -14
View File
@@ -2023,18 +2023,6 @@ Item {
} }
IpcHandler { IpcHandler {
function findTrayItem(itemId: string): var {
if (!itemId)
return null;
return SystemTray.items.values.find(item => {
const id = item?.id || "";
const title = item?.tooltipTitle || "";
const fullKey = title ? `${id}::${title}` : id;
return fullKey === itemId || id === itemId;
});
}
function list(): string { function list(): string {
const items = SystemTray.items.values; const items = SystemTray.items.values;
if (items.length === 0) if (items.length === 0)
@@ -2050,7 +2038,7 @@ Item {
} }
function activate(itemId: string): string { function activate(itemId: string): string {
const item = findTrayItem(itemId); const item = TrayMenuManager.findTrayItem(itemId);
if (!item) if (!item)
return `ERROR: Tray item not found: ${itemId}`; return `ERROR: Tray item not found: ${itemId}`;
@@ -2058,8 +2046,20 @@ Item {
return `SUCCESS: Activated ${itemId}`; return `SUCCESS: Activated ${itemId}`;
} }
function menu(itemId: string): string {
const item = TrayMenuManager.findTrayItem(itemId);
if (!item)
return `ERROR: Tray item not found: ${itemId}`;
if (!item.hasMenu)
return `ERROR: Tray item has no menu: ${itemId}`;
TrayMenuManager.requestOpenMenu(itemId, BarWidgetService.getFocusedScreenName());
return `SUCCESS: Requested menu ${itemId}`;
}
function status(itemId: string): string { function status(itemId: string): string {
const item = findTrayItem(itemId); const item = TrayMenuManager.findTrayItem(itemId);
if (!item) if (!item)
return `ERROR: Tray item not found: ${itemId}`; return `ERROR: Tray item not found: ${itemId}`;
+19 -11
View File
@@ -819,16 +819,24 @@ Item {
} }
if (isCategoryFiltered) { if (isCategoryFiltered) {
var rawApps = AppSearchService.getAppsInCategory(appCategory); var categoryPluginId = AppSearchService.getPluginIdForCategory(appCategory);
for (var i = 0; i < rawApps.length; i++) { if (categoryPluginId) {
allItems.push(getOrTransformApp(rawApps[i])); var pluginCategoryItems = getPluginItems(categoryPluginId, "");
} for (var i = 0; i < pluginCategoryItems.length; i++) {
// Also include core apps (DMS Settings etc.) that match this category allItems.push(pluginCategoryItems[i]);
var allCoreApps = AppSearchService.getCoreApps(""); }
for (var i = 0; i < allCoreApps.length; i++) { } else {
var coreAppCats = AppSearchService.getCategoriesForApp(allCoreApps[i]); var rawApps = AppSearchService.getAppsInCategory(appCategory);
if (coreAppCats.indexOf(appCategory) !== -1) for (var i = 0; i < rawApps.length; i++) {
allItems.push(transformCoreApp(allCoreApps[i])); allItems.push(getOrTransformApp(rawApps[i]));
}
// Also include core apps (DMS Settings etc.) that match this category
var allCoreApps = AppSearchService.getCoreApps("");
for (var i = 0; i < allCoreApps.length; i++) {
var coreAppCats = AppSearchService.getCategoriesForApp(allCoreApps[i]);
if (coreAppCats.indexOf(appCategory) !== -1)
allItems.push(transformCoreApp(allCoreApps[i]));
}
} }
} else { } else {
var apps = searchApps(searchQuery); var apps = searchApps(searchQuery);
@@ -839,7 +847,7 @@ Item {
var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem); var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);
var sortAlpha = !searchQuery && SettingsData.sortAppsAlphabetically; var sortAlpha = !searchQuery && SettingsData.sortAppsAlphabetically;
var newSections = Scorer.groupBySection(scoredItems, sectionDefinitions, sortAlpha, searchQuery ? 50 : 500); var newSections = Scorer.groupBySection(scoredItems, buildDynamicSectionDefs(allItems), sortAlpha, searchQuery ? 50 : 500);
for (var sid in collapsedSections) { for (var sid in collapsedSections) {
for (var i = 0; i < newSections.length; i++) { for (var i = 0; i < newSections.length; i++) {
+17 -5
View File
@@ -13,6 +13,17 @@ FocusScope {
property bool awaitingFprintForPassword: false property bool awaitingFprintForPassword: false
property var windowControls: null property var windowControls: null
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2 readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
readonly property bool _blurActive: Theme.blurForegroundLayers || Theme.transparentBlurLayers
readonly property real _fieldAlpha: {
if (Theme.transparentBlurLayers)
return 0.28;
if (Theme.blurForegroundLayers)
return Math.max(Theme.popupTransparency, 0.62);
return Theme.popupTransparency;
}
readonly property color _fieldColor: Theme.withAlpha(Theme.surfaceContainerHigh, _fieldAlpha)
readonly property color _fieldBorderColor: Theme.withAlpha(Theme.outline, _blurActive ? 0.16 : Theme.layerOutlineOpacity)
readonly property color _fieldFocusedBorderColor: Theme.withAlpha(Theme.primary, _blurActive ? 0.72 : 1.0)
property string polkitEtcPamText: "" property string polkitEtcPamText: ""
property string polkitLibPamText: "" property string polkitLibPamText: ""
@@ -202,7 +213,7 @@ FocusScope {
StyledText { StyledText {
text: root.currentFlow?.message ?? "" text: root.currentFlow?.message ?? ""
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium color: Theme.surfaceText
width: parent.width width: parent.width
wrapMode: Text.Wrap wrapMode: Text.Wrap
maximumLineCount: 2 maximumLineCount: 2
@@ -272,9 +283,10 @@ FocusScope {
width: parent.width width: parent.width
height: root.inputFieldHeight height: root.inputFieldHeight
backgroundColor: Theme.surfaceHover cornerRadius: Theme.cornerRadius
normalBorderColor: Theme.outlineStrong backgroundColor: root._fieldColor
focusedBorderColor: Theme.primary normalBorderColor: root._fieldBorderColor
focusedBorderColor: root._fieldFocusedBorderColor
borderWidth: 1 borderWidth: 1
focusedBorderWidth: 2 focusedBorderWidth: 2
leftIconName: root.polkitPamHasFprint ? "fingerprint" : "" leftIconName: root.polkitPamHasFprint ? "fingerprint" : ""
@@ -352,7 +364,7 @@ FocusScope {
anchors.centerIn: parent anchors.centerIn: parent
text: I18n.tr("Authenticate") text: I18n.tr("Authenticate")
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.background color: Theme.primaryText
font.weight: Font.Medium font.weight: Font.Medium
} }
+20 -1
View File
@@ -29,7 +29,7 @@ FloatingWindow {
title: I18n.tr("Authentication") title: I18n.tr("Authentication")
minimumSize: Qt.size(460, 220) minimumSize: Qt.size(460, 220)
maximumSize: Qt.size(460, 220) maximumSize: Qt.size(460, 220)
color: Theme.surfaceContainer color: Theme.floatingWindowSurface
visible: false visible: false
onClosed: hide() onClosed: hide()
@@ -53,6 +53,25 @@ FloatingWindow {
} }
} }
WindowBlur {
targetWindow: root
blurX: 0
blurY: 0
blurWidth: root.visible ? root.width : 0
blurHeight: root.visible ? root.height : 0
blurRadius: Theme.cornerRadius
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: "transparent"
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
antialiasing: true
z: 100
}
Loader { Loader {
id: contentLoader id: contentLoader
anchors.fill: parent anchors.fill: parent
+26 -6
View File
@@ -175,6 +175,8 @@ DankModal {
visibleActions = allActions.filter(action => { visibleActions = allActions.filter(action => {
if (action === "hibernate" && !SessionService.hibernateSupported) if (action === "hibernate" && !SessionService.hibernateSupported)
return false; return false;
if (action === "softreboot" && !SessionService.softRebootSupported)
return false;
return true; return true;
}); });
@@ -223,6 +225,12 @@ DankModal {
"label": I18n.tr("Reboot"), "label": I18n.tr("Reboot"),
"key": "R" "key": "R"
}; };
case "softreboot":
return {
"icon": "autorenew",
"label": I18n.tr("Soft Reboot"),
"key": "B"
};
case "logout": case "logout":
return { return {
"icon": "logout", "icon": "logout",
@@ -370,7 +378,7 @@ DankModal {
function handleListNavigation(event, isPressed) { function handleListNavigation(event, isPressed) {
if (!isPressed) { if (!isPressed) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_X || event.key === Qt.Key_L || event.key === Qt.Key_S || event.key === Qt.Key_H || event.key === Qt.Key_D || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) { if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_B || event.key === Qt.Key_X || event.key === Qt.Key_L || event.key === Qt.Key_S || event.key === Qt.Key_H || event.key === Qt.Key_D || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) {
cancelHold(); cancelHold();
event.accepted = true; event.accepted = true;
} }
@@ -429,6 +437,12 @@ DankModal {
event.accepted = true; event.accepted = true;
} }
break; break;
case Qt.Key_B:
if (visibleActions.includes("softreboot")) {
startHold("softreboot", visibleActions.indexOf("softreboot"));
event.accepted = true;
}
break;
case Qt.Key_X: case Qt.Key_X:
if (visibleActions.includes("logout")) { if (visibleActions.includes("logout")) {
startHold("logout", visibleActions.indexOf("logout")); startHold("logout", visibleActions.indexOf("logout"));
@@ -464,7 +478,7 @@ DankModal {
function handleGridNavigation(event, isPressed) { function handleGridNavigation(event, isPressed) {
if (!isPressed) { if (!isPressed) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_X || event.key === Qt.Key_L || event.key === Qt.Key_S || event.key === Qt.Key_H || event.key === Qt.Key_D || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) { if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_B || event.key === Qt.Key_X || event.key === Qt.Key_L || event.key === Qt.Key_S || event.key === Qt.Key_H || event.key === Qt.Key_D || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) {
cancelHold(); cancelHold();
event.accepted = true; event.accepted = true;
} }
@@ -539,6 +553,12 @@ DankModal {
event.accepted = true; event.accepted = true;
} }
break; break;
case Qt.Key_B:
if (visibleActions.includes("softreboot")) {
startHold("softreboot", visibleActions.indexOf("softreboot"));
event.accepted = true;
}
break;
case Qt.Key_X: case Qt.Key_X:
if (visibleActions.includes("logout")) { if (visibleActions.includes("logout")) {
startHold("logout", visibleActions.indexOf("logout")); startHold("logout", visibleActions.indexOf("logout"));
@@ -597,7 +617,7 @@ DankModal {
readonly property var actionData: root.getActionData(modelData) readonly property var actionData: root.getActionData(modelData)
readonly property bool isSelected: root.selectedIndex === index readonly property bool isSelected: root.selectedIndex === index
readonly property bool showWarning: modelData === "reboot" || modelData === "poweroff" readonly property bool showWarning: modelData === "reboot" || modelData === "softreboot" || modelData === "poweroff"
readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0 readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0
width: (root.modalWidth - Theme.spacingL * 2 - Theme.spacingS * (root.gridColumns - 1)) / root.gridColumns width: (root.modalWidth - Theme.spacingL * 2 - Theme.spacingS * (root.gridColumns - 1)) / root.gridColumns
@@ -627,7 +647,7 @@ DankModal {
color: { color: {
if (gridButtonRect.modelData === "poweroff") if (gridButtonRect.modelData === "poweroff")
return Theme.errorSelected; return Theme.errorSelected;
if (gridButtonRect.modelData === "reboot") if (gridButtonRect.modelData === "reboot" || gridButtonRect.modelData === "softreboot")
return Theme.withAlpha(Theme.warning, 0.3); return Theme.withAlpha(Theme.warning, 0.3);
return Theme.primarySelected; return Theme.primarySelected;
} }
@@ -722,7 +742,7 @@ DankModal {
readonly property var actionData: root.getActionData(modelData) readonly property var actionData: root.getActionData(modelData)
readonly property bool isSelected: root.selectedIndex === index readonly property bool isSelected: root.selectedIndex === index
readonly property bool showWarning: modelData === "reboot" || modelData === "poweroff" readonly property bool showWarning: modelData === "reboot" || modelData === "softreboot" || modelData === "poweroff"
readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0 readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0
width: parent.width width: parent.width
@@ -752,7 +772,7 @@ DankModal {
color: { color: {
if (listButtonRect.modelData === "poweroff") if (listButtonRect.modelData === "poweroff")
return Theme.errorSelected; return Theme.errorSelected;
if (listButtonRect.modelData === "reboot") if (listButtonRect.modelData === "reboot" || listButtonRect.modelData === "softreboot")
return Theme.withAlpha(Theme.warning, 0.3); return Theme.withAlpha(Theme.warning, 0.3);
return Theme.primarySelected; return Theme.primarySelected;
} }
+21 -9
View File
@@ -8,6 +8,7 @@ import qs.Widgets
FloatingWindow { FloatingWindow {
id: settingsModal id: settingsModal
property bool disablePopupTransparency: true
property var profileBrowser: profileBrowserLoader.item property var profileBrowser: profileBrowserLoader.item
property var wallpaperBrowser: wallpaperBrowserLoader.item property var wallpaperBrowser: wallpaperBrowserLoader.item
@@ -95,7 +96,7 @@ FloatingWindow {
minimumSize: Qt.size(500, 400) minimumSize: Qt.size(500, 400)
implicitWidth: 900 implicitWidth: 900
implicitHeight: screen ? Math.min(940, screen.height - 100) : 940 implicitHeight: screen ? Math.min(940, screen.height - 100) : 940
color: Theme.surfaceContainer color: Theme.floatingWindowSurface
visible: false visible: false
onClosed: hide() onClosed: hide()
@@ -120,6 +121,25 @@ FloatingWindow {
} }
} }
WindowBlur {
targetWindow: settingsModal
blurX: 0
blurY: 0
blurWidth: settingsModal.visible ? settingsModal.width : 0
blurHeight: settingsModal.visible ? settingsModal.height : 0
blurRadius: Theme.cornerRadius
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: "transparent"
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
antialiasing: true
z: 100
}
Loader { Loader {
active: settingsModal.visible active: settingsModal.visible
sourceComponent: Component { sourceComponent: Component {
@@ -180,8 +200,6 @@ FloatingWindow {
FocusScope { FocusScope {
id: contentFocusScope id: contentFocusScope
property bool disablePopupTransparency: true
LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true LayoutMirroring.childrenInherit: true
@@ -203,12 +221,6 @@ FloatingWindow {
onDoubleClicked: windowControls.tryToggleMaximize() onDoubleClicked: windowControls.tryToggleMaximize()
} }
Rectangle {
anchors.fill: parent
color: Theme.surfaceContainer
opacity: 0.5
}
Row { Row {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Theme.spacingL anchors.leftMargin: Theme.spacingL
@@ -641,7 +641,7 @@ Rectangle {
implicitWidth: __calculatedWidth implicitWidth: __calculatedWidth
width: __calculatedWidth width: __calculatedWidth
height: parent.height height: parent.height
color: Theme.surfaceContainer color: "transparent"
radius: Theme.cornerRadius radius: Theme.cornerRadius
Component.onCompleted: { Component.onCompleted: {
@@ -469,6 +469,8 @@ PluginComponent {
} }
MouseArea { MouseArea {
id: peerMouseArea
z: -1 z: -1
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
@@ -41,7 +41,11 @@ Rectangle {
property bool hasWifiAvailable: (NetworkService.wifiDevices?.length ?? 0) > 0 property bool hasWifiAvailable: (NetworkService.wifiDevices?.length ?? 0) > 0
property bool hasBothConnectionTypes: hasEthernetAvailable && hasWifiAvailable property bool hasBothConnectionTypes: hasEthernetAvailable && hasWifiAvailable
property int maxPinnedNetworks: 3 property int maxPinnedNetworks: 3
readonly property int hotspotContentHeight: currentPreferenceIndex === 1 && NetworkService.hotspotAvailable ? 56 + Theme.spacingS : 0 // Hosting on the only wifi adapter with no ethernet uplink just drops connectivity,
// so the hotspot row only shows where sharing can actually work (or is already on).
readonly property bool hotspotRelevant: NetworkService.hotspotEnabled || NetworkService.hotspotActivating || NetworkService.hotspotBusy || NetworkService.ethernetConnected || (NetworkService.wifiDevices?.length ?? 0) > 1
readonly property bool showHotspotRow: currentPreferenceIndex === 1 && NetworkService.hotspotAvailable && hotspotRelevant
readonly property int hotspotContentHeight: showHotspotRow ? 56 + Theme.spacingS : 0
property var hotspotStartConfirm: ConfirmModal {} property var hotspotStartConfirm: ConfirmModal {}
@@ -184,7 +188,7 @@ Rectangle {
anchors.right: parent.right anchors.right: parent.right
anchors.margins: Theme.spacingM anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM anchors.topMargin: Theme.spacingM
visible: currentPreferenceIndex === 1 && NetworkService.hotspotAvailable visible: root.showHotspotRow
height: visible ? 56 : 0 height: visible ? 56 : 0
Rectangle { Rectangle {
+10 -1
View File
@@ -201,7 +201,16 @@ Item {
direction: root.effectiveShadowDirection direction: root.effectiveShadowDirection
fallbackOffset: 4 fallbackOffset: 4
targetRadius: root.rt targetRadius: root.rt
targetColor: barWindow._bgColor // wing-side body corners are square where the gothic fillets attach;
// rounding the shadow there leaves a shadow-filled notch under the
// translucent bar fill (#2975)
topLeftRadius: root.gothEnabled && (root.isBottom || root.isRight) ? 0 : root.rt
topRightRadius: root.gothEnabled && (root.isBottom || root.isLeft) ? 0 : root.rt
bottomLeftRadius: root.gothEnabled && (root.isTop || root.isRight) ? 0 : root.rt
bottomRightRadius: root.gothEnabled && (root.isTop || root.isLeft) ? 0 : root.rt
// barShape below is the sole painter of the bar; a fill here doubles
// the alpha of translucent bars while the wings stay single-painted
targetColor: "transparent"
shadowBlurPx: root.shadowBlurPx shadowBlurPx: root.shadowBlurPx
shadowOffsetX: root.shadowOffsetX shadowOffsetX: root.shadowOffsetX
@@ -609,7 +609,7 @@ Item {
_closeHoverNotepad(); _closeHoverNotepad();
activeHoverTrigger = ""; activeHoverTrigger = "";
PopoutManager.dismissHoverPopoutForScreen(barWindow?.screen); PopoutManager.dismissHoverPopoutForScreen(barWindow?.screen);
TrayMenuManager.closeAllMenus(); TrayMenuManager.closeHoverMenus();
} }
function _beginSupersededCloseForActive() { function _beginSupersededCloseForActive() {
@@ -83,6 +83,22 @@ BasePill {
root.showForTrayItem(trayItem, anchorItem, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis); root.showForTrayItem(trayItem, anchorItem, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
} }
Connections {
target: TrayMenuManager
function onOpenTrayMenuRequested() {
const request = TrayMenuManager.claimMenuRequest(root.parentScreen?.name);
if (!request)
return;
const item = TrayMenuManager.findTrayItem(request.itemId);
if (!item || !item.hasMenu)
return;
root.showForTrayItem(item, root, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
}
}
function openInlineTrayContextMenu(trayItem, areaItem, mouse, anchorItem) { function openInlineTrayContextMenu(trayItem, areaItem, mouse, anchorItem) {
if (!trayItem) { if (!trayItem) {
return; return;
@@ -1484,6 +1500,7 @@ BasePill {
property bool isVertical: false property bool isVertical: false
property var axis: null property var axis: null
property bool showMenu: false property bool showMenu: false
property bool openedByHover: false
property var menuHandle: null property var menuHandle: null
ListModel { ListModel {
@@ -1493,7 +1510,8 @@ BasePill {
return entryStack.count ? entryStack.get(entryStack.count - 1).handle : null; return entryStack.count ? entryStack.get(entryStack.count - 1).handle : null;
} }
function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj) { function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj, byHover) {
openedByHover = byHover === true;
trayItem = item; trayItem = item;
anchorItem = anchor; anchorItem = anchor;
parentScreen = screen; parentScreen = screen;
@@ -2084,7 +2102,7 @@ BasePill {
} }
} }
function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj) { function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj, byHover) {
if (!screen) if (!screen)
return; return;
if (currentTrayMenu) { if (currentTrayMenu) {
@@ -2099,7 +2117,7 @@ BasePill {
currentTrayMenu = trayMenuComponent.createObject(null); currentTrayMenu = trayMenuComponent.createObject(null);
if (!currentTrayMenu) if (!currentTrayMenu)
return; return;
currentTrayMenu.showForTrayItem(item, anchor, screen, atBottom, vertical ?? false, axisObj); currentTrayMenu.showForTrayItem(item, anchor, screen, atBottom, vertical ?? false, axisObj, byHover === true);
} }
function _trayLayoutRoot() { function _trayLayoutRoot() {
@@ -2147,7 +2165,7 @@ BasePill {
if (!hit?.trayItem?.hasMenu) if (!hit?.trayItem?.hasMenu)
return false; return false;
const anchor = hit.children?.length > 0 ? hit.children[0] : hit; const anchor = hit.children?.length > 0 ? hit.children[0] : hit;
showForTrayItem(hit.trayItem, anchor, parentScreen, isAtBottom, isVerticalOrientation, axis); showForTrayItem(hit.trayItem, anchor, parentScreen, isAtBottom, isVerticalOrientation, axis, true);
return true; return true;
} }
} }
@@ -493,7 +493,7 @@ Item {
} }
StyledText { StyledText {
text: activePlayer?.trackAlbum || "" text: MprisController.stableAlbum
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextSecondary color: Theme.surfaceTextSecondary
width: parent.width width: parent.width
+22 -4
View File
@@ -14,6 +14,7 @@ import qs.Services
import qs.Widgets import qs.Widgets
import qs.DankCommon.Session import qs.DankCommon.Session
import "../../DankCommon/Common/LayoutCodes.js" as LayoutCodes import "../../DankCommon/Common/LayoutCodes.js" as LayoutCodes
import "../../Common/KeyUtils.js" as KeyUtils
Item { Item {
id: root id: root
@@ -98,6 +99,18 @@ Item {
return !demoMode && pam && pam.u2f && pam.u2f.available && SettingsData.enableU2f && SettingsData.u2fMode === "or" && !pam.passwd.active && !pam.u2f.active && !pam.u2fPending && !root.unlocking; return !demoMode && pam && pam.u2f && pam.u2f.available && SettingsData.enableU2f && SettingsData.u2fMode === "or" && !pam.passwd.active && !pam.u2f.active && !pam.u2fPending && !root.unlocking;
} }
function triggerSecurityKeyUnlock() {
if (!canStartSecurityKeyUnlock())
return;
passwordField.clear();
pam.u2f.startForAlternativeAuth();
}
function securityKeyShortcutMatches(event) {
return SettingsData.lockScreenSecurityKeyShortcutEnabled
&& KeyUtils.eventMatchesCombo(event, SettingsData.lockScreenSecurityKeyShortcut);
}
Component.onCompleted: { Component.onCompleted: {
WeatherService.addRef(); WeatherService.addRef();
UserInfoService.getUserInfo(); UserInfoService.getUserInfo();
@@ -962,6 +975,12 @@ Item {
} }
if ((event.modifiers & Qt.ControlModifier) && !(event.modifiers & (Qt.AltModifier | Qt.MetaModifier))) { if ((event.modifiers & Qt.ControlModifier) && !(event.modifiers & (Qt.AltModifier | Qt.MetaModifier))) {
if (securityKeyShortcutMatches(event) && canStartSecurityKeyUnlock()) {
triggerSecurityKeyUnlock();
event.accepted = true;
return;
}
switch (event.key) { switch (event.key) {
case Qt.Key_A: case Qt.Key_A:
cursorPosition = 0; cursorPosition = 0;
@@ -1238,10 +1257,8 @@ Item {
buttonSize: 32 buttonSize: 32
visible: root.canStartSecurityKeyUnlock() visible: root.canStartSecurityKeyUnlock()
enabled: visible enabled: visible
onClicked: { tooltipText: SettingsData.lockScreenSecurityKeyShortcutEnabled ? I18n.tr("Security key (%1)", "lock screen security key button tooltip with shortcut").arg(SettingsData.lockScreenSecurityKeyShortcut) : I18n.tr("Security key", "lock screen security key button tooltip")
passwordField.clear(); onClicked: root.triggerSecurityKeyUnlock()
pam.u2f.startForAlternativeAuth();
}
} }
DankActionButton { DankActionButton {
id: virtualKeyboardButton id: virtualKeyboardButton
@@ -1329,6 +1346,7 @@ Item {
width: parent.width width: parent.width
height: parent.height / 2 height: parent.height / 2
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: -1
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
color: Theme.withAlpha(Theme.surfaceContainer, 0.9) color: Theme.withAlpha(Theme.surfaceContainer, 0.9)
} }
@@ -8,6 +8,7 @@ import qs.Modules.Notepad
FloatingWindow { FloatingWindow {
id: win id: win
property bool disablePopupTransparency: true
property alias shouldBeVisible: win.visible property alias shouldBeVisible: win.visible
property alias notepad: notepad property alias notepad: notepad
@@ -27,7 +28,7 @@ FloatingWindow {
minimumSize: Qt.size(360, 320) minimumSize: Qt.size(360, 320)
implicitWidth: 640 implicitWidth: 640
implicitHeight: 760 implicitHeight: 760
color: Theme.surfaceContainer color: Theme.notepadWindowSurface
visible: false visible: false
onVisibleChanged: { onVisibleChanged: {
@@ -38,9 +39,27 @@ FloatingWindow {
} }
} }
// A compositor close (e.g. niri close-window)
onClosed: win.visible = false onClosed: win.visible = false
WindowBlur {
targetWindow: win
blurX: 0
blurY: 0
blurWidth: win.visible ? win.width : 0
blurHeight: win.visible ? win.height : 0
blurRadius: Theme.cornerRadius
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: "transparent"
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
antialiasing: true
z: 100
}
Item { Item {
anchors.fill: parent anchors.fill: parent
@@ -58,12 +77,6 @@ FloatingWindow {
onDoubleClicked: windowControls.tryToggleMaximize() onDoubleClicked: windowControls.tryToggleMaximize()
} }
Rectangle {
anchors.fill: parent
color: Theme.surfaceContainerHigh
opacity: 0.5
}
Row { Row {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Theme.spacingM anchors.leftMargin: Theme.spacingM
+89 -23
View File
@@ -156,9 +156,10 @@ DankOSD {
if (MprisController.isFirefoxYoutubeHoverPreview(player)) if (MprisController.isFirefoxYoutubeHoverPreview(player))
return; return;
const newTitle = player.trackTitle || ""; const metaPlayer = MprisController.bestMetadataPlayer(player);
const newArtist = player.trackArtist || ""; const newTitle = MprisController.displayTrackTitle(metaPlayer);
const newAlbum = player.trackAlbum || ""; const newArtist = metaPlayer.trackArtist || "";
const newAlbum = metaPlayer.trackAlbum || "";
const trackChanged = newTitle !== root._displayTitle || newArtist !== root._displayArtist || newAlbum !== root._displayAlbum; const trackChanged = newTitle !== root._displayTitle || newArtist !== root._displayArtist || newAlbum !== root._displayAlbum;
root._displayTitle = newTitle; root._displayTitle = newTitle;
@@ -263,37 +264,102 @@ DankOSD {
} }
} }
Rectangle { Row {
width: Theme.iconSize id: transportControls
height: Theme.iconSize
radius: Theme.iconSize / 2
color: "transparent"
x: parent.gap x: parent.gap
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXXS
DankIcon { Rectangle {
anchors.centerIn: parent width: Theme.iconSize - 4
name: root._displayIcon height: Theme.iconSize - 4
size: Theme.iconSize radius: (Theme.iconSize - 4) / 2
color: playPauseButton.containsMouse ? Theme.primary : Theme.surfaceText anchors.verticalCenter: parent.verticalCenter
color: prevButton.containsMouse ? Theme.surfaceTextHover : "transparent"
opacity: (root.player?.canGoPrevious ?? false) ? 1 : 0.3
DankIcon {
anchors.centerIn: parent
name: "skip_previous"
size: Theme.iconSize - 10
color: prevButton.containsMouse ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: prevButton
anchors.fill: parent
hoverEnabled: true
enabled: root.player?.canGoPrevious ?? false
cursorShape: Qt.PointingHandCursor
onClicked: {
MprisController.previousOrRewind();
root.hide();
}
}
} }
MouseArea { Rectangle {
id: playPauseButton width: Theme.iconSize
height: Theme.iconSize
radius: Theme.iconSize / 2
anchors.verticalCenter: parent.verticalCenter
color: "transparent"
anchors.fill: parent DankIcon {
hoverEnabled: true anchors.centerIn: parent
cursorShape: Qt.PointingHandCursor name: root._displayIcon
onClicked: { size: Theme.iconSize
togglePlaying(); color: playPauseButton.containsMouse ? Theme.primary : Theme.surfaceText
root.hide(); }
MouseArea {
id: playPauseButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
togglePlaying();
root.hide();
}
}
}
Rectangle {
width: Theme.iconSize - 4
height: Theme.iconSize - 4
radius: (Theme.iconSize - 4) / 2
anchors.verticalCenter: parent.verticalCenter
color: nextButton.containsMouse ? Theme.surfaceTextHover : "transparent"
opacity: (root.player?.canGoNext ?? false) ? 1 : 0.3
DankIcon {
anchors.centerIn: parent
name: "skip_next"
size: Theme.iconSize - 10
color: nextButton.containsMouse ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: nextButton
anchors.fill: parent
hoverEnabled: true
enabled: root.player?.canGoNext ?? false
cursorShape: Qt.PointingHandCursor
onClicked: {
MprisController.next();
root.hide();
}
} }
} }
} }
Column { Column {
x: parent.gap * 2 + Theme.iconSize x: parent.gap * 2 + transportControls.width
width: parent.width - Theme.iconSize - parent.gap * 3 width: parent.width - transportControls.width - parent.gap * 3
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXXS spacing: Theme.spacingXXS
@@ -1111,15 +1111,50 @@ Singleton {
} }
} }
function extractNiriOutputBlocks(content) {
const blocks = [];
const headerRegex = /output\s+"([^"]+)"\s*\{/g;
let match;
while ((match = headerRegex.exec(content)) !== null) {
const start = headerRegex.lastIndex;
let depth = 1;
let i = start;
while (i < content.length && depth > 0) {
const ch = content[i];
if (ch === '{')
depth++;
else if (ch === '}')
depth--;
i++;
}
blocks.push({
"name": match[1],
"body": content.slice(start, i - 1)
});
headerRegex.lastIndex = i;
}
return blocks;
}
function stripNestedBlocks(body) {
let stripped = body;
let prev;
do {
prev = stripped;
stripped = stripped.replace(/[\w-]+\s*\{[^{}]*\}/g, "");
} while (stripped !== prev)
return stripped;
}
function parseNiriOutputs(content) { function parseNiriOutputs(content) {
const result = {}; const result = {};
const outputRegex = /output\s+"([^"]+)"\s*\{([^}]*)\}/g; for (const block of extractNiriOutputBlocks(content)) {
let match; const name = block.name;
while ((match = outputRegex.exec(content)) !== null) { const body = block.body;
const name = match[1];
const body = match[2];
const disabled = /^\s*off\s*$/m.test(body); // off marks the output disabled only at the top level of its block;
// nested sections like hot-corners { off } must not count (#2966)
const disabled = /^\s*off\s*$/m.test(stripNestedBlocks(body));
const modeMatch = body.match(/mode\s+"(\d+)x(\d+)@([\d.]+)"/); const modeMatch = body.match(/mode\s+"(\d+)x(\d+)@([\d.]+)"/);
const posMatch = body.match(/position\s+x=(-?\d+)\s+y=(-?\d+)/); const posMatch = body.match(/position\s+x=(-?\d+)\s+y=(-?\d+)/);
const scaleMatch = body.match(/scale\s+([\d.]+)/); const scaleMatch = body.match(/scale\s+([\d.]+)/);
@@ -5,6 +5,7 @@ import qs.Modals.FileBrowser
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
import qs.Modules.Settings.Widgets import qs.Modules.Settings.Widgets
import "../../Common/KeyUtils.js" as KeyUtils
Item { Item {
id: root id: root
@@ -518,6 +519,117 @@ Item {
} }
} }
SettingsToggleRow {
settingKey: "lockScreenSecurityKeyShortcutEnabled"
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "shortcut", "keybind", "authentication"]
text: I18n.tr("Security key shortcut", "lock screen security key shortcut toggle")
description: I18n.tr("Keyboard shortcut to start security key unlock", "lock screen security key shortcut setting")
checked: SettingsData.lockScreenSecurityKeyShortcutEnabled
visible: SettingsData.enableU2f && SettingsData.u2fMode === "or" && !root.lockU2fControlledByPrimary
onToggled: checked => SettingsData.set("lockScreenSecurityKeyShortcutEnabled", checked)
}
Row {
width: parent.width - Theme.spacingM * 2
x: Theme.spacingM
spacing: Theme.spacingM
visible: SettingsData.lockScreenSecurityKeyShortcutEnabled && SettingsData.enableU2f && SettingsData.u2fMode === "or" && !root.lockU2fControlledByPrimary
Column {
width: parent.width - securityKeyCapture.width - parent.spacing
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Key combination", "lock screen security key shortcut key combination setting")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
horizontalAlignment: Text.AlignLeft
}
StyledText {
text: securityKeyCapture.captureError !== "" ? securityKeyCapture.captureError : I18n.tr("Press Ctrl+key to set. Esc cancels.", "lock screen security key shortcut key combination capture hint")
font.pixelSize: Theme.fontSizeSmall
color: securityKeyCapture.captureError !== "" ? Theme.warning : Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
horizontalAlignment: Text.AlignLeft
}
}
DankButton {
id: securityKeyCapture
width: 200
anchors.verticalCenter: parent.verticalCenter
focus: capturing
text: capturing ? I18n.tr("Press key...", "lock screen security key shortcut key combination capture prompt") : SettingsData.lockScreenSecurityKeyShortcut
backgroundColor: capturing ? Theme.primaryContainer : Theme.surfaceContainer
textColor: Theme.surfaceText
property bool capturing: false
property string captureError: ""
readonly property var reservedKeys: ["A", "E", "B", "F", "U", "K", "W", "H", "D"]
function startCapture() {
captureError = "";
capturing = true;
securityKeyCapture.forceActiveFocus();
}
function stopCapture() {
capturing = false;
}
onClicked: {
if (capturing)
stopCapture();
else
startCapture();
}
Keys.onPressed: event => {
if (!securityKeyCapture.capturing)
return;
if (KeyUtils.isModifierKey(event.key))
return;
if (event.key === Qt.Key_Escape) {
securityKeyCapture.stopCapture();
event.accepted = true;
return;
}
const mods = KeyUtils.modsFromEvent(event.modifiers);
const hasCtrl = mods.includes("Ctrl");
const hasAlt = mods.includes("Alt") || mods.includes("Super");
if (!hasCtrl || hasAlt)
return;
const hasShift = mods.includes("Shift");
const key = KeyUtils.xkbKeyFromQtKey(event.key, !!(event.modifiers & Qt.KeypadModifier), hasShift);
if (!key)
return;
if (!KeyUtils.qtKeyFromName(key))
return;
if (!hasShift && securityKeyCapture.reservedKeys.indexOf(key.toUpperCase()) !== -1) {
securityKeyCapture.captureError = I18n.tr("Ctrl+%1 is used for password editing", "lock screen security key shortcut reserved key warning").arg(key.toUpperCase());
event.accepted = true;
return;
}
SettingsData.set("lockScreenSecurityKeyShortcut", KeyUtils.formatToken(mods, key));
securityKeyCapture.captureError = "";
securityKeyCapture.stopCapture();
event.accepted = true;
}
}
}
SettingsDropdownRow { SettingsDropdownRow {
settingKey: "lockU2fPamPath" settingKey: "lockU2fPamPath"
tags: ["lock", "screen", "pam", "u2f", "security", "key", "source", "service"] tags: ["lock", "screen", "pam", "u2f", "security", "key", "source", "service"]
+15 -3
View File
@@ -410,8 +410,8 @@ Item {
settingKey: "powerMenuDefaultAction" settingKey: "powerMenuDefaultAction"
tags: ["power", "menu", "default", "action", "reboot", "logout", "shutdown"] tags: ["power", "menu", "default", "action", "reboot", "logout", "shutdown"]
text: I18n.tr("Default selected action") text: I18n.tr("Default selected action")
options: [I18n.tr("Reboot"), I18n.tr("Log Out"), I18n.tr("Power Off"), I18n.tr("Lock"), I18n.tr("Suspend"), I18n.tr("Restart DMS"), I18n.tr("Hibernate")] options: [I18n.tr("Reboot"), I18n.tr("Log Out"), I18n.tr("Power Off"), I18n.tr("Lock"), I18n.tr("Suspend"), I18n.tr("Restart DMS"), I18n.tr("Hibernate"), I18n.tr("Soft Reboot")]
property var actionValues: ["reboot", "logout", "poweroff", "lock", "suspend", "restart", "hibernate"] property var actionValues: ["reboot", "logout", "poweroff", "lock", "suspend", "restart", "hibernate", "softreboot"]
Component.onCompleted: { Component.onCompleted: {
const currentAction = SettingsData.powerMenuDefaultAction || "logout"; const currentAction = SettingsData.powerMenuDefaultAction || "logout";
@@ -475,6 +475,12 @@ Item {
label: I18n.tr("Show Hibernate"), label: I18n.tr("Show Hibernate"),
desc: I18n.tr("Only visible if hibernate is supported by your system"), desc: I18n.tr("Only visible if hibernate is supported by your system"),
hibernate: true hibernate: true
},
{
key: "softreboot",
label: I18n.tr("Show Soft Reboot"),
desc: I18n.tr("Restart userspace without rebooting the kernel, requires systemd"),
softreboot: true
} }
] ]
@@ -484,7 +490,13 @@ Item {
tags: ["power", "menu", "action", "show", modelData.key] tags: ["power", "menu", "action", "show", modelData.key]
text: modelData.label text: modelData.label
description: modelData.desc || "" description: modelData.desc || ""
visible: !modelData.hibernate || SessionService.hibernateSupported visible: {
if (modelData.hibernate)
return SessionService.hibernateSupported;
if (modelData.softreboot)
return SessionService.softRebootSupported;
return true;
}
checked: SettingsData.powerMenuActions.includes(modelData.key) checked: SettingsData.powerMenuActions.includes(modelData.key)
onToggled: checked => { onToggled: checked => {
let actions = [...SettingsData.powerMenuActions]; let actions = [...SettingsData.powerMenuActions];
@@ -1728,7 +1728,7 @@ Item {
tags: ["surface", "popup", "transparency", "opacity", "modal"] tags: ["surface", "popup", "transparency", "opacity", "modal"]
settingKey: "popupTransparency" settingKey: "popupTransparency"
text: I18n.tr("Surface Opacity") text: I18n.tr("Surface Opacity")
description: I18n.tr("Controls opacity of shell surfaces, popouts, and modals") description: I18n.tr("Controls opacity of shell surfaces, popouts, modals, and floating windows", "Surface Opacity setting description including floating DMS windows")
visible: !themeColorsTab.connectedFrameModeActive visible: !themeColorsTab.connectedFrameModeActive
value: Math.round(SettingsData.popupTransparency * 100) value: Math.round(SettingsData.popupTransparency * 100)
minimum: 0 minimum: 0
@@ -977,6 +977,33 @@ Item {
} }
} }
SettingsToggleRow {
id: randomToggle
tab: "wallpaper"
tags: ["cycling", "automatic", "random", "shuffle"]
settingKey: "wallpaperCyclingRandom"
width: parent.width - Theme.spacingM * 2
text: I18n.tr("Random Order")
description: I18n.tr("Select a random wallpaper instead of cycling in alphabetical order")
checked: SessionData.perMonitorWallpaper ? SessionData.getMonitorCyclingSettings(selectedMonitorName).random : SessionData.wallpaperCyclingRandom
onToggled: toggled => {
if (SessionData.perMonitorWallpaper) {
SessionData.setMonitorCyclingRandom(selectedMonitorName, toggled);
} else {
SessionData.setWallpaperCyclingRandom(toggled);
}
}
Connections {
target: root
function onSelectedMonitorNameChanged() {
randomToggle.checked = Qt.binding(() => {
return SessionData.perMonitorWallpaper ? SessionData.getMonitorCyclingSettings(selectedMonitorName).random : SessionData.wallpaperCyclingRandom;
});
}
}
}
SettingsDropdownRow { SettingsDropdownRow {
id: intervalDropdown id: intervalDropdown
property var intervalOptions: [I18n.tr("5 seconds", "wallpaper interval"), I18n.tr("10 seconds", "wallpaper interval"), I18n.tr("15 seconds", "wallpaper interval"), I18n.tr("20 seconds", "wallpaper interval"), I18n.tr("25 seconds", "wallpaper interval"), I18n.tr("30 seconds", "wallpaper interval"), I18n.tr("35 seconds", "wallpaper interval"), I18n.tr("40 seconds", "wallpaper interval"), I18n.tr("45 seconds", "wallpaper interval"), I18n.tr("50 seconds", "wallpaper interval"), I18n.tr("55 seconds", "wallpaper interval"), I18n.tr("1 minute", "wallpaper interval"), I18n.tr("5 minutes", "wallpaper interval"), I18n.tr("15 minutes", "wallpaper interval"), I18n.tr("30 minutes", "wallpaper interval"), I18n.tr("1 hour", "wallpaper interval"), I18n.tr("1 hour 30 minutes", "wallpaper interval"), I18n.tr("2 hours", "wallpaper interval"), I18n.tr("3 hours", "wallpaper interval"), I18n.tr("4 hours", "wallpaper interval"), I18n.tr("6 hours", "wallpaper interval"), I18n.tr("8 hours", "wallpaper interval"), I18n.tr("12 hours", "wallpaper interval")] property var intervalOptions: [I18n.tr("5 seconds", "wallpaper interval"), I18n.tr("10 seconds", "wallpaper interval"), I18n.tr("15 seconds", "wallpaper interval"), I18n.tr("20 seconds", "wallpaper interval"), I18n.tr("25 seconds", "wallpaper interval"), I18n.tr("30 seconds", "wallpaper interval"), I18n.tr("35 seconds", "wallpaper interval"), I18n.tr("40 seconds", "wallpaper interval"), I18n.tr("45 seconds", "wallpaper interval"), I18n.tr("50 seconds", "wallpaper interval"), I18n.tr("55 seconds", "wallpaper interval"), I18n.tr("1 minute", "wallpaper interval"), I18n.tr("5 minutes", "wallpaper interval"), I18n.tr("15 minutes", "wallpaper interval"), I18n.tr("30 minutes", "wallpaper interval"), I18n.tr("1 hour", "wallpaper interval"), I18n.tr("1 hour 30 minutes", "wallpaper interval"), I18n.tr("2 hours", "wallpaper interval"), I18n.tr("3 hours", "wallpaper interval"), I18n.tr("4 hours", "wallpaper interval"), I18n.tr("6 hours", "wallpaper interval"), I18n.tr("8 hours", "wallpaper interval"), I18n.tr("12 hours", "wallpaper interval")]
@@ -38,7 +38,9 @@ StyledRect {
return h; return h;
} }
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh color: Theme.nestedSurface
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
readonly property bool collapsed: collapsible && !expanded readonly property bool collapsed: collapsible && !expanded
readonly property bool hasHeader: root.title !== "" || root.iconName !== "" readonly property bool hasHeader: root.title !== "" || root.iconName !== ""
@@ -35,7 +35,9 @@ StyledRect {
width: parent?.width ?? 0 width: parent?.width ?? 0
height: Theme.spacingL * 2 + contentColumn.height height: Theme.spacingL * 2 + contentColumn.height
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh color: Theme.nestedSurface
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
Component.onCompleted: { Component.onCompleted: {
if (!settingKey) if (!settingKey)
@@ -29,7 +29,9 @@ StyledRect {
width: parent?.width ?? 0 width: parent?.width ?? 0
height: Theme.spacingL * 2 + mainColumn.height height: Theme.spacingL * 2 + mainColumn.height
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh color: Theme.nestedSurface
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
Component.onCompleted: { Component.onCompleted: {
if (!settingKey) if (!settingKey)
+18 -22
View File
@@ -785,16 +785,24 @@ Singleton {
if (category === I18n.tr("All")) if (category === I18n.tr("All"))
return visibleApps; return visibleApps;
const pluginItems = getPluginItems(category, "");
if (pluginItems.length > 0)
return pluginItems;
return visibleApps.filter(app => { return visibleApps.filter(app => {
const appCategories = getCategoriesForApp(app); const appCategories = getCategoriesForApp(app);
return appCategories.includes(category); return appCategories.includes(category);
}); });
} }
function getPluginIdForCategory(category) {
if (typeof PluginService === "undefined")
return null;
const launchers = PluginService.getLauncherPlugins();
for (const pluginId in launchers) {
if ((launchers[pluginId].name || pluginId) === category)
return pluginId;
}
return null;
}
// Plugin launcher support functions // Plugin launcher support functions
function getPluginCategories() { function getPluginCategories() {
if (typeof PluginService === "undefined") { if (typeof PluginService === "undefined") {
@@ -814,31 +822,19 @@ Singleton {
} }
function getPluginCategoryIcon(category) { function getPluginCategoryIcon(category) {
if (typeof PluginService === "undefined") const pluginId = getPluginIdForCategory(category);
if (!pluginId)
return null; return null;
const launchers = PluginService.getLauncherPlugins(); return PluginService.getLauncherPlugins()[pluginId].icon || "extension";
for (const pluginId in launchers) {
const plugin = launchers[pluginId];
if ((plugin.name || pluginId) === category) {
return plugin.icon || "extension";
}
}
return null;
} }
function getPluginItems(category, query) { function getPluginItems(category, query) {
if (typeof PluginService === "undefined") const pluginId = getPluginIdForCategory(category);
if (!pluginId)
return []; return [];
const launchers = PluginService.getLauncherPlugins(); return getPluginItemsForPlugin(pluginId, query);
for (const pluginId in launchers) {
const plugin = launchers[pluginId];
if ((plugin.name || pluginId) === category) {
return getPluginItemsForPlugin(pluginId, query);
}
}
return [];
} }
function getPluginItemsForPlugin(pluginId, query) { function getPluginItemsForPlugin(pluginId, query) {
+8 -1
View File
@@ -23,6 +23,13 @@ Singleton {
} }
} }
Connections {
target: typeof PowerProfiles !== "undefined" ? PowerProfiles : null
function onHasPerformanceProfileChanged() {
root.applyPowerProfile();
}
}
function applyPowerProfile() { function applyPowerProfile() {
if (!batteryAvailable) if (!batteryAvailable)
return; return;
@@ -32,7 +39,7 @@ Singleton {
const targetProfile = parseInt(profileValue); const targetProfile = parseInt(profileValue);
if (isNaN(targetProfile) || PowerProfiles.profile === targetProfile) if (isNaN(targetProfile) || PowerProfiles.profile === targetProfile)
return; return;
PowerProfiles.profile = targetProfile; PowerProfileWatcher.applyProfile(targetProfile);
} }
readonly property string preferredBatteryOverride: Quickshell.env("DMS_PREFERRED_BATTERY") readonly property string preferredBatteryOverride: Quickshell.env("DMS_PREFERRED_BATTERY")
@@ -793,6 +793,13 @@ Singleton {
autoRefreshEnabled = false; autoRefreshEnabled = false;
} }
Timer {
interval: 10000
repeat: true
running: root.autoScan && root.networkAvailable && root.wifiEnabled
onTriggered: root.scanWifi()
}
function fetchWiredNetworkInfo(uuid) { function fetchWiredNetworkInfo(uuid) {
if (!networkAvailable) if (!networkAvailable)
return; return;
+4
View File
@@ -1721,6 +1721,8 @@ Singleton {
return "Temperature must be between 2500K and 6000K"; return "Temperature must be between 2500K and 6000K";
const rounded = Math.round(temp / 500) * 500; const rounded = Math.round(temp / 500) * 500;
if (rounded > SessionData.nightModeHighTemperature)
return "Night temperature must not exceed the day temperature (" + SessionData.nightModeHighTemperature + "K)";
SessionData.setNightModeTemperature(rounded); SessionData.setNightModeTemperature(rounded);
if (root.nightModeEnabled) { if (root.nightModeEnabled) {
@@ -1750,6 +1752,8 @@ Singleton {
return "Temperature must be between 2500K and 6500K"; return "Temperature must be between 2500K and 6500K";
const rounded = Math.round(temp / 500) * 500; const rounded = Math.round(temp / 500) * 500;
if (rounded < SessionData.nightModeTemperature)
return "Day temperature must be at least the night temperature (" + SessionData.nightModeTemperature + "K)";
SessionData.setNightModeHighTemperature(rounded); SessionData.setNightModeHighTemperature(rounded);
if (root.nightModeEnabled && SessionData.nightModeAutoEnabled) if (root.nightModeEnabled && SessionData.nightModeAutoEnabled)
+139 -9
View File
@@ -47,6 +47,7 @@ Singleton {
// Chromium can report blank metadata between tracks // Chromium can report blank metadata between tracks
property string stableTitle: "" property string stableTitle: ""
property string stableArtist: "" property string stableArtist: ""
property string stableAlbum: ""
Connections { Connections {
target: root.activePlayer target: root.activePlayer
@@ -59,6 +60,9 @@ Singleton {
root._syncStableMeta(); root._syncStableMeta();
root._checkIdle(); root._checkIdle();
} }
function onTrackAlbumChanged() {
root._syncStableMeta();
}
function onLengthChanged() { function onLengthChanged() {
if (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) { if (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) {
root.activePlayerStableLength = root.activePlayer.length; root.activePlayerStableLength = root.activePlayer.length;
@@ -72,8 +76,10 @@ Singleton {
onActivePlayerChanged: { onActivePlayerChanged: {
activePlayerStableLength = (activePlayer && activePlayer.lengthSupported && activePlayer.length > 1) ? activePlayer.length : 0; activePlayerStableLength = (activePlayer && activePlayer.lengthSupported && activePlayer.length > 1) ? activePlayer.length : 0;
stableTitle = activePlayer?.trackTitle || ""; stableTitle = "";
stableArtist = activePlayer?.trackArtist || ""; stableArtist = "";
stableAlbum = "";
_syncStableMeta();
_checkIdle(); _checkIdle();
} }
@@ -82,14 +88,24 @@ Singleton {
if (!p) { if (!p) {
stableTitle = ""; stableTitle = "";
stableArtist = ""; stableArtist = "";
stableAlbum = "";
return; return;
} }
if (isFirefoxYoutubeHoverPreview(p)) if (isFirefoxYoutubeHoverPreview(p))
return; return;
if (p.trackTitle) const metadataPlayer = bestMetadataPlayer(p);
stableTitle = p.trackTitle; const nextTitle = displayTrackTitle(metadataPlayer);
if (p.trackArtist) const trackChanged = nextTitle && stableTitle && nextTitle.toLowerCase() !== stableTitle.toLowerCase();
stableArtist = p.trackArtist; if (trackChanged) {
stableArtist = "";
stableAlbum = "";
}
if (nextTitle)
stableTitle = nextTitle;
if (metadataPlayer.trackArtist)
stableArtist = metadataPlayer.trackArtist;
if (metadataPlayer.trackAlbum)
stableAlbum = metadataPlayer.trackAlbum;
} }
// Chromium reports stopped media w/blank metadata, resolve by checking idle status // Chromium reports stopped media w/blank metadata, resolve by checking idle status
@@ -101,6 +117,7 @@ Singleton {
return; return;
root.stableTitle = ""; root.stableTitle = "";
root.stableArtist = ""; root.stableArtist = "";
root.stableAlbum = "";
root._resolveActivePlayer(); root._resolveActivePlayer();
} }
} }
@@ -122,20 +139,133 @@ Singleton {
delegate: Connections { delegate: Connections {
required property MprisPlayer modelData required property MprisPlayer modelData
target: modelData target: modelData
ignoreUnknownSignals: true
function onIsPlayingChanged() { function onIsPlayingChanged() {
root._resolveActivePlayer();
root._syncStableMeta();
}
function onTrackTitleChanged() {
if (modelData.isPlaying) if (modelData.isPlaying)
root._resolveActivePlayer(); root._resolveActivePlayer();
root._syncStableMeta();
}
function onTrackArtistChanged() {
if (modelData.isPlaying)
root._resolveActivePlayer();
root._syncStableMeta();
}
function onTrackAlbumChanged() {
if (modelData.isPlaying)
root._resolveActivePlayer();
root._syncStableMeta();
}
function onMetadataChanged() {
if (modelData.isPlaying)
root._resolveActivePlayer();
root._syncStableMeta();
} }
} }
} }
function isIdle(player: MprisPlayer): bool { function isIdle(player: MprisPlayer): bool {
return player && player.playbackState === MprisPlaybackState.Stopped && !player.trackTitle && !player.trackArtist; return player && player.playbackState === MprisPlaybackState.Stopped;
}
// Known "<title> | <App>" suffixes stripped for matching only; display keeps the full title
readonly property var _appTitleSuffixes: ["youtube", "youtube music", "soundcloud", "spotify", "chrome", "chromium", "firefox", "brave", "vivaldi", "twitch"]
function _stripAppTitleSuffix(title: string): string {
const idx = title.lastIndexOf(" | ");
if (idx <= 0)
return title;
const suffix = title.substring(idx + 3).trim().toLowerCase();
return _appTitleSuffixes.indexOf(suffix) !== -1 ? title.substring(0, idx).trim() : title;
}
function normalizedTrackTitle(player: MprisPlayer): string {
return _stripAppTitleSuffix((player?.trackTitle || "").trim()).toLowerCase();
}
function displayTrackTitle(player: MprisPlayer): string {
return (player?.trackTitle || "").trim();
}
function normalizedTrackArtist(player: MprisPlayer): string {
return (player?.trackArtist || "").trim().toLowerCase();
}
// Artist missing on either side: fall back to URL, then album, before trusting a title-only match
function isSameTrack(first: MprisPlayer, second: MprisPlayer): bool {
const firstTitle = normalizedTrackTitle(first);
if (!firstTitle || firstTitle !== normalizedTrackTitle(second))
return false;
const firstArtist = normalizedTrackArtist(first);
const secondArtist = normalizedTrackArtist(second);
if (firstArtist && secondArtist)
return firstArtist === secondArtist;
const firstUrl = (first?.metadata?.["xesam:url"] || "").toString();
const secondUrl = (second?.metadata?.["xesam:url"] || "").toString();
if (firstUrl && secondUrl)
return firstUrl === secondUrl;
const firstAlbum = (first?.trackAlbum || "").trim().toLowerCase();
const secondAlbum = (second?.trackAlbum || "").trim().toLowerCase();
if (firstAlbum && secondAlbum)
return firstAlbum === secondAlbum;
return true;
}
function metadataQuality(player: MprisPlayer): int {
if (!player)
return -1;
let quality = player.trackArtist ? 100 : 0;
quality += player.trackTitle ? 40 : 0;
quality += player.trackAlbum ? 20 : 0;
quality += player.trackArtUrl || player.metadata?.["mpris:artUrl"] ? 10 : 0;
quality += player.metadata?.["xesam:url"] ? 5 : 0;
return quality;
}
function equivalentPlayers(player: MprisPlayer): var {
if (!player)
return [];
return availablePlayers.filter(candidate => {
return candidate.playbackState !== MprisPlaybackState.Stopped && isSameTrack(player, candidate);
});
}
function bestMetadataPlayer(player: MprisPlayer): MprisPlayer {
const equivalents = equivalentPlayers(player);
if (equivalents.length === 0)
return player;
return equivalents.reduce((best, candidate) => {
return metadataQuality(candidate) > metadataQuality(best) ? candidate : best;
}, player);
}
function _bestPlayingPlayer(): MprisPlayer {
const playing = availablePlayers.filter(player => player.isPlaying);
if (playing.length === 0)
return null;
const controllable = playing.filter(player => player.canControl);
if (activePlayer?.isPlaying) {
if (activePlayer.canControl || controllable.length === 0)
return activePlayer;
// Playing but not controllable: only a same-track controllable peer may take over
const mirror = controllable.find(player => isSameTrack(activePlayer, player));
return mirror || activePlayer;
}
if (activePlayer?.canControl && activePlayer.playbackState === MprisPlaybackState.Paused) {
const onlyEquivalentMirrors = playing.every(player => isSameTrack(activePlayer, player));
if (onlyEquivalentMirrors)
return null;
}
return controllable[0] || playing[0];
} }
function _resolveActivePlayer(): void { function _resolveActivePlayer(): void {
// A playing player always wins; otherwise keep the selection stable w/idle const playing = _bestPlayingPlayer();
const playing = availablePlayers.find(p => p.isPlaying);
if (playing) { if (playing) {
if (activePlayer !== playing) { if (activePlayer !== playing) {
activePlayer = playing; activePlayer = playing;
+5
View File
@@ -19,6 +19,7 @@ Singleton {
property bool loginctlCommandAvailable: false property bool loginctlCommandAvailable: false
property bool systemctlCommandAvailable: false property bool systemctlCommandAvailable: false
property bool hibernateSupported: false property bool hibernateSupported: false
readonly property bool softRebootSupported: systemctlCommandAvailable
property bool inhibitorAvailable: true property bool inhibitorAvailable: true
property bool idleInhibited: false property bool idleInhibited: false
property string inhibitReason: "Keep system awake" property string inhibitReason: "Keep system awake"
@@ -470,6 +471,10 @@ Singleton {
} }
} }
function softReboot() {
Quickshell.execDetached(["systemctl", "soft-reboot"]);
}
function poweroff() { function poweroff() {
if (SettingsData.customPowerActionPowerOff.length === 0) { if (SettingsData.customPowerActionPowerOff.length === 0) {
Quickshell.execDetached(powerManagerCommand("poweroff")); Quickshell.execDetached(powerManagerCommand("poweroff"));
+33 -8
View File
@@ -26,7 +26,7 @@ Singleton {
return hash.toString(16).padStart(8, '0'); return hash.toString(16).padStart(8, '0');
} }
function getArtworkUrl(player) { function _directArtworkUrl(player) {
if (!player) return ""; if (!player) return "";
let artUrl = player.trackArtUrl || ""; let artUrl = player.trackArtUrl || "";
@@ -54,6 +54,17 @@ Singleton {
return ""; return "";
} }
function getArtworkUrl(player) {
const directUrl = _directArtworkUrl(player);
if (directUrl !== "")
return directUrl;
const equivalent = MprisController.equivalentPlayers(player).find(candidate => {
return candidate !== player && _directArtworkUrl(candidate) !== "";
});
return _directArtworkUrl(equivalent);
}
function _commit(u, artKey, srcUrl) { function _commit(u, artKey, srcUrl) {
resolvedArtUrl = u; resolvedArtUrl = u;
_committedArtKey = u !== "" ? artKey : ""; _committedArtKey = u !== "" ? artKey : "";
@@ -171,11 +182,25 @@ Singleton {
onActivePlayerChanged: _updateArtUrl() onActivePlayerChanged: _updateArtUrl()
Connections { Connections {
target: root.activePlayer target: MprisController
ignoreUnknownSignals: true function onAvailablePlayersChanged() {
function onTrackTitleChanged() { root._updateArtUrl(); } root._updateArtUrl();
function onTrackArtUrlChanged() { root._updateArtUrl(); } }
function onMetadataChanged() { root._updateArtUrl(); } }
Instantiator {
model: MprisController.availablePlayers
delegate: Connections {
required property MprisPlayer modelData
target: modelData
ignoreUnknownSignals: true
function onIsPlayingChanged() { root._updateArtUrl(); }
function onTrackTitleChanged() { root._updateArtUrl(); }
function onTrackArtistChanged() { root._updateArtUrl(); }
function onTrackAlbumChanged() { root._updateArtUrl(); }
function onTrackArtUrlChanged() { root._updateArtUrl(); }
function onMetadataChanged() { root._updateArtUrl(); }
}
} }
function _trackKey() { function _trackKey() {
@@ -201,8 +226,8 @@ Singleton {
} }
_pendingArtKey = key; _pendingArtKey = key;
const url = getArtworkUrl(activePlayer); const url = getArtworkUrl(activePlayer);
// Ignore Chrome's same-track thumbnail size updates. // Ignore duplicate notifications, but let a richer peer replace same-track art
if (key !== "" && key === _committedArtKey) if (key !== "" && key === _committedArtKey && url === _committedSrcUrl)
return; return;
if (key !== "" && url !== "" && url === _committedSrcUrl) { if (key !== "" && url !== "" && url === _committedSrcUrl) {
// Chrome can publish track metadata before its new artwork URL. // Chrome can publish track metadata before its new artwork URL.
@@ -231,11 +231,28 @@ Singleton {
if (currentIndex === -1) if (currentIndex === -1)
currentIndex = 0; currentIndex = 0;
let targetIndex; let isRandom = false;
if (goToPrevious) { if (targetScreenName) {
targetIndex = currentIndex === 0 ? wallpaperList.length - 1 : currentIndex - 1; isRandom = !!SessionData.getMonitorCyclingSettings(targetScreenName).random;
} else { } else {
targetIndex = (currentIndex + 1) % wallpaperList.length; isRandom = !!SessionData.wallpaperCyclingRandom;
}
let targetIndex;
if (isRandom) {
if (wallpaperList.length > 1) {
do {
targetIndex = Math.floor(Math.random() * wallpaperList.length);
} while (targetIndex === currentIndex);
} else {
targetIndex = 0;
}
} else {
if (goToPrevious) {
targetIndex = currentIndex === 0 ? wallpaperList.length - 1 : currentIndex - 1;
} else {
targetIndex = (currentIndex + 1) % wallpaperList.length;
}
} }
const targetWallpaper = wallpaperList[targetIndex]; const targetWallpaper = wallpaperList[targetIndex];
if (!targetWallpaper || targetWallpaper === currentPath) if (!targetWallpaper || targetWallpaper === currentPath)
@@ -379,6 +379,9 @@ Item {
animationsEnabled = true; animationsEnabled = true;
shouldBeVisible = true; shouldBeVisible = true;
// Content-sized popouts lay out when contentWindow maps, while the geometry
// handlers are still gated off. Re-snapshot so the surface isn't left short.
_setSettledSurfaceGeometry();
if (screen) { if (screen) {
PopoutManager.showPopout(popoutHandle); PopoutManager.showPopout(popoutHandle);
opened(); opened();
@@ -338,6 +338,33 @@
], ],
"icon": "palette" "icon": "palette"
}, },
{
"section": "wallpaperCyclingRandom",
"label": "Random Order",
"tabIndex": 0,
"category": "Personalization",
"keywords": [
"alphabetical",
"appearance",
"automatic",
"background",
"bg",
"custom",
"customize",
"cycling",
"desktop",
"image",
"order",
"personal",
"personalization",
"picture",
"random",
"select",
"shuffle",
"wallpaper"
],
"description": "Select a random wallpaper instead of cycling in alphabetical order"
},
{ {
"section": "wallpaperTransition", "section": "wallpaperTransition",
"label": "Transition Effect", "label": "Transition Effect",
@@ -4536,6 +4563,7 @@
"appearance", "appearance",
"colors", "colors",
"controls", "controls",
"floating",
"look", "look",
"modal", "modal",
"modals", "modals",
@@ -4548,9 +4576,10 @@
"surface", "surface",
"surfaces", "surfaces",
"theme", "theme",
"transparency" "transparency",
"windows"
], ],
"description": "Controls opacity of shell surfaces, popouts, and modals" "description": "Controls opacity of shell surfaces, popouts, modals, and floating windows"
}, },
{ {
"section": "syncModeWithPortal", "section": "syncModeWithPortal",
@@ -5453,6 +5482,29 @@
"yubikey" "yubikey"
] ]
}, },
{
"section": "lockScreenSecurityKeyShortcutEnabled",
"label": "Security key shortcut",
"tabIndex": 11,
"category": "Lock Screen",
"keywords": [
"authentication",
"key",
"keybind",
"keyboard",
"lock",
"login",
"password",
"screen",
"security",
"shortcut",
"start",
"u2f",
"unlock",
"yubikey"
],
"description": "Keyboard shortcut to start security key unlock"
},
{ {
"section": "lockScreenShowMediaPlayer", "section": "lockScreenShowMediaPlayer",
"label": "Show Media Player", "label": "Show Media Player",
+2 -2
View File
@@ -4277,9 +4277,9 @@
"comment": "" "comment": ""
}, },
{ {
"term": "Controls opacity of shell surfaces, popouts, and modals", "term": "Controls opacity of shell surfaces, popouts, modals, and floating windows",
"translation": "", "translation": "",
"context": "Controls opacity of shell surfaces, popouts, and modals", "context": "Surface Opacity setting description including floating DMS windows",
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },