mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-05 21:18:30 -04:00
Compare commits
21 Commits
ef191babb7
...
settings
| Author | SHA1 | Date | |
|---|---|---|---|
| 82a9824175 | |||
| 2baf048293 | |||
| cffc33e14a | |||
| 0c811e3417 | |||
| 6ad46cf2c2 | |||
| 34626070af | |||
| 80b27b9a6a | |||
| 49f968d26b | |||
| 99b0dc596d | |||
| 158c0c12d8 | |||
| e089948225 | |||
| 7f2ba56e06 | |||
| 6de5593216 | |||
| 365474b0d9 | |||
| dc8a47644a | |||
| 27483e68dc | |||
| 400a18a8ed | |||
| 32ddf614c3 | |||
| 19d919ed5c | |||
| 594a2cde19 | |||
| 11287459c3 |
@@ -313,6 +313,7 @@ func EnsureContrastDPSLstar(hexColor, hexBg string, minLc float64, isLightMode b
|
||||
fg := HexToRGB(hexColor)
|
||||
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
|
||||
Lf, af, bf := cf.Lab()
|
||||
Lf *= 100.0
|
||||
|
||||
dir := 1.0
|
||||
if isLightMode {
|
||||
@@ -341,6 +342,7 @@ func EnsureContrastDPSBidirectional(hexColor, hexBg string, minLc float64, isLig
|
||||
fg := HexToRGB(hexColor)
|
||||
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
|
||||
origL, af, bf := cf.Lab()
|
||||
origL *= 100.0
|
||||
|
||||
var darkerResult, lighterResult string
|
||||
darkerL, lighterL := origL, origL
|
||||
@@ -420,6 +422,24 @@ func blendHue(base, target, factor float64) float64 {
|
||||
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 {
|
||||
rgb := HexToRGB(primary)
|
||||
hsv := RGBToHSV(rgb)
|
||||
@@ -500,10 +520,7 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
||||
gray7V := baseVal * 0.28
|
||||
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
|
||||
|
||||
gray8S := baseSat * 0.05
|
||||
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))
|
||||
palette.Color8 = NewColorInfo(DeriveDim(bgColor, hsv.H, baseSat*0.05, true))
|
||||
|
||||
brightRedS := math.Min(baseSat*1.0, 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)
|
||||
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
|
||||
|
||||
gray8S := baseSat * 0.15
|
||||
gray8V := baseVal * 0.65
|
||||
palette.Color8 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray8S, V: gray8V})), bgColor, secondaryTarget, opts))
|
||||
palette.Color8 = NewColorInfo(DeriveDim(bgColor, hsv.H, baseSat*0.15, false))
|
||||
|
||||
brightRedS := math.Min(baseSat*0.75, 1.0)
|
||||
brightRedV := math.Min(baseVal*1.35, 1.0)
|
||||
|
||||
@@ -679,3 +679,73 @@ func TestContrastAlgorithmComparison(t *testing.T) {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
arch := runtime.GOARCH
|
||||
switch arch {
|
||||
@@ -319,6 +324,7 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
||||
}
|
||||
g.log("Portage tree synced successfully")
|
||||
|
||||
args := emergeInstallArgs(missingPkgs)
|
||||
g.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")))
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
@@ -326,12 +332,10 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
||||
Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)),
|
||||
IsComplete: false,
|
||||
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, ", ")),
|
||||
}
|
||||
|
||||
args := []string{"emerge", "--ask=n", "--quiet"}
|
||||
args = append(args, missingPkgs...)
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
@@ -521,8 +525,7 @@ func (g *GentooDistribution) installPortagePackages(ctx context.Context, package
|
||||
}
|
||||
}
|
||||
|
||||
args := []string{"emerge", "--ask=n", "--quiet"}
|
||||
args = append(args, packageNames...)
|
||||
args := emergeInstallArgs(packageNames)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
@@ -713,8 +716,7 @@ func (g *GentooDistribution) installGURUPackages(ctx context.Context, packages [
|
||||
guruPackages[i] = pkg + "::guru"
|
||||
}
|
||||
|
||||
args := []string{"emerge", "--ask=n", "--quiet"}
|
||||
args = append(args, guruPackages...)
|
||||
args := emergeInstallArgs(guruPackages)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
|
||||
@@ -48,7 +48,7 @@ func NewManager() (*Manager, error) {
|
||||
return nil, fmt.Errorf("failed to find keyboards: %w", err)
|
||||
}
|
||||
|
||||
initialCapsLock := readInitialCapsLockState(devices[0])
|
||||
initialCapsLock, _ := capsLockFromDevices(devices)
|
||||
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
@@ -85,14 +85,21 @@ func NewManager() (*Manager, error) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func readInitialCapsLockState(device EvdevDevice) bool {
|
||||
ledStates, err := device.State(evLedType)
|
||||
if err != nil {
|
||||
log.Debugf("Could not read LED state: %v", err)
|
||||
return false
|
||||
func capsLockFromDevices(devices []EvdevDevice) (bool, bool) {
|
||||
for _, device := range devices {
|
||||
if device == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -297,25 +304,22 @@ func (m *Manager) readAndUpdateCapsLockState(deviceIndex int) {
|
||||
m.devicesMutex.RUnlock()
|
||||
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()
|
||||
|
||||
ledStates, err := device.State(evLedType)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to read LED state: %v", err)
|
||||
capsLockState, ok := capsLockFromDevices(ordered)
|
||||
if !ok {
|
||||
log.Debug("No LED-capable device available for caps lock state")
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -306,7 +306,7 @@ func TestNotifySubscribers(t *testing.T) {
|
||||
m.Close()
|
||||
}
|
||||
|
||||
func TestReadInitialCapsLockState(t *testing.T) {
|
||||
func TestCapsLockFromDevices(t *testing.T) {
|
||||
t.Run("caps lock is on", func(t *testing.T) {
|
||||
mockDevice := mocks.NewMockEvdevDevice(t)
|
||||
ledState := evdev.StateMap{
|
||||
@@ -314,7 +314,8 @@ func TestReadInitialCapsLockState(t *testing.T) {
|
||||
}
|
||||
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)
|
||||
})
|
||||
|
||||
@@ -325,7 +326,8 @@ func TestReadInitialCapsLockState(t *testing.T) {
|
||||
}
|
||||
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)
|
||||
})
|
||||
|
||||
@@ -333,9 +335,25 @@ func TestReadInitialCapsLockState(t *testing.T) {
|
||||
mockDevice := mocks.NewMockEvdevDevice(t)
|
||||
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)
|
||||
})
|
||||
|
||||
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) {
|
||||
|
||||
@@ -759,6 +759,9 @@ func (m *Manager) schedulerLoop() {
|
||||
|
||||
now := time.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
|
||||
if enabled {
|
||||
@@ -1104,13 +1107,15 @@ func (m *Manager) SetTemperature(low, high int) error {
|
||||
m.configMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
m.config.LowTemp = low
|
||||
m.config.HighTemp = high
|
||||
err := m.config.Validate()
|
||||
m.configMutex.Unlock()
|
||||
if err != nil {
|
||||
updated := m.config
|
||||
updated.LowTemp = low
|
||||
updated.HighTemp = high
|
||||
if err := updated.Validate(); err != nil {
|
||||
m.configMutex.Unlock()
|
||||
return err
|
||||
}
|
||||
m.config = updated
|
||||
m.configMutex.Unlock()
|
||||
m.triggerUpdate()
|
||||
return nil
|
||||
}
|
||||
@@ -1122,14 +1127,16 @@ func (m *Manager) SetLocation(lat, lon float64) error {
|
||||
m.configMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
m.config.Latitude = &lat
|
||||
m.config.Longitude = &lon
|
||||
m.config.UseIPLocation = false
|
||||
err := m.config.Validate()
|
||||
m.configMutex.Unlock()
|
||||
if err != nil {
|
||||
updated := m.config
|
||||
updated.Latitude = &lat
|
||||
updated.Longitude = &lon
|
||||
updated.UseIPLocation = false
|
||||
if err := updated.Validate(); err != nil {
|
||||
m.configMutex.Unlock()
|
||||
return err
|
||||
}
|
||||
m.config = updated
|
||||
m.configMutex.Unlock()
|
||||
m.triggerUpdate()
|
||||
return nil
|
||||
}
|
||||
@@ -1164,13 +1171,15 @@ func (m *Manager) SetManualTimes(sunrise, sunset time.Time) error {
|
||||
m.configMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
m.config.ManualSunrise = &sunrise
|
||||
m.config.ManualSunset = &sunset
|
||||
err := m.config.Validate()
|
||||
m.configMutex.Unlock()
|
||||
if err != nil {
|
||||
updated := m.config
|
||||
updated.ManualSunrise = &sunrise
|
||||
updated.ManualSunset = &sunset
|
||||
if err := updated.Validate(); err != nil {
|
||||
m.configMutex.Unlock()
|
||||
return err
|
||||
}
|
||||
m.config = updated
|
||||
m.configMutex.Unlock()
|
||||
m.triggerUpdate()
|
||||
return nil
|
||||
}
|
||||
@@ -1193,12 +1202,14 @@ func (m *Manager) SetGamma(gamma float64) error {
|
||||
m.configMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
m.config.Gamma = gamma
|
||||
err := m.config.Validate()
|
||||
m.configMutex.Unlock()
|
||||
if err != nil {
|
||||
updated := m.config
|
||||
updated.Gamma = gamma
|
||||
if err := updated.Validate(); err != nil {
|
||||
m.configMutex.Unlock()
|
||||
return err
|
||||
}
|
||||
m.config = updated
|
||||
m.configMutex.Unlock()
|
||||
m.triggerUpdate()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
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) {
|
||||
@@ -412,3 +413,75 @@ func TestNewManager_InvalidConfig(t *testing.T) {
|
||||
_, err := NewManager(mockDisplay, config)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -271,3 +271,61 @@ function getConflictingBinds(keyCombo, currentAction, allBinds, modKey) {
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -148,6 +148,7 @@ Singleton {
|
||||
property var includedTransitions: availableWallpaperTransitions.filter(t => t !== "none")
|
||||
|
||||
property bool wallpaperCyclingEnabled: false
|
||||
property bool wallpaperCyclingRandom: false
|
||||
property string wallpaperCyclingMode: "interval"
|
||||
property int wallpaperCyclingInterval: 300
|
||||
property string wallpaperCyclingTime: "06:00"
|
||||
@@ -646,6 +647,11 @@ Singleton {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setWallpaperCyclingRandom(random) {
|
||||
wallpaperCyclingRandom = random;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setWallpaperCyclingMode(mode) {
|
||||
wallpaperCyclingMode = mode;
|
||||
saveSettings();
|
||||
@@ -692,6 +698,37 @@ Singleton {
|
||||
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) {
|
||||
var screen = null;
|
||||
var screens = Quickshell.screens;
|
||||
@@ -1322,6 +1359,7 @@ Singleton {
|
||||
function getMonitorCyclingSettings(screenName) {
|
||||
var defaults = {
|
||||
"enabled": false,
|
||||
"random": false,
|
||||
"mode": "interval",
|
||||
"interval": 300,
|
||||
"time": "06:00"
|
||||
|
||||
@@ -659,9 +659,6 @@ Singleton {
|
||||
readonly property string iconTheme: resolveIconTheme()
|
||||
property var availableIconThemes: ["System Default"]
|
||||
property string systemDefaultIconTheme: ""
|
||||
property bool qt5ctAvailable: false
|
||||
property bool qt6ctAvailable: false
|
||||
property bool gtkAvailable: false
|
||||
|
||||
property var cursorSettings: ({
|
||||
"theme": "System Default",
|
||||
@@ -798,8 +795,6 @@ Singleton {
|
||||
property int fadeToDpmsGracePeriod: 5
|
||||
property string launchPrefix: ""
|
||||
|
||||
property bool gtkThemingEnabled: false
|
||||
property bool qtThemingEnabled: false
|
||||
property bool syncModeWithPortal: true
|
||||
property bool terminalsAlwaysDark: false
|
||||
|
||||
@@ -922,6 +917,8 @@ Singleton {
|
||||
property bool lockPamInlineU2f: false
|
||||
property bool lockPamExternallyManaged: false
|
||||
property string lockU2fPamPath: ""
|
||||
property string lockScreenSecurityKeyShortcut: "Ctrl+Q"
|
||||
property bool lockScreenSecurityKeyShortcutEnabled: false
|
||||
property bool greeterPamExternallyManaged: false
|
||||
property string lockScreenInactiveColor: "#000000"
|
||||
property int lockScreenNotificationMode: 0
|
||||
@@ -1683,7 +1680,6 @@ Singleton {
|
||||
_hasLoaded = true;
|
||||
applyStoredTheme();
|
||||
updateCompositorCursor();
|
||||
Processes.detectQtTools();
|
||||
Qt.callLater(checkIconThemeDrift);
|
||||
|
||||
_checkSettingsWritable();
|
||||
|
||||
@@ -96,8 +96,6 @@ Singleton {
|
||||
}
|
||||
|
||||
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 pendingThemeRequest: null
|
||||
|
||||
@@ -352,6 +350,8 @@ Singleton {
|
||||
readonly property color readableSurfaceHigh: withAlpha(surfaceContainerHigh, popupTransparency)
|
||||
readonly property color floatingSurface: foregroundLayers ? readableSurface : withAlpha(readableSurface, 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 notificationFloatingSurface: notificationForegroundLayers ? readableSurface : withAlpha(readableSurface, 0)
|
||||
readonly property color notificationFloatingSurfaceHigh: notificationForegroundLayers ? readableSurfaceHigh : withAlpha(readableSurfaceHigh, 0)
|
||||
|
||||
@@ -1,31 +1,69 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Services.SystemTray
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
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) {
|
||||
if (!screenName || !menu) return
|
||||
const newMenus = Object.assign({}, activeTrayMenus)
|
||||
newMenus[screenName] = menu
|
||||
activeTrayMenus = newMenus
|
||||
if (!screenName || !menu)
|
||||
return;
|
||||
const newMenus = Object.assign({}, activeTrayMenus);
|
||||
newMenus[screenName] = menu;
|
||||
activeTrayMenus = newMenus;
|
||||
}
|
||||
|
||||
function unregisterMenu(screenName) {
|
||||
if (!screenName) return
|
||||
const newMenus = Object.assign({}, activeTrayMenus)
|
||||
delete newMenus[screenName]
|
||||
activeTrayMenus = newMenus
|
||||
if (!screenName)
|
||||
return;
|
||||
const newMenus = Object.assign({}, activeTrayMenus);
|
||||
delete newMenus[screenName];
|
||||
activeTrayMenus = newMenus;
|
||||
}
|
||||
|
||||
function closeAllMenus() {
|
||||
function closeHoverMenus() {
|
||||
for (const screenName in activeTrayMenus) {
|
||||
const menu = activeTrayMenus[screenName]
|
||||
if (!menu) continue
|
||||
if (!menu || menu.openedByHover !== true) continue
|
||||
if (typeof menu.close === "function") {
|
||||
menu.close()
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,41 +487,10 @@ Singleton {
|
||||
return pamFprintDetected ? "probe_failed" : "missing_pam_support";
|
||||
}
|
||||
|
||||
// --- Qt tools detection ---
|
||||
|
||||
function detectQtTools() {
|
||||
qtToolsDetectionProcess.running = true;
|
||||
}
|
||||
|
||||
function checkPluginSettings() {
|
||||
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 {
|
||||
id: authApplyDebounce
|
||||
interval: 300
|
||||
|
||||
@@ -19,6 +19,7 @@ var SPEC = {
|
||||
includedTransitions: { def: ["fade", "wipe", "disc", "stripes", "iris bloom", "pixelate", "portal"] },
|
||||
|
||||
wallpaperCyclingEnabled: { def: false },
|
||||
wallpaperCyclingRandom: { def: false },
|
||||
wallpaperCyclingMode: { def: "interval" },
|
||||
wallpaperCyclingInterval: { def: 300 },
|
||||
wallpaperCyclingTime: { def: "06:00" },
|
||||
|
||||
@@ -298,9 +298,6 @@ var SPEC = {
|
||||
lastAppliedIconTheme: { def: "" },
|
||||
availableIconThemes: { def: ["System Default"], 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" },
|
||||
availableCursorThemes: { def: ["System Default"], persist: false },
|
||||
@@ -374,8 +371,6 @@ var SPEC = {
|
||||
fadeToDpmsGracePeriod: { def: 5 },
|
||||
launchPrefix: { def: "" },
|
||||
|
||||
gtkThemingEnabled: { def: false, onChange: "regenSystemThemes" },
|
||||
qtThemingEnabled: { def: false, onChange: "regenSystemThemes" },
|
||||
syncModeWithPortal: { def: true },
|
||||
terminalsAlwaysDark: { def: false, onChange: "regenSystemThemes" },
|
||||
|
||||
@@ -473,6 +468,8 @@ var SPEC = {
|
||||
enableU2f: { def: false, onChange: "scheduleAuthApply" },
|
||||
u2fMode: { def: "or" },
|
||||
lockPamPath: { def: "" },
|
||||
lockScreenSecurityKeyShortcut: { def: "Ctrl+Q" },
|
||||
lockScreenSecurityKeyShortcutEnabled: { def: false },
|
||||
lockPamInlineFprint: { def: false },
|
||||
lockPamInlineU2f: { def: false },
|
||||
lockPamExternallyManaged: { def: false },
|
||||
|
||||
@@ -1015,6 +1015,9 @@ Item {
|
||||
case "reboot":
|
||||
SessionService.reboot();
|
||||
break;
|
||||
case "softreboot":
|
||||
SessionService.softReboot();
|
||||
break;
|
||||
case "poweroff":
|
||||
SessionService.poweroff();
|
||||
break;
|
||||
|
||||
+14
-14
@@ -2023,18 +2023,6 @@ Item {
|
||||
}
|
||||
|
||||
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 {
|
||||
const items = SystemTray.items.values;
|
||||
if (items.length === 0)
|
||||
@@ -2050,7 +2038,7 @@ Item {
|
||||
}
|
||||
|
||||
function activate(itemId: string): string {
|
||||
const item = findTrayItem(itemId);
|
||||
const item = TrayMenuManager.findTrayItem(itemId);
|
||||
if (!item)
|
||||
return `ERROR: Tray item not found: ${itemId}`;
|
||||
|
||||
@@ -2058,8 +2046,20 @@ Item {
|
||||
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 {
|
||||
const item = findTrayItem(itemId);
|
||||
const item = TrayMenuManager.findTrayItem(itemId);
|
||||
if (!item)
|
||||
return `ERROR: Tray item not found: ${itemId}`;
|
||||
|
||||
|
||||
@@ -819,16 +819,24 @@ Item {
|
||||
}
|
||||
|
||||
if (isCategoryFiltered) {
|
||||
var rawApps = AppSearchService.getAppsInCategory(appCategory);
|
||||
for (var i = 0; i < rawApps.length; 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]));
|
||||
var categoryPluginId = AppSearchService.getPluginIdForCategory(appCategory);
|
||||
if (categoryPluginId) {
|
||||
var pluginCategoryItems = getPluginItems(categoryPluginId, "");
|
||||
for (var i = 0; i < pluginCategoryItems.length; i++) {
|
||||
allItems.push(pluginCategoryItems[i]);
|
||||
}
|
||||
} else {
|
||||
var rawApps = AppSearchService.getAppsInCategory(appCategory);
|
||||
for (var i = 0; i < rawApps.length; 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 {
|
||||
var apps = searchApps(searchQuery);
|
||||
@@ -839,7 +847,7 @@ Item {
|
||||
|
||||
var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);
|
||||
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 i = 0; i < newSections.length; i++) {
|
||||
|
||||
@@ -13,6 +13,17 @@ FocusScope {
|
||||
property bool awaitingFprintForPassword: false
|
||||
property var windowControls: null
|
||||
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 polkitLibPamText: ""
|
||||
@@ -202,7 +213,7 @@ FocusScope {
|
||||
StyledText {
|
||||
text: root.currentFlow?.message ?? ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceTextMedium
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
wrapMode: Text.Wrap
|
||||
maximumLineCount: 2
|
||||
@@ -272,9 +283,10 @@ FocusScope {
|
||||
|
||||
width: parent.width
|
||||
height: root.inputFieldHeight
|
||||
backgroundColor: Theme.surfaceHover
|
||||
normalBorderColor: Theme.outlineStrong
|
||||
focusedBorderColor: Theme.primary
|
||||
cornerRadius: Theme.cornerRadius
|
||||
backgroundColor: root._fieldColor
|
||||
normalBorderColor: root._fieldBorderColor
|
||||
focusedBorderColor: root._fieldFocusedBorderColor
|
||||
borderWidth: 1
|
||||
focusedBorderWidth: 2
|
||||
leftIconName: root.polkitPamHasFprint ? "fingerprint" : ""
|
||||
@@ -352,7 +364,7 @@ FocusScope {
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Authenticate")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.background
|
||||
color: Theme.primaryText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ FloatingWindow {
|
||||
title: I18n.tr("Authentication")
|
||||
minimumSize: Qt.size(460, 220)
|
||||
maximumSize: Qt.size(460, 220)
|
||||
color: Theme.surfaceContainer
|
||||
color: Theme.floatingWindowSurface
|
||||
visible: false
|
||||
|
||||
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 {
|
||||
id: contentLoader
|
||||
anchors.fill: parent
|
||||
|
||||
@@ -175,6 +175,8 @@ DankModal {
|
||||
visibleActions = allActions.filter(action => {
|
||||
if (action === "hibernate" && !SessionService.hibernateSupported)
|
||||
return false;
|
||||
if (action === "softreboot" && !SessionService.softRebootSupported)
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -223,6 +225,12 @@ DankModal {
|
||||
"label": I18n.tr("Reboot"),
|
||||
"key": "R"
|
||||
};
|
||||
case "softreboot":
|
||||
return {
|
||||
"icon": "autorenew",
|
||||
"label": I18n.tr("Soft Reboot"),
|
||||
"key": "B"
|
||||
};
|
||||
case "logout":
|
||||
return {
|
||||
"icon": "logout",
|
||||
@@ -370,7 +378,7 @@ DankModal {
|
||||
|
||||
function handleListNavigation(event, 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();
|
||||
event.accepted = true;
|
||||
}
|
||||
@@ -429,6 +437,12 @@ DankModal {
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_B:
|
||||
if (visibleActions.includes("softreboot")) {
|
||||
startHold("softreboot", visibleActions.indexOf("softreboot"));
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_X:
|
||||
if (visibleActions.includes("logout")) {
|
||||
startHold("logout", visibleActions.indexOf("logout"));
|
||||
@@ -464,7 +478,7 @@ DankModal {
|
||||
|
||||
function handleGridNavigation(event, 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();
|
||||
event.accepted = true;
|
||||
}
|
||||
@@ -539,6 +553,12 @@ DankModal {
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_B:
|
||||
if (visibleActions.includes("softreboot")) {
|
||||
startHold("softreboot", visibleActions.indexOf("softreboot"));
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_X:
|
||||
if (visibleActions.includes("logout")) {
|
||||
startHold("logout", visibleActions.indexOf("logout"));
|
||||
@@ -597,7 +617,7 @@ DankModal {
|
||||
|
||||
readonly property var actionData: root.getActionData(modelData)
|
||||
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
|
||||
|
||||
width: (root.modalWidth - Theme.spacingL * 2 - Theme.spacingS * (root.gridColumns - 1)) / root.gridColumns
|
||||
@@ -627,7 +647,7 @@ DankModal {
|
||||
color: {
|
||||
if (gridButtonRect.modelData === "poweroff")
|
||||
return Theme.errorSelected;
|
||||
if (gridButtonRect.modelData === "reboot")
|
||||
if (gridButtonRect.modelData === "reboot" || gridButtonRect.modelData === "softreboot")
|
||||
return Theme.withAlpha(Theme.warning, 0.3);
|
||||
return Theme.primarySelected;
|
||||
}
|
||||
@@ -722,7 +742,7 @@ DankModal {
|
||||
|
||||
readonly property var actionData: root.getActionData(modelData)
|
||||
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
|
||||
|
||||
width: parent.width
|
||||
@@ -752,7 +772,7 @@ DankModal {
|
||||
color: {
|
||||
if (listButtonRect.modelData === "poweroff")
|
||||
return Theme.errorSelected;
|
||||
if (listButtonRect.modelData === "reboot")
|
||||
if (listButtonRect.modelData === "reboot" || listButtonRect.modelData === "softreboot")
|
||||
return Theme.withAlpha(Theme.warning, 0.3);
|
||||
return Theme.primarySelected;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import qs.Widgets
|
||||
FloatingWindow {
|
||||
id: settingsModal
|
||||
|
||||
property bool disablePopupTransparency: true
|
||||
property var profileBrowser: profileBrowserLoader.item
|
||||
property var wallpaperBrowser: wallpaperBrowserLoader.item
|
||||
|
||||
@@ -95,7 +96,7 @@ FloatingWindow {
|
||||
minimumSize: Qt.size(500, 400)
|
||||
implicitWidth: 900
|
||||
implicitHeight: screen ? Math.min(940, screen.height - 100) : 940
|
||||
color: Theme.surfaceContainer
|
||||
color: Theme.floatingWindowSurface
|
||||
visible: false
|
||||
|
||||
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 {
|
||||
active: settingsModal.visible
|
||||
sourceComponent: Component {
|
||||
@@ -180,8 +200,6 @@ FloatingWindow {
|
||||
FocusScope {
|
||||
id: contentFocusScope
|
||||
|
||||
property bool disablePopupTransparency: true
|
||||
|
||||
LayoutMirroring.enabled: I18n.isRtl
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
@@ -203,12 +221,6 @@ FloatingWindow {
|
||||
onDoubleClicked: windowControls.tryToggleMaximize()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.surfaceContainer
|
||||
opacity: 0.5
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
|
||||
@@ -641,7 +641,7 @@ Rectangle {
|
||||
implicitWidth: __calculatedWidth
|
||||
width: __calculatedWidth
|
||||
height: parent.height
|
||||
color: Theme.surfaceContainer
|
||||
color: "transparent"
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
Component.onCompleted: {
|
||||
|
||||
@@ -201,7 +201,16 @@ Item {
|
||||
direction: root.effectiveShadowDirection
|
||||
fallbackOffset: 4
|
||||
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
|
||||
shadowOffsetX: root.shadowOffsetX
|
||||
|
||||
@@ -609,7 +609,7 @@ Item {
|
||||
_closeHoverNotepad();
|
||||
activeHoverTrigger = "";
|
||||
PopoutManager.dismissHoverPopoutForScreen(barWindow?.screen);
|
||||
TrayMenuManager.closeAllMenus();
|
||||
TrayMenuManager.closeHoverMenus();
|
||||
}
|
||||
|
||||
function _beginSupersededCloseForActive() {
|
||||
|
||||
@@ -83,6 +83,22 @@ BasePill {
|
||||
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) {
|
||||
if (!trayItem) {
|
||||
return;
|
||||
@@ -1484,6 +1500,7 @@ BasePill {
|
||||
property bool isVertical: false
|
||||
property var axis: null
|
||||
property bool showMenu: false
|
||||
property bool openedByHover: false
|
||||
property var menuHandle: null
|
||||
|
||||
ListModel {
|
||||
@@ -1493,7 +1510,8 @@ BasePill {
|
||||
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;
|
||||
anchorItem = anchor;
|
||||
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)
|
||||
return;
|
||||
if (currentTrayMenu) {
|
||||
@@ -2099,7 +2117,7 @@ BasePill {
|
||||
currentTrayMenu = trayMenuComponent.createObject(null);
|
||||
if (!currentTrayMenu)
|
||||
return;
|
||||
currentTrayMenu.showForTrayItem(item, anchor, screen, atBottom, vertical ?? false, axisObj);
|
||||
currentTrayMenu.showForTrayItem(item, anchor, screen, atBottom, vertical ?? false, axisObj, byHover === true);
|
||||
}
|
||||
|
||||
function _trayLayoutRoot() {
|
||||
@@ -2147,7 +2165,7 @@ BasePill {
|
||||
if (!hit?.trayItem?.hasMenu)
|
||||
return false;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,7 +493,7 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: activePlayer?.trackAlbum || ""
|
||||
text: MprisController.stableAlbum
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextSecondary
|
||||
width: parent.width
|
||||
|
||||
@@ -14,6 +14,7 @@ import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Session
|
||||
import "../../DankCommon/Common/LayoutCodes.js" as LayoutCodes
|
||||
import "../../Common/KeyUtils.js" as KeyUtils
|
||||
|
||||
Item {
|
||||
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;
|
||||
}
|
||||
|
||||
function triggerSecurityKeyUnlock() {
|
||||
if (!canStartSecurityKeyUnlock())
|
||||
return;
|
||||
passwordField.clear();
|
||||
pam.u2f.startForAlternativeAuth();
|
||||
}
|
||||
|
||||
function securityKeyShortcutMatches(event) {
|
||||
return SettingsData.lockScreenSecurityKeyShortcutEnabled
|
||||
&& KeyUtils.eventMatchesCombo(event, SettingsData.lockScreenSecurityKeyShortcut);
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
WeatherService.addRef();
|
||||
UserInfoService.getUserInfo();
|
||||
@@ -962,6 +975,12 @@ Item {
|
||||
}
|
||||
|
||||
if ((event.modifiers & Qt.ControlModifier) && !(event.modifiers & (Qt.AltModifier | Qt.MetaModifier))) {
|
||||
if (securityKeyShortcutMatches(event) && canStartSecurityKeyUnlock()) {
|
||||
triggerSecurityKeyUnlock();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.key) {
|
||||
case Qt.Key_A:
|
||||
cursorPosition = 0;
|
||||
@@ -1238,10 +1257,8 @@ Item {
|
||||
buttonSize: 32
|
||||
visible: root.canStartSecurityKeyUnlock()
|
||||
enabled: visible
|
||||
onClicked: {
|
||||
passwordField.clear();
|
||||
pam.u2f.startForAlternativeAuth();
|
||||
}
|
||||
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")
|
||||
onClicked: root.triggerSecurityKeyUnlock()
|
||||
}
|
||||
DankActionButton {
|
||||
id: virtualKeyboardButton
|
||||
@@ -1329,6 +1346,7 @@ Item {
|
||||
width: parent.width
|
||||
height: parent.height / 2
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: -1
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, 0.9)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import qs.Modules.Notepad
|
||||
FloatingWindow {
|
||||
id: win
|
||||
|
||||
property bool disablePopupTransparency: true
|
||||
property alias shouldBeVisible: win.visible
|
||||
property alias notepad: notepad
|
||||
|
||||
@@ -27,7 +28,7 @@ FloatingWindow {
|
||||
minimumSize: Qt.size(360, 320)
|
||||
implicitWidth: 640
|
||||
implicitHeight: 760
|
||||
color: Theme.surfaceContainer
|
||||
color: Theme.notepadWindowSurface
|
||||
visible: false
|
||||
|
||||
onVisibleChanged: {
|
||||
@@ -38,9 +39,27 @@ FloatingWindow {
|
||||
}
|
||||
}
|
||||
|
||||
// A compositor close (e.g. niri close-window)
|
||||
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 {
|
||||
anchors.fill: parent
|
||||
|
||||
@@ -58,12 +77,6 @@ FloatingWindow {
|
||||
onDoubleClicked: windowControls.tryToggleMaximize()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.surfaceContainerHigh
|
||||
opacity: 0.5
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
|
||||
@@ -156,9 +156,10 @@ DankOSD {
|
||||
if (MprisController.isFirefoxYoutubeHoverPreview(player))
|
||||
return;
|
||||
|
||||
const newTitle = player.trackTitle || "";
|
||||
const newArtist = player.trackArtist || "";
|
||||
const newAlbum = player.trackAlbum || "";
|
||||
const metaPlayer = MprisController.bestMetadataPlayer(player);
|
||||
const newTitle = MprisController.displayTrackTitle(metaPlayer);
|
||||
const newArtist = metaPlayer.trackArtist || "";
|
||||
const newAlbum = metaPlayer.trackAlbum || "";
|
||||
const trackChanged = newTitle !== root._displayTitle || newArtist !== root._displayArtist || newAlbum !== root._displayAlbum;
|
||||
|
||||
root._displayTitle = newTitle;
|
||||
@@ -263,37 +264,102 @@ DankOSD {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Theme.iconSize
|
||||
height: Theme.iconSize
|
||||
radius: Theme.iconSize / 2
|
||||
color: "transparent"
|
||||
Row {
|
||||
id: transportControls
|
||||
|
||||
x: parent.gap
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXXS
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: root._displayIcon
|
||||
size: Theme.iconSize
|
||||
color: playPauseButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||
Rectangle {
|
||||
width: Theme.iconSize - 4
|
||||
height: Theme.iconSize - 4
|
||||
radius: (Theme.iconSize - 4) / 2
|
||||
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 {
|
||||
id: playPauseButton
|
||||
Rectangle {
|
||||
width: Theme.iconSize
|
||||
height: Theme.iconSize
|
||||
radius: Theme.iconSize / 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: "transparent"
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
togglePlaying();
|
||||
root.hide();
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: root._displayIcon
|
||||
size: Theme.iconSize
|
||||
color: playPauseButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
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 {
|
||||
x: parent.gap * 2 + Theme.iconSize
|
||||
width: parent.width - Theme.iconSize - parent.gap * 3
|
||||
x: parent.gap * 2 + transportControls.width
|
||||
width: parent.width - transportControls.width - parent.gap * 3
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
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) {
|
||||
const result = {};
|
||||
const outputRegex = /output\s+"([^"]+)"\s*\{([^}]*)\}/g;
|
||||
let match;
|
||||
while ((match = outputRegex.exec(content)) !== null) {
|
||||
const name = match[1];
|
||||
const body = match[2];
|
||||
for (const block of extractNiriOutputBlocks(content)) {
|
||||
const name = block.name;
|
||||
const body = block.body;
|
||||
|
||||
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 posMatch = body.match(/position\s+x=(-?\d+)\s+y=(-?\d+)/);
|
||||
const scaleMatch = body.match(/scale\s+([\d.]+)/);
|
||||
|
||||
@@ -5,6 +5,7 @@ import qs.Modals.FileBrowser
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Settings.Widgets
|
||||
import "../../Common/KeyUtils.js" as KeyUtils
|
||||
|
||||
Item {
|
||||
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 {
|
||||
settingKey: "lockU2fPamPath"
|
||||
tags: ["lock", "screen", "pam", "u2f", "security", "key", "source", "service"]
|
||||
|
||||
@@ -410,8 +410,8 @@ Item {
|
||||
settingKey: "powerMenuDefaultAction"
|
||||
tags: ["power", "menu", "default", "action", "reboot", "logout", "shutdown"]
|
||||
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")]
|
||||
property var actionValues: ["reboot", "logout", "poweroff", "lock", "suspend", "restart", "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", "softreboot"]
|
||||
|
||||
Component.onCompleted: {
|
||||
const currentAction = SettingsData.powerMenuDefaultAction || "logout";
|
||||
@@ -475,6 +475,12 @@ Item {
|
||||
label: I18n.tr("Show Hibernate"),
|
||||
desc: I18n.tr("Only visible if hibernate is supported by your system"),
|
||||
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]
|
||||
text: modelData.label
|
||||
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)
|
||||
onToggled: checked => {
|
||||
let actions = [...SettingsData.powerMenuActions];
|
||||
|
||||
@@ -1728,7 +1728,7 @@ Item {
|
||||
tags: ["surface", "popup", "transparency", "opacity", "modal"]
|
||||
settingKey: "popupTransparency"
|
||||
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
|
||||
value: Math.round(SettingsData.popupTransparency * 100)
|
||||
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 {
|
||||
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")]
|
||||
|
||||
@@ -38,7 +38,9 @@ StyledRect {
|
||||
return h;
|
||||
}
|
||||
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 hasHeader: root.title !== "" || root.iconName !== ""
|
||||
|
||||
@@ -35,7 +35,9 @@ StyledRect {
|
||||
width: parent?.width ?? 0
|
||||
height: Theme.spacingL * 2 + contentColumn.height
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
color: Theme.nestedSurface
|
||||
border.color: Theme.outlineMedium
|
||||
border.width: Theme.layerOutlineWidth
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!settingKey)
|
||||
|
||||
@@ -29,7 +29,9 @@ StyledRect {
|
||||
width: parent?.width ?? 0
|
||||
height: Theme.spacingL * 2 + mainColumn.height
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
color: Theme.nestedSurface
|
||||
border.color: Theme.outlineMedium
|
||||
border.width: Theme.layerOutlineWidth
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!settingKey)
|
||||
|
||||
@@ -785,16 +785,24 @@ Singleton {
|
||||
if (category === I18n.tr("All"))
|
||||
return visibleApps;
|
||||
|
||||
const pluginItems = getPluginItems(category, "");
|
||||
if (pluginItems.length > 0)
|
||||
return pluginItems;
|
||||
|
||||
return visibleApps.filter(app => {
|
||||
const appCategories = getCategoriesForApp(app);
|
||||
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
|
||||
function getPluginCategories() {
|
||||
if (typeof PluginService === "undefined") {
|
||||
@@ -814,31 +822,19 @@ Singleton {
|
||||
}
|
||||
|
||||
function getPluginCategoryIcon(category) {
|
||||
if (typeof PluginService === "undefined")
|
||||
const pluginId = getPluginIdForCategory(category);
|
||||
if (!pluginId)
|
||||
return null;
|
||||
|
||||
const launchers = PluginService.getLauncherPlugins();
|
||||
for (const pluginId in launchers) {
|
||||
const plugin = launchers[pluginId];
|
||||
if ((plugin.name || pluginId) === category) {
|
||||
return plugin.icon || "extension";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return PluginService.getLauncherPlugins()[pluginId].icon || "extension";
|
||||
}
|
||||
|
||||
function getPluginItems(category, query) {
|
||||
if (typeof PluginService === "undefined")
|
||||
const pluginId = getPluginIdForCategory(category);
|
||||
if (!pluginId)
|
||||
return [];
|
||||
|
||||
const launchers = PluginService.getLauncherPlugins();
|
||||
for (const pluginId in launchers) {
|
||||
const plugin = launchers[pluginId];
|
||||
if ((plugin.name || pluginId) === category) {
|
||||
return getPluginItemsForPlugin(pluginId, query);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
return getPluginItemsForPlugin(pluginId, query);
|
||||
}
|
||||
|
||||
function getPluginItemsForPlugin(pluginId, query) {
|
||||
|
||||
@@ -23,6 +23,13 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: typeof PowerProfiles !== "undefined" ? PowerProfiles : null
|
||||
function onHasPerformanceProfileChanged() {
|
||||
root.applyPowerProfile();
|
||||
}
|
||||
}
|
||||
|
||||
function applyPowerProfile() {
|
||||
if (!batteryAvailable)
|
||||
return;
|
||||
@@ -32,7 +39,7 @@ Singleton {
|
||||
const targetProfile = parseInt(profileValue);
|
||||
if (isNaN(targetProfile) || PowerProfiles.profile === targetProfile)
|
||||
return;
|
||||
PowerProfiles.profile = targetProfile;
|
||||
PowerProfileWatcher.applyProfile(targetProfile);
|
||||
}
|
||||
|
||||
readonly property string preferredBatteryOverride: Quickshell.env("DMS_PREFERRED_BATTERY")
|
||||
|
||||
@@ -1721,6 +1721,8 @@ Singleton {
|
||||
return "Temperature must be between 2500K and 6000K";
|
||||
|
||||
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);
|
||||
|
||||
if (root.nightModeEnabled) {
|
||||
@@ -1750,6 +1752,8 @@ Singleton {
|
||||
return "Temperature must be between 2500K and 6500K";
|
||||
|
||||
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);
|
||||
|
||||
if (root.nightModeEnabled && SessionData.nightModeAutoEnabled)
|
||||
|
||||
@@ -47,6 +47,7 @@ Singleton {
|
||||
// Chromium can report blank metadata between tracks
|
||||
property string stableTitle: ""
|
||||
property string stableArtist: ""
|
||||
property string stableAlbum: ""
|
||||
|
||||
Connections {
|
||||
target: root.activePlayer
|
||||
@@ -59,6 +60,9 @@ Singleton {
|
||||
root._syncStableMeta();
|
||||
root._checkIdle();
|
||||
}
|
||||
function onTrackAlbumChanged() {
|
||||
root._syncStableMeta();
|
||||
}
|
||||
function onLengthChanged() {
|
||||
if (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) {
|
||||
root.activePlayerStableLength = root.activePlayer.length;
|
||||
@@ -72,8 +76,10 @@ Singleton {
|
||||
|
||||
onActivePlayerChanged: {
|
||||
activePlayerStableLength = (activePlayer && activePlayer.lengthSupported && activePlayer.length > 1) ? activePlayer.length : 0;
|
||||
stableTitle = activePlayer?.trackTitle || "";
|
||||
stableArtist = activePlayer?.trackArtist || "";
|
||||
stableTitle = "";
|
||||
stableArtist = "";
|
||||
stableAlbum = "";
|
||||
_syncStableMeta();
|
||||
_checkIdle();
|
||||
}
|
||||
|
||||
@@ -82,14 +88,24 @@ Singleton {
|
||||
if (!p) {
|
||||
stableTitle = "";
|
||||
stableArtist = "";
|
||||
stableAlbum = "";
|
||||
return;
|
||||
}
|
||||
if (isFirefoxYoutubeHoverPreview(p))
|
||||
return;
|
||||
if (p.trackTitle)
|
||||
stableTitle = p.trackTitle;
|
||||
if (p.trackArtist)
|
||||
stableArtist = p.trackArtist;
|
||||
const metadataPlayer = bestMetadataPlayer(p);
|
||||
const nextTitle = displayTrackTitle(metadataPlayer);
|
||||
const trackChanged = nextTitle && stableTitle && nextTitle.toLowerCase() !== stableTitle.toLowerCase();
|
||||
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
|
||||
@@ -101,6 +117,7 @@ Singleton {
|
||||
return;
|
||||
root.stableTitle = "";
|
||||
root.stableArtist = "";
|
||||
root.stableAlbum = "";
|
||||
root._resolveActivePlayer();
|
||||
}
|
||||
}
|
||||
@@ -122,20 +139,133 @@ Singleton {
|
||||
delegate: Connections {
|
||||
required property MprisPlayer modelData
|
||||
target: modelData
|
||||
ignoreUnknownSignals: true
|
||||
function onIsPlayingChanged() {
|
||||
root._resolveActivePlayer();
|
||||
root._syncStableMeta();
|
||||
}
|
||||
function onTrackTitleChanged() {
|
||||
if (modelData.isPlaying)
|
||||
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 {
|
||||
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 {
|
||||
// A playing player always wins; otherwise keep the selection stable w/idle
|
||||
const playing = availablePlayers.find(p => p.isPlaying);
|
||||
const playing = _bestPlayingPlayer();
|
||||
if (playing) {
|
||||
if (activePlayer !== playing) {
|
||||
activePlayer = playing;
|
||||
|
||||
@@ -19,6 +19,7 @@ Singleton {
|
||||
property bool loginctlCommandAvailable: false
|
||||
property bool systemctlCommandAvailable: false
|
||||
property bool hibernateSupported: false
|
||||
readonly property bool softRebootSupported: systemctlCommandAvailable
|
||||
property bool inhibitorAvailable: true
|
||||
property bool idleInhibited: false
|
||||
property string inhibitReason: "Keep system awake"
|
||||
@@ -470,6 +471,10 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function softReboot() {
|
||||
Quickshell.execDetached(["systemctl", "soft-reboot"]);
|
||||
}
|
||||
|
||||
function poweroff() {
|
||||
if (SettingsData.customPowerActionPowerOff.length === 0) {
|
||||
Quickshell.execDetached(powerManagerCommand("poweroff"));
|
||||
|
||||
@@ -26,7 +26,7 @@ Singleton {
|
||||
return hash.toString(16).padStart(8, '0');
|
||||
}
|
||||
|
||||
function getArtworkUrl(player) {
|
||||
function _directArtworkUrl(player) {
|
||||
if (!player) return "";
|
||||
|
||||
let artUrl = player.trackArtUrl || "";
|
||||
@@ -54,6 +54,17 @@ Singleton {
|
||||
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) {
|
||||
resolvedArtUrl = u;
|
||||
_committedArtKey = u !== "" ? artKey : "";
|
||||
@@ -171,11 +182,25 @@ Singleton {
|
||||
onActivePlayerChanged: _updateArtUrl()
|
||||
|
||||
Connections {
|
||||
target: root.activePlayer
|
||||
ignoreUnknownSignals: true
|
||||
function onTrackTitleChanged() { root._updateArtUrl(); }
|
||||
function onTrackArtUrlChanged() { root._updateArtUrl(); }
|
||||
function onMetadataChanged() { root._updateArtUrl(); }
|
||||
target: MprisController
|
||||
function onAvailablePlayersChanged() {
|
||||
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() {
|
||||
@@ -201,8 +226,8 @@ Singleton {
|
||||
}
|
||||
_pendingArtKey = key;
|
||||
const url = getArtworkUrl(activePlayer);
|
||||
// Ignore Chrome's same-track thumbnail size updates.
|
||||
if (key !== "" && key === _committedArtKey)
|
||||
// Ignore duplicate notifications, but let a richer peer replace same-track art
|
||||
if (key !== "" && key === _committedArtKey && url === _committedSrcUrl)
|
||||
return;
|
||||
if (key !== "" && url !== "" && url === _committedSrcUrl) {
|
||||
// Chrome can publish track metadata before its new artwork URL.
|
||||
|
||||
@@ -231,11 +231,28 @@ Singleton {
|
||||
if (currentIndex === -1)
|
||||
currentIndex = 0;
|
||||
|
||||
let targetIndex;
|
||||
if (goToPrevious) {
|
||||
targetIndex = currentIndex === 0 ? wallpaperList.length - 1 : currentIndex - 1;
|
||||
let isRandom = false;
|
||||
if (targetScreenName) {
|
||||
isRandom = !!SessionData.getMonitorCyclingSettings(targetScreenName).random;
|
||||
} 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];
|
||||
if (!targetWallpaper || targetWallpaper === currentPath)
|
||||
|
||||
@@ -379,6 +379,9 @@ Item {
|
||||
|
||||
animationsEnabled = 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) {
|
||||
PopoutManager.showPopout(popoutHandle);
|
||||
opened();
|
||||
|
||||
@@ -338,6 +338,33 @@
|
||||
],
|
||||
"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",
|
||||
"label": "Transition Effect",
|
||||
@@ -4536,6 +4563,7 @@
|
||||
"appearance",
|
||||
"colors",
|
||||
"controls",
|
||||
"floating",
|
||||
"look",
|
||||
"modal",
|
||||
"modals",
|
||||
@@ -4548,9 +4576,10 @@
|
||||
"surface",
|
||||
"surfaces",
|
||||
"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",
|
||||
@@ -5453,6 +5482,29 @@
|
||||
"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",
|
||||
"label": "Show Media Player",
|
||||
|
||||
@@ -4277,9 +4277,9 @@
|
||||
"comment": ""
|
||||
},
|
||||
{
|
||||
"term": "Controls opacity of shell surfaces, popouts, and modals",
|
||||
"term": "Controls opacity of shell surfaces, popouts, modals, and floating windows",
|
||||
"translation": "",
|
||||
"context": "Controls opacity of shell surfaces, popouts, and modals",
|
||||
"context": "Surface Opacity setting description including floating DMS windows",
|
||||
"reference": "",
|
||||
"comment": ""
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user