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

Compare commits

..

15 Commits

Author SHA1 Message Date
purian23 8d94117a69 Cleanup 2026-06-12 10:56:16 -04:00
purian23 92569d8b4d Refactor connected chrome rendering & remove legacy components 2026-06-12 10:56:16 -04:00
purian23 fdee09b583 refactor: enhance plugin visibility w/bar reveal state 2026-06-12 10:56:16 -04:00
purian23 b60af507d7 refactor: implement keyboard focus management 2026-06-12 10:56:16 -04:00
purian23 2cc12b70d2 Update frameBlur performance 2026-06-12 10:56:15 -04:00
purian23 2df1dfe0bd Refactor shadow handling & improve connected chrome rendering 2026-06-12 10:56:15 -04:00
purian23 abf084eea2 refactor(framemode): connected surfaces 2026-06-12 10:56:15 -04:00
Klesh Wong 08fd6e26d8 feat(notifications): user-configurable font size for notification summary and body (#2461)
* feat(notifications): add user-configurable font size for summary and body in notification popups

* feat: add Unset for falling back to previous default values

* fix: prek hook errors

---------

Co-authored-by: Klesh Wong <kleshwong@gmail.com>
2026-06-11 15:40:33 -04:00
Youseffo13 29e8470f2e fix(settings): fix text truncation in some section of settings and update icons (#2618)
* fixed spacing issues

* added one missing icon and replaced two
2026-06-11 15:35:51 -04:00
Bogdan Velicu 573785d4ce feat(notifications): add opt-in timeout progress bar on popups (#2587)
Adds a thin bar pinned to the bottom of the notification card that drains
full->empty over the auto-dismiss timer, as a visual countdown to
dismissal. Opt-in via notificationShowTimeoutBar (default off), with a
toggle in Settings > Notifications. Shown for any timed notification
(timer.interval > 0, including timed criticals); inset by the corner
radius, and frozen while hovered or during the exit animation. Plain
Rectangle - no offscreen textures or shader passes. A Connections on the
timer resets the bar on every (re)start, including the in-place restart
on a deduped notification.

Co-authored-by: bogdan-velicu <hydrotech074@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:22:22 -04:00
jbwfu 5483303714 Fix/clipboard pinned recents dedupe (#2605)
* fix(clipboard): unpin pinned duplicates from history entries

* fix(clipboard): dedupe recents when using pinned entries
2026-06-11 15:05:28 -04:00
David Mireles 5a5cc4f4e9 feat(plugins): expose scan/rescan/reload IPC handlers for runtime plugin discovery (#2611)
* feat(plugins): expose IPC handlers for runtime plugin discovery

Follow-up to #1659. That issue landed hot-reload for settings.json via
FileView.watchChanges + a 1ms Timer to skirt the JSON parse race. It does
not cover plugin discovery in runtime: adding a new plugin directory to
~/.config/DankMaterialShell/plugins/ while the shell is running is not
consistently picked up by the existing FolderListModel watcher in
PluginService.qml, and there is no IPC handle for forcing a rescan from
outside the shell.

Adds an IpcHandler on PluginService with five small functions:

- scan(): wraps existing scanPlugins(), returns count snapshot
- rescan(pluginId): wraps existing forceRescanPlugin(id), validates id
- reload(pluginId): wraps existing reloadPlugin(id), validates id
- list(): newline-joined id\tloaded\ttype\tname for every known plugin
- status(pluginId): loaded\ttype\terror for one plugin

Scope intentionally small: no file-watcher changes, no new daemons, no
schema additions. Target string "plugins" does not collide with any
existing target in DMSShellIPC.qml.

Validation:
- qs ipc --pid <PID> call plugins list returns one row per known plugin
- qs ipc --pid <PID> call plugins scan returns SCAN_TRIGGERED with count
- qs ipc --pid <PID> call plugins rescan <id> returns RESCAN_TRIGGERED
- Empty-arg paths return ERROR strings instead of throwing
- git merge-tree against origin/master is clean

* hardening(plugins): fix 7 review findings in scan-ipc IPC handlers

Follow-up to commit 43603f56 which ported PR #2601 (AvengeMedia scan-ipc)
to the fork. The original port was functionally correct but had seven
review issues that would block upstream adoption. This patch addresses
each one with a minimal, focused change.

* B1 IPC target collision: renamed `target: "plugins"` to
  `target: "plugin-scan"`. The original name collided with the
  existing IpcHandler in DMSShellIPC.qml:1180 which already registers
  enable/disable/toggle/list/status under "plugins". The split keeps
  both APIs discoverable without one shadowing the other.

* H1 Fire-and-forget scan: documented that scan() returns the
  pre-debounce count and that callers must poll list/status (or wait
  ~200ms) to observe the post-debounce state. A proper requestId +
  await mechanism was considered and rejected for scope reasons.

* H2 TOCTOU in rescan(): the handler now reads availablePlugins[id]
  inside forceRescanPlugin via the id string only — no captured
  object reference. A parallel resyncDebounce tick can otherwise
  mutate the entry between the read and the use.

* M1 list() cap: added a 256-entry cap and a leading header line
  (`# count=N returned=M`) so callers can detect truncation. A
  hostile / buggy plugin mass-creating entries could otherwise
  allocate 80 KB+ per IPC call.

* M2 status() prefix: "unknown\t\t" became
  `ERROR: unknown pluginId '...'` to match the rest of the
  handlers' prefix convention. Empty trailing field means no error.

* M3 id sanitization: every handler that takes pluginId now
  validates against `/^[a-zA-Z0-9_\-:]{1,64}$` before use. This
  rejects shell-injection payloads ("foo\tmalicious") and prototype
  pollution attempts ("__proto__", "constructor"). The list() and
  status() handlers also sanitize \t/\n in name and error fields
  so callers can rely on the TSV structure.

Verification: brace count balanced (252/252). Manual read of all
five handlers confirms no logic regression. QML runtime tests are
not part of the DMS test suite, so end-to-end validation requires
rebuilding the shell — deferred to the user.

Not pushed. Stage-local-first rule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(plugins): strip inline comments per review feedback

Purian23 in PR #2611 review: 'let's address the amount of line
comments in the code, there's not a need for all of them to exist.'

Removed 48 comment lines. The substantive justification (why the
regex, why fire-and-forget, why re-read inside forceRescanPlugin,
why the 256 cap, why the target rename) now lives in the PR body
under 'Review-driven fixes in this iteration' and 'What changed'
where the reviewer already reads it.

No code logic changed. Brace count 252/252. Diff is -48/+0 on
quickshell/Services/PluginService.qml.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------
2026-06-11 14:44:41 -04:00
bbedward cd672c341f settings: add DankSpinner, re-org some settings 2026-06-10 18:53:43 -04:00
bbedward 12438d63c2 mango: remove legacy dwl service 2026-06-10 17:01:03 -04:00
Ralph Zhou 35255e4053 fix(lock): bypass IME for password input (#2609) 2026-06-10 16:29:05 -04:00
123 changed files with 5915 additions and 9051 deletions
+2
View File
@@ -115,3 +115,5 @@ core.*
.direnv/ .direnv/
quickshell/dms-plugins quickshell/dms-plugins
__pycache__ __pycache__
.vscode/
+1 -1
View File
@@ -72,7 +72,7 @@ func runResolveInclude(cmd *cobra.Command, args []string) {
result, err = checkHyprlandInclude(filename) result, err = checkHyprlandInclude(filename)
case "niri": case "niri":
result, err = checkNiriInclude(filename) result, err = checkNiriInclude(filename)
case "mangowc", "dwl", "mango": case "mangowc", "mango":
result, err = checkMangoWCInclude(filename) result, err = checkMangoWCInclude(filename)
default: default:
log.Fatalf("Unknown compositor: %s", compositor) log.Fatalf("Unknown compositor: %s", compositor)
+2 -2
View File
@@ -39,7 +39,7 @@ Modes:
full - Capture the focused output full - Capture the focused output
all - Capture all outputs combined all - Capture all outputs combined
output - Capture a specific output by name output - Capture a specific output by name
window - Capture the focused window (Hyprland/DWL) window - Capture the focused window (Hyprland/Mango)
last - Capture the last selected region last - Capture the last selected region
Output format (--format): Output format (--format):
@@ -97,7 +97,7 @@ If no previous region exists, falls back to interactive selection.`,
var ssWindowCmd = &cobra.Command{ var ssWindowCmd = &cobra.Command{
Use: "window", Use: "window",
Short: "Capture the focused window", Short: "Capture the focused window",
Long: `Capture the currently focused window. Supported on Hyprland and DWL.`, Long: `Capture the currently focused window. Supported on Hyprland and Mango.`,
Run: runScreenshotWindow, Run: runScreenshotWindow,
} }
-791
View File
@@ -1,791 +0,0 @@
// Generated by go-wayland-scanner
// https://github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/cmd/go-wayland-scanner
// XML file : internal/proto/xml/dwl-ipc-unstable-v2.xml
//
// dwl_ipc_unstable_v2 Protocol Copyright:
package dwl_ipc
import "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
// ZdwlIpcManagerV2InterfaceName is the name of the interface as it appears in the [client.Registry].
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
const ZdwlIpcManagerV2InterfaceName = "zdwl_ipc_manager_v2"
// ZdwlIpcManagerV2 : manage dwl state
//
// This interface is exposed as a global in wl_registry.
//
// Clients can use this interface to get a dwl_ipc_output.
// After binding the client will recieve the dwl_ipc_manager.tags and dwl_ipc_manager.layout events.
// The dwl_ipc_manager.tags and dwl_ipc_manager.layout events expose tags and layouts to the client.
type ZdwlIpcManagerV2 struct {
client.BaseProxy
tagsHandler ZdwlIpcManagerV2TagsHandlerFunc
layoutHandler ZdwlIpcManagerV2LayoutHandlerFunc
}
// NewZdwlIpcManagerV2 : manage dwl state
//
// This interface is exposed as a global in wl_registry.
//
// Clients can use this interface to get a dwl_ipc_output.
// After binding the client will recieve the dwl_ipc_manager.tags and dwl_ipc_manager.layout events.
// The dwl_ipc_manager.tags and dwl_ipc_manager.layout events expose tags and layouts to the client.
func NewZdwlIpcManagerV2(ctx *client.Context) *ZdwlIpcManagerV2 {
zdwlIpcManagerV2 := &ZdwlIpcManagerV2{}
ctx.Register(zdwlIpcManagerV2)
return zdwlIpcManagerV2
}
// Release : release dwl_ipc_manager
//
// Indicates that the client will not the dwl_ipc_manager object anymore.
// Objects created through this instance are not affected.
func (i *ZdwlIpcManagerV2) Release() error {
defer i.MarkZombie()
const opcode = 0
const _reqBufLen = 8
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// GetOutput : get a dwl_ipc_outout for a wl_output
//
// Get a dwl_ipc_outout for the specified wl_output.
func (i *ZdwlIpcManagerV2) GetOutput(output *client.Output) (*ZdwlIpcOutputV2, error) {
id := NewZdwlIpcOutputV2(i.Context())
const opcode = 1
const _reqBufLen = 8 + 4 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], id.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], output.ID())
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return id, err
}
// ZdwlIpcManagerV2TagsEvent : Announces tag amount
//
// This event is sent after binding.
// A roundtrip after binding guarantees the client recieved all tags.
type ZdwlIpcManagerV2TagsEvent struct {
Amount uint32
}
type ZdwlIpcManagerV2TagsHandlerFunc func(ZdwlIpcManagerV2TagsEvent)
// SetTagsHandler : sets handler for ZdwlIpcManagerV2TagsEvent
func (i *ZdwlIpcManagerV2) SetTagsHandler(f ZdwlIpcManagerV2TagsHandlerFunc) {
i.tagsHandler = f
}
// ZdwlIpcManagerV2LayoutEvent : Announces a layout
//
// This event is sent after binding.
// A roundtrip after binding guarantees the client recieved all layouts.
type ZdwlIpcManagerV2LayoutEvent struct {
Name string
}
type ZdwlIpcManagerV2LayoutHandlerFunc func(ZdwlIpcManagerV2LayoutEvent)
// SetLayoutHandler : sets handler for ZdwlIpcManagerV2LayoutEvent
func (i *ZdwlIpcManagerV2) SetLayoutHandler(f ZdwlIpcManagerV2LayoutHandlerFunc) {
i.layoutHandler = f
}
func (i *ZdwlIpcManagerV2) Dispatch(opcode uint32, fd int, data []byte) {
switch opcode {
case 0:
if i.tagsHandler == nil {
return
}
var e ZdwlIpcManagerV2TagsEvent
l := 0
e.Amount = client.Uint32(data[l : l+4])
l += 4
i.tagsHandler(e)
case 1:
if i.layoutHandler == nil {
return
}
var e ZdwlIpcManagerV2LayoutEvent
l := 0
nameLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
l += 4
e.Name = client.String(data[l : l+nameLen])
l += nameLen
i.layoutHandler(e)
}
}
// ZdwlIpcOutputV2InterfaceName is the name of the interface as it appears in the [client.Registry].
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
const ZdwlIpcOutputV2InterfaceName = "zdwl_ipc_output_v2"
// ZdwlIpcOutputV2 : control dwl output
//
// Observe and control a dwl output.
//
// Events are double-buffered:
// Clients should cache events and redraw when a dwl_ipc_output.frame event is sent.
//
// Request are not double-buffered:
// The compositor will update immediately upon request.
type ZdwlIpcOutputV2 struct {
client.BaseProxy
toggleVisibilityHandler ZdwlIpcOutputV2ToggleVisibilityHandlerFunc
activeHandler ZdwlIpcOutputV2ActiveHandlerFunc
tagHandler ZdwlIpcOutputV2TagHandlerFunc
layoutHandler ZdwlIpcOutputV2LayoutHandlerFunc
titleHandler ZdwlIpcOutputV2TitleHandlerFunc
appidHandler ZdwlIpcOutputV2AppidHandlerFunc
layoutSymbolHandler ZdwlIpcOutputV2LayoutSymbolHandlerFunc
frameHandler ZdwlIpcOutputV2FrameHandlerFunc
fullscreenHandler ZdwlIpcOutputV2FullscreenHandlerFunc
floatingHandler ZdwlIpcOutputV2FloatingHandlerFunc
xHandler ZdwlIpcOutputV2XHandlerFunc
yHandler ZdwlIpcOutputV2YHandlerFunc
widthHandler ZdwlIpcOutputV2WidthHandlerFunc
heightHandler ZdwlIpcOutputV2HeightHandlerFunc
lastLayerHandler ZdwlIpcOutputV2LastLayerHandlerFunc
kbLayoutHandler ZdwlIpcOutputV2KbLayoutHandlerFunc
keymodeHandler ZdwlIpcOutputV2KeymodeHandlerFunc
scalefactorHandler ZdwlIpcOutputV2ScalefactorHandlerFunc
}
// NewZdwlIpcOutputV2 : control dwl output
//
// Observe and control a dwl output.
//
// Events are double-buffered:
// Clients should cache events and redraw when a dwl_ipc_output.frame event is sent.
//
// Request are not double-buffered:
// The compositor will update immediately upon request.
func NewZdwlIpcOutputV2(ctx *client.Context) *ZdwlIpcOutputV2 {
zdwlIpcOutputV2 := &ZdwlIpcOutputV2{}
ctx.Register(zdwlIpcOutputV2)
return zdwlIpcOutputV2
}
// Release : release dwl_ipc_outout
//
// Indicates to that the client no longer needs this dwl_ipc_output.
func (i *ZdwlIpcOutputV2) Release() error {
defer i.MarkZombie()
const opcode = 0
const _reqBufLen = 8
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// SetTags : Set the active tags of this output
//
// tagmask: bitmask of the tags that should be set.
// toggleTagset: toggle the selected tagset, zero for invalid, nonzero for valid.
func (i *ZdwlIpcOutputV2) SetTags(tagmask, toggleTagset uint32) error {
const opcode = 1
const _reqBufLen = 8 + 4 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(tagmask))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(toggleTagset))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// SetClientTags : Set the tags of the focused client.
//
// The tags are updated as follows:
// new_tags = (current_tags AND and_tags) XOR xor_tags
func (i *ZdwlIpcOutputV2) SetClientTags(andTags, xorTags uint32) error {
const opcode = 2
const _reqBufLen = 8 + 4 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(andTags))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(xorTags))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// SetLayout : Set the layout of this output
//
// index: index of a layout recieved by dwl_ipc_manager.layout
func (i *ZdwlIpcOutputV2) SetLayout(index uint32) error {
const opcode = 3
const _reqBufLen = 8 + 4
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(index))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// Quit : Quit mango
// This request allows clients to instruct the compositor to quit mango.
func (i *ZdwlIpcOutputV2) Quit() error {
const opcode = 4
const _reqBufLen = 8
var _reqBuf [_reqBufLen]byte
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
err := i.Context().WriteMsg(_reqBuf[:], nil)
return err
}
// SendDispatch : Set the active tags of this output
//
// dispatch: dispatch name.
// arg1: arg1.
// arg2: arg2.
// arg3: arg3.
// arg4: arg4.
// arg5: arg5.
func (i *ZdwlIpcOutputV2) SendDispatch(dispatch, arg1, arg2, arg3, arg4, arg5 string) error {
const opcode = 5
dispatchLen := client.PaddedLen(len(dispatch) + 1)
arg1Len := client.PaddedLen(len(arg1) + 1)
arg2Len := client.PaddedLen(len(arg2) + 1)
arg3Len := client.PaddedLen(len(arg3) + 1)
arg4Len := client.PaddedLen(len(arg4) + 1)
arg5Len := client.PaddedLen(len(arg5) + 1)
_reqBufLen := 8 + (4 + dispatchLen) + (4 + arg1Len) + (4 + arg2Len) + (4 + arg3Len) + (4 + arg4Len) + (4 + arg5Len)
_reqBuf := make([]byte, _reqBufLen)
l := 0
client.PutUint32(_reqBuf[l:4], i.ID())
l += 4
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
l += 4
client.PutString(_reqBuf[l:l+(4+dispatchLen)], dispatch)
l += (4 + dispatchLen)
client.PutString(_reqBuf[l:l+(4+arg1Len)], arg1)
l += (4 + arg1Len)
client.PutString(_reqBuf[l:l+(4+arg2Len)], arg2)
l += (4 + arg2Len)
client.PutString(_reqBuf[l:l+(4+arg3Len)], arg3)
l += (4 + arg3Len)
client.PutString(_reqBuf[l:l+(4+arg4Len)], arg4)
l += (4 + arg4Len)
client.PutString(_reqBuf[l:l+(4+arg5Len)], arg5)
l += (4 + arg5Len)
err := i.Context().WriteMsg(_reqBuf, nil)
return err
}
type ZdwlIpcOutputV2TagState uint32
// ZdwlIpcOutputV2TagState :
const (
// ZdwlIpcOutputV2TagStateNone : no state
ZdwlIpcOutputV2TagStateNone ZdwlIpcOutputV2TagState = 0
// ZdwlIpcOutputV2TagStateActive : tag is active
ZdwlIpcOutputV2TagStateActive ZdwlIpcOutputV2TagState = 1
// ZdwlIpcOutputV2TagStateUrgent : tag has at least one urgent client
ZdwlIpcOutputV2TagStateUrgent ZdwlIpcOutputV2TagState = 2
)
func (e ZdwlIpcOutputV2TagState) Name() string {
switch e {
case ZdwlIpcOutputV2TagStateNone:
return "none"
case ZdwlIpcOutputV2TagStateActive:
return "active"
case ZdwlIpcOutputV2TagStateUrgent:
return "urgent"
default:
return ""
}
}
func (e ZdwlIpcOutputV2TagState) Value() string {
switch e {
case ZdwlIpcOutputV2TagStateNone:
return "0"
case ZdwlIpcOutputV2TagStateActive:
return "1"
case ZdwlIpcOutputV2TagStateUrgent:
return "2"
default:
return ""
}
}
func (e ZdwlIpcOutputV2TagState) String() string {
return e.Name() + "=" + e.Value()
}
// ZdwlIpcOutputV2ToggleVisibilityEvent : Toggle client visibilty
//
// Indicates the client should hide or show themselves.
// If the client is visible then hide, if hidden then show.
type ZdwlIpcOutputV2ToggleVisibilityEvent struct{}
type ZdwlIpcOutputV2ToggleVisibilityHandlerFunc func(ZdwlIpcOutputV2ToggleVisibilityEvent)
// SetToggleVisibilityHandler : sets handler for ZdwlIpcOutputV2ToggleVisibilityEvent
func (i *ZdwlIpcOutputV2) SetToggleVisibilityHandler(f ZdwlIpcOutputV2ToggleVisibilityHandlerFunc) {
i.toggleVisibilityHandler = f
}
// ZdwlIpcOutputV2ActiveEvent : Update the selected output.
//
// Indicates if the output is active. Zero is invalid, nonzero is valid.
type ZdwlIpcOutputV2ActiveEvent struct {
Active uint32
}
type ZdwlIpcOutputV2ActiveHandlerFunc func(ZdwlIpcOutputV2ActiveEvent)
// SetActiveHandler : sets handler for ZdwlIpcOutputV2ActiveEvent
func (i *ZdwlIpcOutputV2) SetActiveHandler(f ZdwlIpcOutputV2ActiveHandlerFunc) {
i.activeHandler = f
}
// ZdwlIpcOutputV2TagEvent : Update the state of a tag.
//
// Indicates that a tag has been updated.
type ZdwlIpcOutputV2TagEvent struct {
Tag uint32
State uint32
Clients uint32
Focused uint32
}
type ZdwlIpcOutputV2TagHandlerFunc func(ZdwlIpcOutputV2TagEvent)
// SetTagHandler : sets handler for ZdwlIpcOutputV2TagEvent
func (i *ZdwlIpcOutputV2) SetTagHandler(f ZdwlIpcOutputV2TagHandlerFunc) {
i.tagHandler = f
}
// ZdwlIpcOutputV2LayoutEvent : Update the layout.
//
// Indicates a new layout is selected.
type ZdwlIpcOutputV2LayoutEvent struct {
Layout uint32
}
type ZdwlIpcOutputV2LayoutHandlerFunc func(ZdwlIpcOutputV2LayoutEvent)
// SetLayoutHandler : sets handler for ZdwlIpcOutputV2LayoutEvent
func (i *ZdwlIpcOutputV2) SetLayoutHandler(f ZdwlIpcOutputV2LayoutHandlerFunc) {
i.layoutHandler = f
}
// ZdwlIpcOutputV2TitleEvent : Update the title.
//
// Indicates the title has changed.
type ZdwlIpcOutputV2TitleEvent struct {
Title string
}
type ZdwlIpcOutputV2TitleHandlerFunc func(ZdwlIpcOutputV2TitleEvent)
// SetTitleHandler : sets handler for ZdwlIpcOutputV2TitleEvent
func (i *ZdwlIpcOutputV2) SetTitleHandler(f ZdwlIpcOutputV2TitleHandlerFunc) {
i.titleHandler = f
}
// ZdwlIpcOutputV2AppidEvent : Update the appid.
//
// Indicates the appid has changed.
type ZdwlIpcOutputV2AppidEvent struct {
Appid string
}
type ZdwlIpcOutputV2AppidHandlerFunc func(ZdwlIpcOutputV2AppidEvent)
// SetAppidHandler : sets handler for ZdwlIpcOutputV2AppidEvent
func (i *ZdwlIpcOutputV2) SetAppidHandler(f ZdwlIpcOutputV2AppidHandlerFunc) {
i.appidHandler = f
}
// ZdwlIpcOutputV2LayoutSymbolEvent : Update the current layout symbol
//
// Indicates the layout has changed. Since layout symbols are dynamic.
// As opposed to the zdwl_ipc_manager.layout event, this should take precendence when displaying.
// You can ignore the zdwl_ipc_output.layout event.
type ZdwlIpcOutputV2LayoutSymbolEvent struct {
Layout string
}
type ZdwlIpcOutputV2LayoutSymbolHandlerFunc func(ZdwlIpcOutputV2LayoutSymbolEvent)
// SetLayoutSymbolHandler : sets handler for ZdwlIpcOutputV2LayoutSymbolEvent
func (i *ZdwlIpcOutputV2) SetLayoutSymbolHandler(f ZdwlIpcOutputV2LayoutSymbolHandlerFunc) {
i.layoutSymbolHandler = f
}
// ZdwlIpcOutputV2FrameEvent : The update sequence is done.
//
// Indicates that a sequence of status updates have finished and the client should redraw.
type ZdwlIpcOutputV2FrameEvent struct{}
type ZdwlIpcOutputV2FrameHandlerFunc func(ZdwlIpcOutputV2FrameEvent)
// SetFrameHandler : sets handler for ZdwlIpcOutputV2FrameEvent
func (i *ZdwlIpcOutputV2) SetFrameHandler(f ZdwlIpcOutputV2FrameHandlerFunc) {
i.frameHandler = f
}
// ZdwlIpcOutputV2FullscreenEvent : Update fullscreen status
//
// Indicates if the selected client on this output is fullscreen.
type ZdwlIpcOutputV2FullscreenEvent struct {
IsFullscreen uint32
}
type ZdwlIpcOutputV2FullscreenHandlerFunc func(ZdwlIpcOutputV2FullscreenEvent)
// SetFullscreenHandler : sets handler for ZdwlIpcOutputV2FullscreenEvent
func (i *ZdwlIpcOutputV2) SetFullscreenHandler(f ZdwlIpcOutputV2FullscreenHandlerFunc) {
i.fullscreenHandler = f
}
// ZdwlIpcOutputV2FloatingEvent : Update the floating status
//
// Indicates if the selected client on this output is floating.
type ZdwlIpcOutputV2FloatingEvent struct {
IsFloating uint32
}
type ZdwlIpcOutputV2FloatingHandlerFunc func(ZdwlIpcOutputV2FloatingEvent)
// SetFloatingHandler : sets handler for ZdwlIpcOutputV2FloatingEvent
func (i *ZdwlIpcOutputV2) SetFloatingHandler(f ZdwlIpcOutputV2FloatingHandlerFunc) {
i.floatingHandler = f
}
// ZdwlIpcOutputV2XEvent : Update the x coordinates
//
// Indicates if x coordinates of the selected client.
type ZdwlIpcOutputV2XEvent struct {
X int32
}
type ZdwlIpcOutputV2XHandlerFunc func(ZdwlIpcOutputV2XEvent)
// SetXHandler : sets handler for ZdwlIpcOutputV2XEvent
func (i *ZdwlIpcOutputV2) SetXHandler(f ZdwlIpcOutputV2XHandlerFunc) {
i.xHandler = f
}
// ZdwlIpcOutputV2YEvent : Update the y coordinates
//
// Indicates if y coordinates of the selected client.
type ZdwlIpcOutputV2YEvent struct {
Y int32
}
type ZdwlIpcOutputV2YHandlerFunc func(ZdwlIpcOutputV2YEvent)
// SetYHandler : sets handler for ZdwlIpcOutputV2YEvent
func (i *ZdwlIpcOutputV2) SetYHandler(f ZdwlIpcOutputV2YHandlerFunc) {
i.yHandler = f
}
// ZdwlIpcOutputV2WidthEvent : Update the width
//
// Indicates if width of the selected client.
type ZdwlIpcOutputV2WidthEvent struct {
Width int32
}
type ZdwlIpcOutputV2WidthHandlerFunc func(ZdwlIpcOutputV2WidthEvent)
// SetWidthHandler : sets handler for ZdwlIpcOutputV2WidthEvent
func (i *ZdwlIpcOutputV2) SetWidthHandler(f ZdwlIpcOutputV2WidthHandlerFunc) {
i.widthHandler = f
}
// ZdwlIpcOutputV2HeightEvent : Update the height
//
// Indicates if height of the selected client.
type ZdwlIpcOutputV2HeightEvent struct {
Height int32
}
type ZdwlIpcOutputV2HeightHandlerFunc func(ZdwlIpcOutputV2HeightEvent)
// SetHeightHandler : sets handler for ZdwlIpcOutputV2HeightEvent
func (i *ZdwlIpcOutputV2) SetHeightHandler(f ZdwlIpcOutputV2HeightHandlerFunc) {
i.heightHandler = f
}
// ZdwlIpcOutputV2LastLayerEvent : last map layer.
//
// last map layer.
type ZdwlIpcOutputV2LastLayerEvent struct {
LastLayer string
}
type ZdwlIpcOutputV2LastLayerHandlerFunc func(ZdwlIpcOutputV2LastLayerEvent)
// SetLastLayerHandler : sets handler for ZdwlIpcOutputV2LastLayerEvent
func (i *ZdwlIpcOutputV2) SetLastLayerHandler(f ZdwlIpcOutputV2LastLayerHandlerFunc) {
i.lastLayerHandler = f
}
// ZdwlIpcOutputV2KbLayoutEvent : current keyboard layout.
//
// current keyboard layout.
type ZdwlIpcOutputV2KbLayoutEvent struct {
KbLayout string
}
type ZdwlIpcOutputV2KbLayoutHandlerFunc func(ZdwlIpcOutputV2KbLayoutEvent)
// SetKbLayoutHandler : sets handler for ZdwlIpcOutputV2KbLayoutEvent
func (i *ZdwlIpcOutputV2) SetKbLayoutHandler(f ZdwlIpcOutputV2KbLayoutHandlerFunc) {
i.kbLayoutHandler = f
}
// ZdwlIpcOutputV2KeymodeEvent : current keybind mode.
//
// current keybind mode.
type ZdwlIpcOutputV2KeymodeEvent struct {
Keymode string
}
type ZdwlIpcOutputV2KeymodeHandlerFunc func(ZdwlIpcOutputV2KeymodeEvent)
// SetKeymodeHandler : sets handler for ZdwlIpcOutputV2KeymodeEvent
func (i *ZdwlIpcOutputV2) SetKeymodeHandler(f ZdwlIpcOutputV2KeymodeHandlerFunc) {
i.keymodeHandler = f
}
// ZdwlIpcOutputV2ScalefactorEvent : scale factor of monitor.
//
// scale factor of monitor.
type ZdwlIpcOutputV2ScalefactorEvent struct {
Scalefactor uint32
}
type ZdwlIpcOutputV2ScalefactorHandlerFunc func(ZdwlIpcOutputV2ScalefactorEvent)
// SetScalefactorHandler : sets handler for ZdwlIpcOutputV2ScalefactorEvent
func (i *ZdwlIpcOutputV2) SetScalefactorHandler(f ZdwlIpcOutputV2ScalefactorHandlerFunc) {
i.scalefactorHandler = f
}
func (i *ZdwlIpcOutputV2) Dispatch(opcode uint32, fd int, data []byte) {
switch opcode {
case 0:
if i.toggleVisibilityHandler == nil {
return
}
var e ZdwlIpcOutputV2ToggleVisibilityEvent
i.toggleVisibilityHandler(e)
case 1:
if i.activeHandler == nil {
return
}
var e ZdwlIpcOutputV2ActiveEvent
l := 0
e.Active = client.Uint32(data[l : l+4])
l += 4
i.activeHandler(e)
case 2:
if i.tagHandler == nil {
return
}
var e ZdwlIpcOutputV2TagEvent
l := 0
e.Tag = client.Uint32(data[l : l+4])
l += 4
e.State = client.Uint32(data[l : l+4])
l += 4
e.Clients = client.Uint32(data[l : l+4])
l += 4
e.Focused = client.Uint32(data[l : l+4])
l += 4
i.tagHandler(e)
case 3:
if i.layoutHandler == nil {
return
}
var e ZdwlIpcOutputV2LayoutEvent
l := 0
e.Layout = client.Uint32(data[l : l+4])
l += 4
i.layoutHandler(e)
case 4:
if i.titleHandler == nil {
return
}
var e ZdwlIpcOutputV2TitleEvent
l := 0
titleLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
l += 4
e.Title = client.String(data[l : l+titleLen])
l += titleLen
i.titleHandler(e)
case 5:
if i.appidHandler == nil {
return
}
var e ZdwlIpcOutputV2AppidEvent
l := 0
appidLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
l += 4
e.Appid = client.String(data[l : l+appidLen])
l += appidLen
i.appidHandler(e)
case 6:
if i.layoutSymbolHandler == nil {
return
}
var e ZdwlIpcOutputV2LayoutSymbolEvent
l := 0
layoutLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
l += 4
e.Layout = client.String(data[l : l+layoutLen])
l += layoutLen
i.layoutSymbolHandler(e)
case 7:
if i.frameHandler == nil {
return
}
var e ZdwlIpcOutputV2FrameEvent
i.frameHandler(e)
case 8:
if i.fullscreenHandler == nil {
return
}
var e ZdwlIpcOutputV2FullscreenEvent
l := 0
e.IsFullscreen = client.Uint32(data[l : l+4])
l += 4
i.fullscreenHandler(e)
case 9:
if i.floatingHandler == nil {
return
}
var e ZdwlIpcOutputV2FloatingEvent
l := 0
e.IsFloating = client.Uint32(data[l : l+4])
l += 4
i.floatingHandler(e)
case 10:
if i.xHandler == nil {
return
}
var e ZdwlIpcOutputV2XEvent
l := 0
e.X = int32(client.Uint32(data[l : l+4]))
l += 4
i.xHandler(e)
case 11:
if i.yHandler == nil {
return
}
var e ZdwlIpcOutputV2YEvent
l := 0
e.Y = int32(client.Uint32(data[l : l+4]))
l += 4
i.yHandler(e)
case 12:
if i.widthHandler == nil {
return
}
var e ZdwlIpcOutputV2WidthEvent
l := 0
e.Width = int32(client.Uint32(data[l : l+4]))
l += 4
i.widthHandler(e)
case 13:
if i.heightHandler == nil {
return
}
var e ZdwlIpcOutputV2HeightEvent
l := 0
e.Height = int32(client.Uint32(data[l : l+4]))
l += 4
i.heightHandler(e)
case 14:
if i.lastLayerHandler == nil {
return
}
var e ZdwlIpcOutputV2LastLayerEvent
l := 0
lastLayerLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
l += 4
e.LastLayer = client.String(data[l : l+lastLayerLen])
l += lastLayerLen
i.lastLayerHandler(e)
case 15:
if i.kbLayoutHandler == nil {
return
}
var e ZdwlIpcOutputV2KbLayoutEvent
l := 0
kbLayoutLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
l += 4
e.KbLayout = client.String(data[l : l+kbLayoutLen])
l += kbLayoutLen
i.kbLayoutHandler(e)
case 16:
if i.keymodeHandler == nil {
return
}
var e ZdwlIpcOutputV2KeymodeEvent
l := 0
keymodeLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
l += 4
e.Keymode = client.String(data[l : l+keymodeLen])
l += keymodeLen
i.keymodeHandler(e)
case 17:
if i.scalefactorHandler == nil {
return
}
var e ZdwlIpcOutputV2ScalefactorEvent
l := 0
e.Scalefactor = client.Uint32(data[l : l+4])
l += 4
i.scalefactorHandler(e)
}
}
@@ -0,0 +1,25 @@
package qmlchecks
import (
"os"
"regexp"
"strings"
"testing"
)
func TestLockScreenPasswordFieldBypassesTextInputIME(t *testing.T) {
data, err := os.ReadFile("../../../quickshell/Modules/Lock/LockScreenContent.qml")
if err != nil {
t.Fatalf("read lock screen QML: %v", err)
}
content := string(data)
textInputPasswordField := regexp.MustCompile(`(?s)TextInput\s*\{[^{}]*id:\s*passwordField`)
if textInputPasswordField.MatchString(content) {
t.Fatalf("passwordField must not be a TextInput because TextInput can route physical keyboard input through IME")
}
if !strings.Contains(content, "Keys.onPressed") || !strings.Contains(content, "event.text") {
t.Fatalf("passwordField should handle physical key text manually instead of relying on a text input control")
}
}
+107 -325
View File
@@ -6,7 +6,6 @@ import (
"os" "os"
"os/exec" "os/exec"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client" wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
@@ -19,9 +18,9 @@ const (
CompositorHyprland CompositorHyprland
CompositorSway CompositorSway
CompositorNiri CompositorNiri
CompositorDWL
CompositorScroll CompositorScroll
CompositorMiracle CompositorMiracle
CompositorMango
) )
var detectedCompositor Compositor = -1 var detectedCompositor Compositor = -1
@@ -36,8 +35,14 @@ func DetectCompositor() Compositor {
swaySocket := os.Getenv("SWAYSOCK") swaySocket := os.Getenv("SWAYSOCK")
scrollSocket := os.Getenv("SCROLLSOCK") scrollSocket := os.Getenv("SCROLLSOCK")
miracleSocket := os.Getenv("MIRACLESOCK") miracleSocket := os.Getenv("MIRACLESOCK")
mangoSocket := os.Getenv("MANGO_INSTANCE_SIGNATURE")
switch { switch {
case mangoSocket != "":
if _, err := os.Stat(mangoSocket); err == nil {
detectedCompositor = CompositorMango
return detectedCompositor
}
case niriSocket != "": case niriSocket != "":
if _, err := os.Stat(niriSocket); err == nil { if _, err := os.Stat(niriSocket); err == nil {
detectedCompositor = CompositorNiri detectedCompositor = CompositorNiri
@@ -63,66 +68,29 @@ func DetectCompositor() Compositor {
return detectedCompositor return detectedCompositor
} }
if detectDWLProtocol() {
detectedCompositor = CompositorDWL
return detectedCompositor
}
detectedCompositor = CompositorUnknown detectedCompositor = CompositorUnknown
return detectedCompositor return detectedCompositor
} }
func detectDWLProtocol() bool {
display, err := client.Connect("")
if err != nil {
return false
}
ctx := display.Context()
defer ctx.Close()
registry, err := display.GetRegistry()
if err != nil {
return false
}
found := false
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
if e.Interface == dwl_ipc.ZdwlIpcManagerV2InterfaceName {
found = true
}
})
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
return false
}
return found
}
func SetCompositorDWL() {
detectedCompositor = CompositorDWL
}
type WindowGeometry struct { type WindowGeometry struct {
X int32 X int32
Y int32 Y int32
Width int32 Width int32
Height int32 Height int32
Output string Output string
Scale float64 Scale float64
OutputX int32 OutputX int32
OutputY int32 OutputY int32
OutputTransform int32
} }
func GetActiveWindow() (*WindowGeometry, error) { func GetActiveWindow() (*WindowGeometry, error) {
switch DetectCompositor() { switch DetectCompositor() {
case CompositorHyprland: case CompositorHyprland:
return getHyprlandActiveWindow() return getHyprlandActiveWindow()
case CompositorDWL: case CompositorMango:
return getDWLActiveWindow() return getMangoActiveWindow()
default: default:
return nil, fmt.Errorf("window capture requires Hyprland or DWL") return nil, fmt.Errorf("window capture requires Hyprland or Mango")
} }
} }
@@ -285,6 +253,93 @@ func getMiracleFocusedMonitor() string {
return "" return ""
} }
type mangoMonitor struct {
Name string `json:"name"`
Active bool `json:"active"`
X int32 `json:"x"`
Y int32 `json:"y"`
Scale float64 `json:"scale"`
}
func getMangoMonitors() []mangoMonitor {
output, err := exec.Command("mmsg", "get", "all-monitors").Output()
if err != nil {
return nil
}
var data struct {
Monitors []mangoMonitor `json:"monitors"`
}
if err := json.Unmarshal(output, &data); err != nil {
return nil
}
return data.Monitors
}
func getMangoFocusedMonitor() string {
for _, m := range getMangoMonitors() {
if m.Active {
return m.Name
}
}
return ""
}
type mangoClient struct {
Monitor string `json:"monitor"`
IsFocused bool `json:"is_focused"`
X int32 `json:"x"`
Y int32 `json:"y"`
Width int32 `json:"width"`
Height int32 `json:"height"`
}
func getMangoActiveWindow() (*WindowGeometry, error) {
output, err := exec.Command("mmsg", "get", "all-clients").Output()
if err != nil {
return nil, fmt.Errorf("mmsg get all-clients: %w", err)
}
var data struct {
Clients []mangoClient `json:"clients"`
}
if err := json.Unmarshal(output, &data); err != nil {
return nil, fmt.Errorf("parse all-clients: %w", err)
}
for _, c := range data.Clients {
if !c.IsFocused {
continue
}
if c.Width <= 0 || c.Height <= 0 {
return nil, fmt.Errorf("no active window")
}
geom := &WindowGeometry{
X: c.X,
Y: c.Y,
Width: c.Width,
Height: c.Height,
Output: c.Monitor,
Scale: 1.0,
}
for _, m := range getMangoMonitors() {
if m.Name != c.Monitor {
continue
}
geom.OutputX = m.X
geom.OutputY = m.Y
if m.Scale > 0 {
geom.Scale = m.Scale
}
break
}
return geom, nil
}
return nil, fmt.Errorf("no focused window")
}
type niriWorkspace struct { type niriWorkspace struct {
Output string `json:"output"` Output string `json:"output"`
IsFocused bool `json:"is_focused"` IsFocused bool `json:"is_focused"`
@@ -309,121 +364,6 @@ func getNiriFocusedMonitor() string {
return "" return ""
} }
var dwlActiveOutput string
func SetDWLActiveOutput(name string) {
dwlActiveOutput = name
}
func getDWLFocusedMonitor() string {
if dwlActiveOutput != "" {
return dwlActiveOutput
}
return queryDWLActiveOutput()
}
func queryDWLActiveOutput() string {
display, err := client.Connect("")
if err != nil {
return ""
}
ctx := display.Context()
defer ctx.Close()
registry, err := display.GetRegistry()
if err != nil {
return ""
}
var dwlManager *dwl_ipc.ZdwlIpcManagerV2
outputs := make(map[uint32]*client.Output)
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
switch e.Interface {
case dwl_ipc.ZdwlIpcManagerV2InterfaceName:
mgr := dwl_ipc.NewZdwlIpcManagerV2(ctx)
if err := registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil {
dwlManager = mgr
}
case client.OutputInterfaceName:
out := client.NewOutput(ctx)
version := e.Version
if version > 4 {
version = 4
}
if err := registry.Bind(e.Name, e.Interface, version, out); err == nil {
outputs[e.Name] = out
}
}
})
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
return ""
}
if dwlManager == nil || len(outputs) == 0 {
return ""
}
outputNames := make(map[uint32]string)
for name, out := range outputs {
n := name
out.SetNameHandler(func(e client.OutputNameEvent) {
outputNames[n] = e.Name
})
}
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
return ""
}
type outputState struct {
name string
active bool
gotFrame bool
}
states := make(map[uint32]*outputState)
for name, out := range outputs {
dwlOut, err := dwlManager.GetOutput(out)
if err != nil {
continue
}
state := &outputState{name: outputNames[name]}
states[name] = state
dwlOut.SetActiveHandler(func(e dwl_ipc.ZdwlIpcOutputV2ActiveEvent) {
state.active = e.Active != 0
})
dwlOut.SetFrameHandler(func(e dwl_ipc.ZdwlIpcOutputV2FrameEvent) {
state.gotFrame = true
})
}
allFramesReceived := func() bool {
for _, s := range states {
if !s.gotFrame {
return false
}
}
return true
}
for !allFramesReceived() {
if err := ctx.Dispatch(); err != nil {
return ""
}
}
for _, state := range states {
if state.active {
return state.name
}
}
return ""
}
func GetFocusedMonitor() string { func GetFocusedMonitor() string {
switch DetectCompositor() { switch DetectCompositor() {
case CompositorHyprland: case CompositorHyprland:
@@ -436,8 +376,8 @@ func GetFocusedMonitor() string {
return getMiracleFocusedMonitor() return getMiracleFocusedMonitor()
case CompositorNiri: case CompositorNiri:
return getNiriFocusedMonitor() return getNiriFocusedMonitor()
case CompositorDWL: case CompositorMango:
return getDWLFocusedMonitor() return getMangoFocusedMonitor()
} }
return "" return ""
} }
@@ -534,161 +474,3 @@ func getAllOutputInfos() map[string]*outputInfo {
} }
return result return result
} }
func getOutputInfo(outputName string) (*outputInfo, bool) {
infos := getAllOutputInfos()
if infos == nil {
return nil, false
}
info, ok := infos[outputName]
return info, ok
}
func getDWLActiveWindow() (*WindowGeometry, error) {
display, err := client.Connect("")
if err != nil {
return nil, fmt.Errorf("connect: %w", err)
}
ctx := display.Context()
defer ctx.Close()
registry, err := display.GetRegistry()
if err != nil {
return nil, fmt.Errorf("get registry: %w", err)
}
var dwlManager *dwl_ipc.ZdwlIpcManagerV2
outputs := make(map[uint32]*client.Output)
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
switch e.Interface {
case dwl_ipc.ZdwlIpcManagerV2InterfaceName:
mgr := dwl_ipc.NewZdwlIpcManagerV2(ctx)
if err := registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil {
dwlManager = mgr
}
case client.OutputInterfaceName:
out := client.NewOutput(ctx)
version := e.Version
if version > 4 {
version = 4
}
if err := registry.Bind(e.Name, e.Interface, version, out); err == nil {
outputs[e.Name] = out
}
}
})
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
return nil, fmt.Errorf("roundtrip: %w", err)
}
if dwlManager == nil {
return nil, fmt.Errorf("dwl_ipc_manager not available")
}
if len(outputs) == 0 {
return nil, fmt.Errorf("no outputs found")
}
outputNames := make(map[uint32]string)
for name, out := range outputs {
n := name
out.SetNameHandler(func(e client.OutputNameEvent) {
outputNames[n] = e.Name
})
}
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
return nil, fmt.Errorf("roundtrip: %w", err)
}
type dwlOutputState struct {
output *dwl_ipc.ZdwlIpcOutputV2
name string
active bool
x, y int32
w, h int32
scalefactor uint32
gotFrame bool
}
dwlOutputs := make(map[uint32]*dwlOutputState)
for name, out := range outputs {
dwlOut, err := dwlManager.GetOutput(out)
if err != nil {
continue
}
state := &dwlOutputState{output: dwlOut, name: outputNames[name]}
dwlOutputs[name] = state
dwlOut.SetActiveHandler(func(e dwl_ipc.ZdwlIpcOutputV2ActiveEvent) {
state.active = e.Active != 0
})
dwlOut.SetXHandler(func(e dwl_ipc.ZdwlIpcOutputV2XEvent) {
state.x = e.X
})
dwlOut.SetYHandler(func(e dwl_ipc.ZdwlIpcOutputV2YEvent) {
state.y = e.Y
})
dwlOut.SetWidthHandler(func(e dwl_ipc.ZdwlIpcOutputV2WidthEvent) {
state.w = e.Width
})
dwlOut.SetHeightHandler(func(e dwl_ipc.ZdwlIpcOutputV2HeightEvent) {
state.h = e.Height
})
dwlOut.SetScalefactorHandler(func(e dwl_ipc.ZdwlIpcOutputV2ScalefactorEvent) {
state.scalefactor = e.Scalefactor
})
dwlOut.SetFrameHandler(func(e dwl_ipc.ZdwlIpcOutputV2FrameEvent) {
state.gotFrame = true
})
}
allFramesReceived := func() bool {
for _, s := range dwlOutputs {
if !s.gotFrame {
return false
}
}
return true
}
for !allFramesReceived() {
if err := ctx.Dispatch(); err != nil {
return nil, fmt.Errorf("dispatch: %w", err)
}
}
for _, state := range dwlOutputs {
if !state.active {
continue
}
if state.w <= 0 || state.h <= 0 {
return nil, fmt.Errorf("no active window")
}
scale := float64(state.scalefactor) / 100.0
if scale <= 0 {
scale = 1.0
}
geom := &WindowGeometry{
X: state.x,
Y: state.y,
Width: state.w,
Height: state.h,
Output: state.name,
Scale: scale,
}
if info, ok := getOutputInfo(state.name); ok {
geom.OutputX = info.x
geom.OutputY = info.y
geom.OutputTransform = info.transform
}
return geom, nil
}
return nil, fmt.Errorf("no active output found")
}
+4 -4
View File
@@ -156,14 +156,14 @@ func (s *Screenshoter) captureWindow() (*CaptureResult, error) {
switch DetectCompositor() { switch DetectCompositor() {
case CompositorHyprland: case CompositorHyprland:
return s.captureAndCrop(output, region) return s.captureAndCrop(output, region)
case CompositorDWL: case CompositorMango:
return s.captureDWLWindow(output, region, geom) return s.captureMangoWindow(output, region, geom)
default: default:
return s.captureRegionOnOutput(output, region) return s.captureRegionOnOutput(output, region)
} }
} }
func (s *Screenshoter) captureDWLWindow(output *WaylandOutput, region Region, geom *WindowGeometry) (*CaptureResult, error) { func (s *Screenshoter) captureMangoWindow(output *WaylandOutput, region Region, geom *WindowGeometry) (*CaptureResult, error) {
result, err := s.captureWholeOutput(output) result, err := s.captureWholeOutput(output)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -628,7 +628,7 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio
w := int32(float64(region.Width) * scale) w := int32(float64(region.Width) * scale)
h := int32(float64(region.Height) * scale) h := int32(float64(region.Height) * scale)
if DetectCompositor() == CompositorDWL { if DetectCompositor() == CompositorMango {
scaledOutW := int32(float64(output.width) * scale) scaledOutW := int32(float64(output.width) * scale)
scaledOutH := int32(float64(output.height) * scale) scaledOutH := int32(float64(output.height) * scale)
if localX >= scaledOutW { if localX >= scaledOutW {
+32 -31
View File
@@ -935,7 +935,7 @@ func (m *Manager) CreateHistoryEntryFromPinned(pinnedEntry *Entry) error {
Pinned: false, Pinned: false,
} }
if err := m.storeEntryWithoutDedup(newEntry); err != nil { if err := m.storeEntry(newEntry); err != nil {
return err return err
} }
@@ -945,36 +945,6 @@ func (m *Manager) CreateHistoryEntryFromPinned(pinnedEntry *Entry) error {
return nil return nil
} }
func (m *Manager) storeEntryWithoutDedup(entry Entry) error {
if m.db == nil {
return fmt.Errorf("database not available")
}
entry.Hash = computeHash(entry.Data)
return m.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("clipboard"))
id, err := b.NextSequence()
if err != nil {
return err
}
entry.ID = id
encoded, err := encodeEntry(entry)
if err != nil {
return err
}
if err := b.Put(itob(id), encoded); err != nil {
return err
}
return m.trimLengthInTx(b)
})
}
func (m *Manager) ClearHistory() { func (m *Manager) ClearHistory() {
if m.db == nil { if m.db == nil {
return return
@@ -1653,6 +1623,37 @@ func (m *Manager) UnpinEntry(id uint64) error {
return err return err
} }
if entry.Pinned {
currentKey := itob(id)
var keepKey []byte
var deleteKeys [][]byte
c := b.Cursor()
for k, v := c.Last(); k != nil; k, v = c.Prev() {
if bytes.Equal(k, currentKey) || extractHash(v) != entry.Hash {
continue
}
duplicate, err := decodeEntryMeta(v)
if err == nil && !duplicate.Pinned {
key := append([]byte(nil), k...)
if keepKey == nil {
keepKey = key
} else {
deleteKeys = append(deleteKeys, key)
}
}
}
if keepKey != nil {
for _, key := range deleteKeys {
if err := b.Delete(key); err != nil {
return err
}
}
return b.Delete(currentKey)
}
}
entry.Pinned = false entry.Pinned = false
encoded, err := encodeEntry(entry) encoded, err := encodeEntry(entry)
if err != nil { if err != nil {
@@ -14,6 +14,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
bolt "go.etcd.io/bbolt"
mocks_wlcontext "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlcontext" mocks_wlcontext "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlcontext"
) )
@@ -273,6 +274,110 @@ func TestHandleGetEntry_MissingIDReturnsNullResult(t *testing.T) {
assert.Nil(t, resp.Result) assert.Nil(t, resp.Result)
} }
func TestUnpinEntry_KeepsTopUnpinnedDuplicate(t *testing.T) {
m := newTestManagerWithDB(t)
require.NoError(t, m.storeEntry(Entry{
Data: []byte("saved content"),
MimeType: "text/plain;charset=utf-8",
Preview: "saved content",
Size: len("saved content"),
Timestamp: time.Now().Add(-time.Minute).Truncate(time.Second),
IsImage: false,
}))
history := m.GetHistory()
require.Len(t, history, 1)
pinnedID := history[0].ID
require.NoError(t, m.PinEntry(pinnedID))
pinnedEntry, err := m.GetEntry(pinnedID)
require.NoError(t, err)
require.True(t, pinnedEntry.Pinned)
// Bypass storeEntry to simulate legacy duplicate ordinary history entries.
insertLegacyUnpinnedDuplicate := func(timestamp time.Time) Entry {
duplicate := Entry{
Data: pinnedEntry.Data,
MimeType: pinnedEntry.MimeType,
Preview: pinnedEntry.Preview,
Size: pinnedEntry.Size,
Timestamp: timestamp,
IsImage: pinnedEntry.IsImage,
Pinned: false,
}
duplicate.Hash = computeHash(duplicate.Data)
require.NoError(t, m.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("clipboard"))
id, err := b.NextSequence()
if err != nil {
return err
}
duplicate.ID = id
encoded, err := encodeEntry(duplicate)
if err != nil {
return err
}
return b.Put(itob(id), encoded)
}))
return duplicate
}
olderHistoryDuplicate := insertLegacyUnpinnedDuplicate(time.Now().Add(time.Hour))
topHistoryDuplicate := insertLegacyUnpinnedDuplicate(time.Now().Add(-time.Hour))
require.Greater(t, topHistoryDuplicate.ID, olderHistoryDuplicate.ID)
require.True(t, olderHistoryDuplicate.Timestamp.After(topHistoryDuplicate.Timestamp))
history = m.GetHistory()
require.Len(t, history, 3)
require.Equal(t, topHistoryDuplicate.ID, history[0].ID)
require.NoError(t, m.UnpinEntry(pinnedID))
history = m.GetHistory()
require.Len(t, history, 1)
assert.False(t, history[0].Pinned)
assert.Equal(t, pinnedEntry.Hash, history[0].Hash)
assert.Equal(t, topHistoryDuplicate.ID, history[0].ID)
}
func TestCreateHistoryEntryFromPinned_KeepsLatestUnpinnedDuplicate(t *testing.T) {
m := newTestManagerWithDB(t)
require.NoError(t, m.storeEntry(Entry{
Data: []byte("saved content"),
MimeType: "text/plain;charset=utf-8",
Preview: "saved content",
Size: len("saved content"),
Timestamp: time.Now().Add(-time.Minute).Truncate(time.Second),
IsImage: false,
}))
history := m.GetHistory()
require.Len(t, history, 1)
pinnedID := history[0].ID
require.NoError(t, m.PinEntry(pinnedID))
pinnedEntry, err := m.GetEntry(pinnedID)
require.NoError(t, err)
require.True(t, pinnedEntry.Pinned)
require.NoError(t, m.CreateHistoryEntryFromPinned(pinnedEntry))
firstDuplicate := m.GetHistory()[0]
require.NotEqual(t, pinnedID, firstDuplicate.ID)
require.NoError(t, m.CreateHistoryEntryFromPinned(pinnedEntry))
latestDuplicate := m.GetHistory()[0]
history = m.GetHistory()
require.Len(t, history, 2)
assert.Equal(t, latestDuplicate.ID, history[0].ID)
assert.False(t, history[0].Pinned)
assert.Equal(t, pinnedID, history[1].ID)
assert.True(t, history[1].Pinned)
assert.NotEqual(t, firstDuplicate.ID, latestDuplicate.ID)
}
func TestManager_ConcurrentSubscriberAccess(t *testing.T) { func TestManager_ConcurrentSubscriberAccess(t *testing.T) {
m := &Manager{ m := &Manager{
subscribers: make(map[string]chan State), subscribers: make(map[string]chan State),
-138
View File
@@ -1,138 +0,0 @@
package dwl
import (
"encoding/json"
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
type SuccessResult struct {
Success bool `json:"success"`
Message string `json:"message"`
}
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
if manager == nil {
models.RespondError(conn, req.ID, "dwl manager not initialized")
return
}
switch req.Method {
case "dwl.getState":
handleGetState(conn, req, manager)
case "dwl.setTags":
handleSetTags(conn, req, manager)
case "dwl.setClientTags":
handleSetClientTags(conn, req, manager)
case "dwl.setLayout":
handleSetLayout(conn, req, manager)
case "dwl.subscribe":
handleSubscribe(conn, req, manager)
default:
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
}
}
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
state := manager.GetState()
models.Respond(conn, req.ID, state)
}
func handleSetTags(conn net.Conn, req models.Request, manager *Manager) {
output, ok := models.Get[string](req, "output")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'output' parameter")
return
}
tagmask, ok := models.Get[float64](req, "tagmask")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'tagmask' parameter")
return
}
toggleTagset, ok := models.Get[float64](req, "toggleTagset")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'toggleTagset' parameter")
return
}
if err := manager.SetTags(output, uint32(tagmask), uint32(toggleTagset)); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "tags set"})
}
func handleSetClientTags(conn net.Conn, req models.Request, manager *Manager) {
output, ok := models.Get[string](req, "output")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'output' parameter")
return
}
andTags, ok := models.Get[float64](req, "andTags")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'andTags' parameter")
return
}
xorTags, ok := models.Get[float64](req, "xorTags")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'xorTags' parameter")
return
}
if err := manager.SetClientTags(output, uint32(andTags), uint32(xorTags)); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "client tags set"})
}
func handleSetLayout(conn net.Conn, req models.Request, manager *Manager) {
output, ok := models.Get[string](req, "output")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'output' parameter")
return
}
index, ok := models.Get[float64](req, "index")
if !ok {
models.RespondError(conn, req.ID, "missing or invalid 'index' parameter")
return
}
if err := manager.SetLayout(output, uint32(index)); err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "layout set"})
}
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID)
initialState := manager.GetState()
if err := json.NewEncoder(conn).Encode(models.Response[State]{
ID: req.ID,
Result: &initialState,
}); err != nil {
return
}
for state := range stateChan {
if err := json.NewEncoder(conn).Encode(models.Response[State]{
Result: &state,
}); err != nil {
return
}
}
}
-522
View File
@@ -1,522 +0,0 @@
package dwl
import (
"fmt"
"time"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc"
)
func NewManager(display wlclient.WaylandDisplay) (*Manager, error) {
m := &Manager{
display: display,
ctx: display.Context(),
cmdq: make(chan cmd, 128),
outputSetupReq: make(chan uint32, 16),
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
layouts: make([]string, 0),
}
if err := m.setupRegistry(); err != nil {
return nil, err
}
m.updateState()
m.notifierWg.Add(1)
go m.notifier()
m.wg.Add(1)
go m.waylandActor()
return m, nil
}
func (m *Manager) post(fn func()) {
select {
case m.cmdq <- cmd{fn: fn}:
default:
log.Warn("DWL actor command queue full, dropping command")
}
}
func (m *Manager) waylandActor() {
defer m.wg.Done()
for {
select {
case <-m.stopChan:
return
case c := <-m.cmdq:
c.fn()
case outputID := <-m.outputSetupReq:
out, exists := m.outputs.Load(outputID)
if !exists {
log.Warnf("DWL: Output %d no longer exists, skipping setup", outputID)
continue
}
if out.ipcOutput != nil {
continue
}
mgr, ok := m.manager.(*dwl_ipc.ZdwlIpcManagerV2)
if !ok || mgr == nil {
log.Errorf("DWL: Manager not available for output %d setup", outputID)
continue
}
log.Infof("DWL: Setting up ipcOutput for dynamically added output %d", outputID)
if err := m.setupOutput(mgr, out.output); err != nil {
log.Errorf("DWL: Failed to setup output %d: %v", outputID, err)
} else {
m.updateState()
}
}
}
}
func (m *Manager) setupRegistry() error {
log.Info("DWL: starting registry setup")
registry, err := m.display.GetRegistry()
if err != nil {
return fmt.Errorf("failed to get registry: %w", err)
}
m.registry = registry
outputs := make([]*wlclient.Output, 0)
outputRegNames := make(map[uint32]uint32)
var dwlMgr *dwl_ipc.ZdwlIpcManagerV2
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
switch e.Interface {
case dwl_ipc.ZdwlIpcManagerV2InterfaceName:
log.Infof("DWL: found %s", dwl_ipc.ZdwlIpcManagerV2InterfaceName)
manager := dwl_ipc.NewZdwlIpcManagerV2(m.ctx)
version := e.Version
if version > 2 {
version = 2
}
if err := registry.Bind(e.Name, e.Interface, version, manager); err == nil {
dwlMgr = manager
log.Info("DWL: manager bound successfully")
// Set handlers immediately after binding, before roundtrips
manager.SetTagsHandler(func(e dwl_ipc.ZdwlIpcManagerV2TagsEvent) {
log.Infof("DWL: Tags count: %d", e.Amount)
m.tagCount = e.Amount
m.updateState()
})
manager.SetLayoutHandler(func(e dwl_ipc.ZdwlIpcManagerV2LayoutEvent) {
log.Infof("DWL: Layout: %s", e.Name)
m.layouts = append(m.layouts, e.Name)
m.updateState()
})
} else {
log.Errorf("DWL: failed to bind manager: %v", err)
}
case "wl_output":
log.Debugf("DWL: found wl_output (name=%d)", e.Name)
output := wlclient.NewOutput(m.ctx)
outState := &outputState{
registryName: e.Name,
output: output,
tags: make([]TagState, 0),
}
output.SetNameHandler(func(ev wlclient.OutputNameEvent) {
log.Debugf("DWL: Output name: %s (registry=%d)", ev.Name, e.Name)
outState.name = ev.Name
})
output.SetDescriptionHandler(func(ev wlclient.OutputDescriptionEvent) {
log.Debugf("DWL: Output description: %s", ev.Description)
})
version := e.Version
if version > 4 {
version = 4
}
if err := registry.Bind(e.Name, e.Interface, version, output); err == nil {
outputID := output.ID()
outState.id = outputID
log.Infof("DWL: Bound wl_output id=%d registry_name=%d", outputID, e.Name)
outputs = append(outputs, output)
outputRegNames[outputID] = e.Name
m.outputs.Store(outputID, outState)
if m.manager != nil {
select {
case m.outputSetupReq <- outputID:
log.Debugf("DWL: Queued setup for output %d", outputID)
default:
log.Warnf("DWL: Setup queue full, output %d will not be initialized", outputID)
}
}
} else {
log.Errorf("DWL: Failed to bind wl_output: %v", err)
}
}
})
registry.SetGlobalRemoveHandler(func(e wlclient.RegistryGlobalRemoveEvent) {
m.post(func() {
var outToRelease *outputState
m.outputs.Range(func(id uint32, out *outputState) bool {
if out.registryName == e.Name {
log.Infof("DWL: Output %d removed", id)
outToRelease = out
m.outputs.Delete(id)
return false
}
return true
})
if outToRelease != nil {
if ipcOut, ok := outToRelease.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2); ok && ipcOut != nil {
m.wlMutex.Lock()
ipcOut.Release()
m.wlMutex.Unlock()
log.Debugf("DWL: Released ipcOutput for removed output %d", outToRelease.id)
}
m.updateState()
}
})
})
if err := m.display.Roundtrip(); err != nil {
return fmt.Errorf("first roundtrip failed: %w", err)
}
if err := m.display.Roundtrip(); err != nil {
return fmt.Errorf("second roundtrip failed: %w", err)
}
if dwlMgr == nil {
log.Info("DWL: manager not found in registry")
return fmt.Errorf("dwl_ipc_manager_v2 not available")
}
m.manager = dwlMgr
for _, output := range outputs {
if err := m.setupOutput(dwlMgr, output); err != nil {
log.Warnf("DWL: Failed to setup output %d: %v", output.ID(), err)
}
}
if err := m.display.Roundtrip(); err != nil {
return fmt.Errorf("final roundtrip failed: %w", err)
}
log.Info("DWL: registry setup complete")
return nil
}
func (m *Manager) setupOutput(manager *dwl_ipc.ZdwlIpcManagerV2, output *wlclient.Output) error {
m.wlMutex.Lock()
ipcOutput, err := manager.GetOutput(output)
m.wlMutex.Unlock()
if err != nil {
return fmt.Errorf("failed to get dwl output: %w", err)
}
outState, exists := m.outputs.Load(output.ID())
if !exists {
return fmt.Errorf("output state not found for id %d", output.ID())
}
outState.ipcOutput = ipcOutput
ipcOutput.SetActiveHandler(func(e dwl_ipc.ZdwlIpcOutputV2ActiveEvent) {
outState.active = e.Active
})
ipcOutput.SetTagHandler(func(e dwl_ipc.ZdwlIpcOutputV2TagEvent) {
updated := false
for i, tag := range outState.tags {
if tag.Tag == e.Tag {
outState.tags[i] = TagState{
Tag: e.Tag,
State: e.State,
Clients: e.Clients,
Focused: e.Focused,
}
updated = true
break
}
}
if !updated {
outState.tags = append(outState.tags, TagState{
Tag: e.Tag,
State: e.State,
Clients: e.Clients,
Focused: e.Focused,
})
}
m.updateState()
})
ipcOutput.SetLayoutHandler(func(e dwl_ipc.ZdwlIpcOutputV2LayoutEvent) {
outState.layout = e.Layout
})
ipcOutput.SetTitleHandler(func(e dwl_ipc.ZdwlIpcOutputV2TitleEvent) {
outState.title = e.Title
})
ipcOutput.SetAppidHandler(func(e dwl_ipc.ZdwlIpcOutputV2AppidEvent) {
outState.appID = e.Appid
})
ipcOutput.SetLayoutSymbolHandler(func(e dwl_ipc.ZdwlIpcOutputV2LayoutSymbolEvent) {
outState.layoutSymbol = e.Layout
})
ipcOutput.SetKbLayoutHandler(func(e dwl_ipc.ZdwlIpcOutputV2KbLayoutEvent) {
outState.kbLayout = e.KbLayout
})
ipcOutput.SetKeymodeHandler(func(e dwl_ipc.ZdwlIpcOutputV2KeymodeEvent) {
outState.keymode = e.Keymode
})
ipcOutput.SetFrameHandler(func(e dwl_ipc.ZdwlIpcOutputV2FrameEvent) {
m.updateState()
})
return nil
}
func (m *Manager) updateState() {
outputs := make(map[string]*OutputState)
activeOutput := ""
m.outputs.Range(func(key uint32, out *outputState) bool {
name := out.name
if name == "" {
name = fmt.Sprintf("output-%d", out.id)
}
tagsCopy := make([]TagState, len(out.tags))
copy(tagsCopy, out.tags)
outputs[name] = &OutputState{
Name: name,
Active: out.active,
Tags: tagsCopy,
Layout: out.layout,
LayoutSymbol: out.layoutSymbol,
Title: out.title,
AppID: out.appID,
KbLayout: out.kbLayout,
Keymode: out.keymode,
}
if out.active != 0 {
activeOutput = name
}
return true
})
newState := State{
Outputs: outputs,
TagCount: m.tagCount,
Layouts: m.layouts,
ActiveOutput: activeOutput,
}
m.stateMutex.Lock()
m.state = &newState
m.stateMutex.Unlock()
m.notifySubscribers()
}
func (m *Manager) notifier() {
defer m.notifierWg.Done()
const minGap = 100 * time.Millisecond
timer := time.NewTimer(minGap)
timer.Stop()
var pending bool
for {
select {
case <-m.stopChan:
timer.Stop()
return
case <-m.dirty:
if pending {
continue
}
pending = true
timer.Reset(minGap)
case <-timer.C:
if !pending {
continue
}
currentState := m.GetState()
if m.lastNotified != nil && !stateChanged(m.lastNotified, &currentState) {
pending = false
continue
}
m.subscribers.Range(func(key string, ch chan State) bool {
select {
case ch <- currentState:
default:
log.Warn("DWL: subscriber channel full, dropping update")
}
return true
})
stateCopy := currentState
m.lastNotified = &stateCopy
pending = false
}
}
}
func (m *Manager) ensureOutputSetup(out *outputState) error {
if out.ipcOutput != nil {
return nil
}
return fmt.Errorf("output not yet initialized - setup in progress, retry in a moment")
}
func (m *Manager) SetTags(outputName string, tagmask uint32, toggleTagset uint32) error {
availableOutputs := make([]string, 0)
var targetOut *outputState
m.outputs.Range(func(key uint32, out *outputState) bool {
name := out.name
if name == "" {
name = fmt.Sprintf("output-%d", out.id)
}
availableOutputs = append(availableOutputs, name)
if name == outputName {
targetOut = out
return false
}
return true
})
if targetOut == nil {
return fmt.Errorf("output not found: %s (available: %v)", outputName, availableOutputs)
}
if err := m.ensureOutputSetup(targetOut); err != nil {
return fmt.Errorf("failed to setup output %s: %w", outputName, err)
}
ipcOut, ok := targetOut.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2)
if !ok {
return fmt.Errorf("output %s has invalid ipcOutput type", outputName)
}
m.wlMutex.Lock()
err := ipcOut.SetTags(tagmask, toggleTagset)
m.wlMutex.Unlock()
return err
}
func (m *Manager) SetClientTags(outputName string, andTags uint32, xorTags uint32) error {
var targetOut *outputState
m.outputs.Range(func(key uint32, out *outputState) bool {
name := out.name
if name == "" {
name = fmt.Sprintf("output-%d", out.id)
}
if name == outputName {
targetOut = out
return false
}
return true
})
if targetOut == nil {
return fmt.Errorf("output not found: %s", outputName)
}
if err := m.ensureOutputSetup(targetOut); err != nil {
return fmt.Errorf("failed to setup output %s: %w", outputName, err)
}
ipcOut, ok := targetOut.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2)
if !ok {
return fmt.Errorf("output %s has invalid ipcOutput type", outputName)
}
m.wlMutex.Lock()
err := ipcOut.SetClientTags(andTags, xorTags)
m.wlMutex.Unlock()
return err
}
func (m *Manager) SetLayout(outputName string, index uint32) error {
var targetOut *outputState
m.outputs.Range(func(key uint32, out *outputState) bool {
name := out.name
if name == "" {
name = fmt.Sprintf("output-%d", out.id)
}
if name == outputName {
targetOut = out
return false
}
return true
})
if targetOut == nil {
return fmt.Errorf("output not found: %s", outputName)
}
if err := m.ensureOutputSetup(targetOut); err != nil {
return fmt.Errorf("failed to setup output %s: %w", outputName, err)
}
ipcOut, ok := targetOut.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2)
if !ok {
return fmt.Errorf("output %s has invalid ipcOutput type", outputName)
}
m.wlMutex.Lock()
err := ipcOut.SetLayout(index)
m.wlMutex.Unlock()
return err
}
func (m *Manager) Close() {
close(m.stopChan)
m.wg.Wait()
m.notifierWg.Wait()
m.subscribers.Range(func(key string, ch chan State) bool {
close(ch)
m.subscribers.Delete(key)
return true
})
m.outputs.Range(func(key uint32, out *outputState) bool {
if ipcOut, ok := out.ipcOutput.(*dwl_ipc.ZdwlIpcOutputV2); ok {
ipcOut.Release()
}
m.outputs.Delete(key)
return true
})
if mgr, ok := m.manager.(*dwl_ipc.ZdwlIpcManagerV2); ok {
mgr.Release()
}
}
-366
View File
@@ -1,366 +0,0 @@
package dwl
import (
"errors"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient"
)
func TestStateChanged_BothNil(t *testing.T) {
assert.True(t, stateChanged(nil, nil))
}
func TestStateChanged_OneNil(t *testing.T) {
s := &State{TagCount: 9}
assert.True(t, stateChanged(s, nil))
assert.True(t, stateChanged(nil, s))
}
func TestStateChanged_TagCountDiffers(t *testing.T) {
a := &State{TagCount: 9, Outputs: make(map[string]*OutputState), Layouts: []string{}}
b := &State{TagCount: 10, Outputs: make(map[string]*OutputState), Layouts: []string{}}
assert.True(t, stateChanged(a, b))
}
func TestStateChanged_LayoutLengthDiffers(t *testing.T) {
a := &State{TagCount: 9, Layouts: []string{"tile"}, Outputs: make(map[string]*OutputState)}
b := &State{TagCount: 9, Layouts: []string{"tile", "monocle"}, Outputs: make(map[string]*OutputState)}
assert.True(t, stateChanged(a, b))
}
func TestStateChanged_ActiveOutputDiffers(t *testing.T) {
a := &State{TagCount: 9, ActiveOutput: "eDP-1", Outputs: make(map[string]*OutputState), Layouts: []string{}}
b := &State{TagCount: 9, ActiveOutput: "HDMI-A-1", Outputs: make(map[string]*OutputState), Layouts: []string{}}
assert.True(t, stateChanged(a, b))
}
func TestStateChanged_OutputCountDiffers(t *testing.T) {
a := &State{
TagCount: 9,
Outputs: map[string]*OutputState{"eDP-1": {}},
Layouts: []string{},
}
b := &State{
TagCount: 9,
Outputs: map[string]*OutputState{},
Layouts: []string{},
}
assert.True(t, stateChanged(a, b))
}
func TestStateChanged_OutputFieldsDiffer(t *testing.T) {
a := &State{
TagCount: 9,
Layouts: []string{},
Outputs: map[string]*OutputState{
"eDP-1": {Active: 1, Layout: 0, Title: "Firefox"},
},
}
b := &State{
TagCount: 9,
Layouts: []string{},
Outputs: map[string]*OutputState{
"eDP-1": {Active: 0, Layout: 0, Title: "Firefox"},
},
}
assert.True(t, stateChanged(a, b))
b.Outputs["eDP-1"].Active = 1
b.Outputs["eDP-1"].Layout = 1
assert.True(t, stateChanged(a, b))
b.Outputs["eDP-1"].Layout = 0
b.Outputs["eDP-1"].Title = "Code"
assert.True(t, stateChanged(a, b))
}
func TestStateChanged_TagsDiffer(t *testing.T) {
a := &State{
TagCount: 9,
Layouts: []string{},
Outputs: map[string]*OutputState{
"eDP-1": {Tags: []TagState{{Tag: 1, State: 1, Clients: 2, Focused: 1}}},
},
}
b := &State{
TagCount: 9,
Layouts: []string{},
Outputs: map[string]*OutputState{
"eDP-1": {Tags: []TagState{{Tag: 1, State: 2, Clients: 2, Focused: 1}}},
},
}
assert.True(t, stateChanged(a, b))
b.Outputs["eDP-1"].Tags[0].State = 1
b.Outputs["eDP-1"].Tags[0].Clients = 3
assert.True(t, stateChanged(a, b))
}
func TestStateChanged_Equal(t *testing.T) {
a := &State{
TagCount: 9,
ActiveOutput: "eDP-1",
Layouts: []string{"tile", "monocle"},
Outputs: map[string]*OutputState{
"eDP-1": {
Name: "eDP-1",
Active: 1,
Layout: 0,
LayoutSymbol: "[]=",
Title: "Firefox",
AppID: "firefox",
KbLayout: "us",
Keymode: "",
Tags: []TagState{{Tag: 1, State: 1, Clients: 2, Focused: 1}},
},
},
}
b := &State{
TagCount: 9,
ActiveOutput: "eDP-1",
Layouts: []string{"tile", "monocle"},
Outputs: map[string]*OutputState{
"eDP-1": {
Name: "eDP-1",
Active: 1,
Layout: 0,
LayoutSymbol: "[]=",
Title: "Firefox",
AppID: "firefox",
KbLayout: "us",
Keymode: "",
Tags: []TagState{{Tag: 1, State: 1, Clients: 2, Focused: 1}},
},
},
}
assert.False(t, stateChanged(a, b))
}
func TestManager_ConcurrentGetState(t *testing.T) {
m := &Manager{
state: &State{
TagCount: 9,
Layouts: []string{"tile"},
Outputs: map[string]*OutputState{"eDP-1": {Name: "eDP-1"}},
},
}
var wg sync.WaitGroup
const goroutines = 50
const iterations = 100
for i := 0; i < goroutines/2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < iterations; j++ {
s := m.GetState()
_ = s.TagCount
_ = s.Outputs
}
}()
}
for i := 0; i < goroutines/2; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
for j := 0; j < iterations; j++ {
m.stateMutex.Lock()
m.state = &State{
TagCount: uint32(j % 10),
Layouts: []string{"tile", "monocle"},
Outputs: map[string]*OutputState{"eDP-1": {Active: uint32(j % 2)}},
}
m.stateMutex.Unlock()
}
}(i)
}
wg.Wait()
}
func TestManager_ConcurrentSubscriberAccess(t *testing.T) {
m := &Manager{
stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1),
}
var wg sync.WaitGroup
const goroutines = 20
for i := 0; i < goroutines; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
subID := string(rune('a' + id))
ch := m.Subscribe(subID)
assert.NotNil(t, ch)
time.Sleep(time.Millisecond)
m.Unsubscribe(subID)
}(i)
}
wg.Wait()
}
func TestManager_SyncmapOutputsConcurrentAccess(t *testing.T) {
m := &Manager{}
var wg sync.WaitGroup
const goroutines = 30
const iterations = 50
for i := 0; i < goroutines; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
key := uint32(id)
for j := 0; j < iterations; j++ {
state := &outputState{
id: key,
name: "test-output",
active: uint32(j % 2),
tags: []TagState{{Tag: uint32(j), State: 1}},
}
m.outputs.Store(key, state)
if loaded, ok := m.outputs.Load(key); ok {
assert.Equal(t, key, loaded.id)
}
m.outputs.Range(func(k uint32, v *outputState) bool {
_ = v.name
_ = v.active
return true
})
}
m.outputs.Delete(key)
}(i)
}
wg.Wait()
}
func TestManager_NotifySubscribersNonBlocking(t *testing.T) {
m := &Manager{
dirty: make(chan struct{}, 1),
}
for i := 0; i < 10; i++ {
m.notifySubscribers()
}
assert.Len(t, m.dirty, 1)
}
func TestManager_PostQueueFull(t *testing.T) {
m := &Manager{
cmdq: make(chan cmd, 2),
stopChan: make(chan struct{}),
}
m.post(func() {})
m.post(func() {})
m.post(func() {})
m.post(func() {})
assert.Len(t, m.cmdq, 2)
}
func TestManager_GetStateNilState(t *testing.T) {
m := &Manager{}
s := m.GetState()
assert.NotNil(t, s.Outputs)
assert.NotNil(t, s.Layouts)
assert.Equal(t, uint32(0), s.TagCount)
}
func TestTagState_Fields(t *testing.T) {
tag := TagState{
Tag: 1,
State: 2,
Clients: 3,
Focused: 1,
}
assert.Equal(t, uint32(1), tag.Tag)
assert.Equal(t, uint32(2), tag.State)
assert.Equal(t, uint32(3), tag.Clients)
assert.Equal(t, uint32(1), tag.Focused)
}
func TestOutputState_Fields(t *testing.T) {
out := OutputState{
Name: "eDP-1",
Active: 1,
Tags: []TagState{{Tag: 1}},
Layout: 0,
LayoutSymbol: "[]=",
Title: "Firefox",
AppID: "firefox",
KbLayout: "us",
Keymode: "",
}
assert.Equal(t, "eDP-1", out.Name)
assert.Equal(t, uint32(1), out.Active)
assert.Len(t, out.Tags, 1)
assert.Equal(t, "[]=", out.LayoutSymbol)
}
func TestStateChanged_NewOutputAppears(t *testing.T) {
a := &State{
TagCount: 9,
Layouts: []string{},
Outputs: map[string]*OutputState{
"eDP-1": {Name: "eDP-1"},
},
}
b := &State{
TagCount: 9,
Layouts: []string{},
Outputs: map[string]*OutputState{
"eDP-1": {Name: "eDP-1"},
"HDMI-A-1": {Name: "HDMI-A-1"},
},
}
assert.True(t, stateChanged(a, b))
}
func TestStateChanged_TagsLengthDiffers(t *testing.T) {
a := &State{
TagCount: 9,
Layouts: []string{},
Outputs: map[string]*OutputState{
"eDP-1": {Tags: []TagState{{Tag: 1}}},
},
}
b := &State{
TagCount: 9,
Layouts: []string{},
Outputs: map[string]*OutputState{
"eDP-1": {Tags: []TagState{{Tag: 1}, {Tag: 2}}},
},
}
assert.True(t, stateChanged(a, b))
}
func TestNewManager_GetRegistryError(t *testing.T) {
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
mockDisplay.EXPECT().Context().Return(nil)
mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry"))
_, err := NewManager(mockDisplay)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to get registry")
}
-176
View File
@@ -1,176 +0,0 @@
package dwl
import (
"sync"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
type TagState struct {
Tag uint32 `json:"tag"`
State uint32 `json:"state"`
Clients uint32 `json:"clients"`
Focused uint32 `json:"focused"`
}
type OutputState struct {
Name string `json:"name"`
Active uint32 `json:"active"`
Tags []TagState `json:"tags"`
Layout uint32 `json:"layout"`
LayoutSymbol string `json:"layoutSymbol"`
Title string `json:"title"`
AppID string `json:"appId"`
KbLayout string `json:"kbLayout"`
Keymode string `json:"keymode"`
}
type State struct {
Outputs map[string]*OutputState `json:"outputs"`
TagCount uint32 `json:"tagCount"`
Layouts []string `json:"layouts"`
ActiveOutput string `json:"activeOutput"`
}
type cmd struct {
fn func()
}
type Manager struct {
display wlclient.WaylandDisplay
ctx *wlclient.Context
registry *wlclient.Registry
manager any
outputs syncmap.Map[uint32, *outputState]
tagCount uint32
layouts []string
wlMutex sync.Mutex
cmdq chan cmd
outputSetupReq chan uint32
stopChan chan struct{}
wg sync.WaitGroup
subscribers syncmap.Map[string, chan State]
dirty chan struct{}
notifierWg sync.WaitGroup
lastNotified *State
stateMutex sync.RWMutex
state *State
}
type outputState struct {
id uint32
registryName uint32
output *wlclient.Output
ipcOutput any
name string
active uint32
tags []TagState
layout uint32
layoutSymbol string
title string
appID string
kbLayout string
keymode string
}
func (m *Manager) GetState() State {
m.stateMutex.RLock()
defer m.stateMutex.RUnlock()
if m.state == nil {
return State{
Outputs: make(map[string]*OutputState),
Layouts: []string{},
TagCount: 0,
}
}
stateCopy := *m.state
return stateCopy
}
func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 64)
m.subscribers.Store(id, ch)
return ch
}
func (m *Manager) Unsubscribe(id string) {
if val, ok := m.subscribers.LoadAndDelete(id); ok {
close(val)
}
}
func (m *Manager) notifySubscribers() {
select {
case m.dirty <- struct{}{}:
default:
}
}
func stateChanged(old, new *State) bool {
if old == nil || new == nil {
return true
}
if old.TagCount != new.TagCount {
return true
}
if len(old.Layouts) != len(new.Layouts) {
return true
}
if old.ActiveOutput != new.ActiveOutput {
return true
}
if len(old.Outputs) != len(new.Outputs) {
return true
}
for name, newOut := range new.Outputs {
oldOut, exists := old.Outputs[name]
if !exists {
return true
}
if oldOut.Active != newOut.Active {
return true
}
if oldOut.Layout != newOut.Layout {
return true
}
if oldOut.LayoutSymbol != newOut.LayoutSymbol {
return true
}
if oldOut.Title != newOut.Title {
return true
}
if oldOut.AppID != newOut.AppID {
return true
}
if oldOut.KbLayout != newOut.KbLayout {
return true
}
if oldOut.Keymode != newOut.Keymode {
return true
}
if len(oldOut.Tags) != len(newOut.Tags) {
return true
}
for i, newTag := range newOut.Tags {
if i >= len(oldOut.Tags) {
return true
}
oldTag := oldOut.Tags[i]
if oldTag.Tag != newTag.Tag || oldTag.State != newTag.State ||
oldTag.Clients != newTag.Clients || oldTag.Focused != newTag.Focused {
return true
}
}
}
return false
}
-10
View File
@@ -11,7 +11,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups"
serverDbus "github.com/AvengeMedia/DankMaterialShell/core/internal/server/dbus" serverDbus "github.com/AvengeMedia/DankMaterialShell/core/internal/server/dbus"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/location" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/location"
@@ -125,15 +124,6 @@ func RouteRequest(conn net.Conn, req models.Request) {
return return
} }
if strings.HasPrefix(req.Method, "dwl.") {
if dwlManager == nil {
models.RespondError(conn, req.ID, "dwl manager not initialized")
return
}
dwl.HandleRequest(conn, req, dwlManager)
return
}
if strings.HasPrefix(req.Method, "brightness.") { if strings.HasPrefix(req.Method, "brightness.") {
if brightnessManager == nil { if brightnessManager == nil {
models.RespondError(conn, req.ID, "brightness manager not initialized") models.RespondError(conn, req.ID, "brightness manager not initialized")
+1 -87
View File
@@ -22,7 +22,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups"
serverDbus "github.com/AvengeMedia/DankMaterialShell/core/internal/server/dbus" serverDbus "github.com/AvengeMedia/DankMaterialShell/core/internal/server/dbus"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/location" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/location"
@@ -39,7 +38,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
const APIVersion = 24 const APIVersion = 25
var CLIVersion = "dev" var CLIVersion = "dev"
@@ -66,7 +65,6 @@ var bluezManager *bluez.Manager
var appPickerManager *apppicker.Manager var appPickerManager *apppicker.Manager
var cupsManager *cups.Manager var cupsManager *cups.Manager
var tailscaleManager *tailscale.Manager var tailscaleManager *tailscale.Manager
var dwlManager *dwl.Manager
var brightnessManager *brightness.Manager var brightnessManager *brightness.Manager
var wlrOutputManager *wlroutput.Manager var wlrOutputManager *wlroutput.Manager
var evdevManager *evdev.Manager var evdevManager *evdev.Manager
@@ -252,30 +250,6 @@ func InitializeCupsManager() error {
return nil return nil
} }
func InitializeDwlManager() error {
log.Info("Attempting to initialize DWL IPC...")
if wlContext == nil {
ctx, err := wlcontext.New()
if err != nil {
log.Errorf("Failed to create shared Wayland context: %v", err)
return err
}
wlContext = ctx
}
manager, err := dwl.NewManager(wlContext.Display())
if err != nil {
log.Debug("Failed to initialize dwl manager: %v", err)
return err
}
dwlManager = manager
log.Info("DWL IPC initialized successfully")
return nil
}
func InitializeBrightnessManager() error { func InitializeBrightnessManager() error {
manager, err := brightness.NewManager() manager, err := brightness.NewManager()
if err != nil { if err != nil {
@@ -468,10 +442,6 @@ func getCapabilities() Capabilities {
caps = append(caps, "tailscale") caps = append(caps, "tailscale")
} }
if dwlManager != nil {
caps = append(caps, "dwl")
}
if brightnessManager != nil { if brightnessManager != nil {
caps = append(caps, "brightness") caps = append(caps, "brightness")
} }
@@ -538,10 +508,6 @@ func getServerInfo() ServerInfo {
caps = append(caps, "tailscale") caps = append(caps, "tailscale")
} }
if dwlManager != nil {
caps = append(caps, "dwl")
}
if brightnessManager != nil { if brightnessManager != nil {
caps = append(caps, "brightness") caps = append(caps, "brightness")
} }
@@ -1046,38 +1012,6 @@ func handleSubscribe(conn net.Conn, req models.Request) {
}() }()
} }
if shouldSubscribe("dwl") && dwlManager != nil {
wg.Add(1)
dwlChan := dwlManager.Subscribe(clientID + "-dwl")
go func() {
defer wg.Done()
defer dwlManager.Unsubscribe(clientID + "-dwl")
initialState := dwlManager.GetState()
select {
case eventChan <- ServiceEvent{Service: "dwl", Data: initialState}:
case <-stopChan:
return
}
for {
select {
case state, ok := <-dwlChan:
if !ok {
return
}
select {
case eventChan <- ServiceEvent{Service: "dwl", Data: state}:
case <-stopChan:
return
}
case <-stopChan:
return
}
}
}()
}
if shouldSubscribe("brightness") && brightnessManager != nil { if shouldSubscribe("brightness") && brightnessManager != nil {
wg.Add(2) wg.Add(2)
brightnessStateChan := brightnessManager.Subscribe(clientID + "-brightness-state") brightnessStateChan := brightnessManager.Subscribe(clientID + "-brightness-state")
@@ -1333,9 +1267,6 @@ func cleanupManagers() {
if cupsManager != nil { if cupsManager != nil {
cupsManager.Close() cupsManager.Close()
} }
if dwlManager != nil {
dwlManager.Close()
}
if brightnessManager != nil { if brightnessManager != nil {
brightnessManager.Close() brightnessManager.Close()
} }
@@ -1502,19 +1433,6 @@ func Start(printDocs bool) error {
log.Info(" cups.resumePrinter - Resume printer (params: printerName)") log.Info(" cups.resumePrinter - Resume printer (params: printerName)")
log.Info(" cups.cancelJob - Cancel job (params: printerName, jobID)") log.Info(" cups.cancelJob - Cancel job (params: printerName, jobID)")
log.Info(" cups.purgeJobs - Cancel all jobs (params: printerName)") log.Info(" cups.purgeJobs - Cancel all jobs (params: printerName)")
log.Info("DWL:")
log.Info(" dwl.getState - Get current dwl state (tags, windows, layouts, keyboard)")
log.Info(" dwl.setTags - Set active tags (params: output, tagmask, toggleTagset)")
log.Info(" dwl.setClientTags - Set focused client tags (params: output, andTags, xorTags)")
log.Info(" dwl.setLayout - Set layout (params: output, index)")
log.Info(" dwl.subscribe - Subscribe to dwl state changes (streaming)")
log.Info(" Output state includes:")
log.Info(" - tags : Tag states (active, clients, focused)")
log.Info(" - layoutSymbol : Current layout name")
log.Info(" - title : Focused window title")
log.Info(" - appId : Focused window app ID")
log.Info(" - kbLayout : Current keyboard layout")
log.Info(" - keymode : Current keybind mode")
log.Info("Brightness:") log.Info("Brightness:")
log.Info(" brightness.getState - Get current brightness state for all devices") log.Info(" brightness.getState - Get current brightness state for all devices")
log.Info(" brightness.setBrightness - Set device brightness (params: device, percent)") log.Info(" brightness.setBrightness - Set device brightness (params: device, percent)")
@@ -1691,10 +1609,6 @@ func Start(printDocs bool) error {
log.Debugf("AppPicker manager unavailable: %v", err) log.Debugf("AppPicker manager unavailable: %v", err)
} }
if err := InitializeDwlManager(); err != nil {
log.Debugf("DWL manager unavailable: %v", err)
}
if err := InitializeWlrOutputManager(); err != nil { if err := InitializeWlrOutputManager(); err != nil {
log.Debugf("WlrOutput manager unavailable: %v", err) log.Debugf("WlrOutput manager unavailable: %v", err)
} }
+4 -41
View File
@@ -74,10 +74,6 @@ Singleton {
}, descriptor); }, descriptor);
} }
function legacySurfaceState(screenName, kind) {
return SurfaceDescriptor.toLegacyState(surfaceDescriptor(screenName, kind));
}
function hasSurfaceDescriptor(screenName, kind, ownerId) { function hasSurfaceDescriptor(screenName, kind, ownerId) {
const descriptor = surfaceDescriptor(screenName, kind); const descriptor = surfaceDescriptor(screenName, kind);
return descriptor.phase !== "hidden" && (!ownerId || descriptor.ownerId === ownerId); return descriptor.phase !== "hidden" && (!ownerId || descriptor.ownerId === ownerId);
@@ -124,20 +120,6 @@ Singleton {
return true; return true;
} }
function _setSurfaceAnimation(screenName, kind, ownerId, x, y) {
const current = surfaceDescriptor(screenName, kind);
if (current.phase === "hidden" || (ownerId && current.ownerId !== ownerId))
return false;
return true;
}
function _setSurfaceBody(screenName, kind, ownerId, x, y, width, height) {
const current = surfaceDescriptor(screenName, kind);
if (current.phase === "hidden" || (ownerId && current.ownerId !== ownerId))
return false;
return true;
}
readonly property var emptyDockState: ({ readonly property var emptyDockState: ({
"reveal": false, "reveal": false,
"barSide": "bottom", "barSide": "bottom",
@@ -149,7 +131,6 @@ Singleton {
"slideY": 0 "slideY": 0
}) })
// Popout state (updated by DankPopout when connectedFrameModeActive)
property string popoutOwnerId: "" property string popoutOwnerId: ""
property bool popoutVisible: false property bool popoutVisible: false
property string popoutBarSide: "top" property string popoutBarSide: "top"
@@ -163,14 +144,10 @@ Singleton {
property bool popoutOmitStartConnector: false property bool popoutOmitStartConnector: false
property bool popoutOmitEndConnector: false property bool popoutOmitEndConnector: false
// Dock state (updated by Dock when connectedFrameModeActive), keyed by screen.name
property var dockStates: ({}) property var dockStates: ({})
// Dock slide offsets — hot-path updates separated from full geometry state
property var dockSlides: ({}) property var dockSlides: ({})
// Surfaces are keyed by screen.name. FrameWindow watches to refresh connected chrome
// after claim/release boundaries without tracking each animation frame
property var surfaceRevisions: ({}) property var surfaceRevisions: ({})
function _cloneDict(src) { function _cloneDict(src) {
@@ -289,7 +266,6 @@ Singleton {
if (!isNaN(nextY) && popoutAnimY !== nextY) if (!isNaN(nextY) && popoutAnimY !== nextY)
popoutAnimY = nextY; popoutAnimY = nextY;
} }
_setSurfaceAnimation(popoutScreen, "popout", claimId, animX, animY);
return true; return true;
} }
@@ -316,7 +292,6 @@ Singleton {
if (!isNaN(nextH) && popoutBodyH !== nextH) if (!isNaN(nextH) && popoutBodyH !== nextH)
popoutBodyH = nextH; popoutBodyH = nextH;
} }
_setSurfaceBody(popoutScreen, "popout", claimId, bodyX, bodyY, bodyW, bodyH);
return true; return true;
} }
@@ -352,8 +327,8 @@ Singleton {
"phase": normalized.reveal ? (state.phase || "open") : "hidden" "phase": normalized.reveal ? (state.phase || "open") : "hidden"
}); });
const previous = dockStates[screenName] || emptyDockState; const previous = dockStates[screenName] || emptyDockState;
const legacyChanged = !_sameDockState(dockStates[screenName], normalized); const stateChanged = !_sameDockState(dockStates[screenName], normalized);
if (legacyChanged) { if (stateChanged) {
const next = _cloneDict(dockStates); const next = _cloneDict(dockStates);
next[screenName] = normalized; next[screenName] = normalized;
dockStates = next; dockStates = next;
@@ -373,7 +348,6 @@ Singleton {
dockStates = next; dockStates = next;
_clearSurfaceDescriptor(screenName, "dock"); _clearSurfaceDescriptor(screenName, "dock");
// Also clear corresponding slide
if (dockSlides[screenName]) { if (dockSlides[screenName]) {
const nextSlides = _cloneDict(dockSlides); const nextSlides = _cloneDict(dockSlides);
delete nextSlides[screenName]; delete nextSlides[screenName];
@@ -397,7 +371,6 @@ Singleton {
"y": numY "y": numY
}; };
dockSlides = next; dockSlides = next;
_setSurfaceAnimation(screenName, "dock", "dock:" + screenName, numX, numY);
return true; return true;
} }
@@ -451,8 +424,8 @@ Singleton {
"phase": normalized.visible ? (state.phase || "open") : "hidden" "phase": normalized.visible ? (state.phase || "open") : "hidden"
}); });
const previous = notificationStates[screenName] || emptyNotificationState; const previous = notificationStates[screenName] || emptyNotificationState;
const legacyChanged = !_sameNotificationState(notificationStates[screenName], normalized); const stateChanged = !_sameNotificationState(notificationStates[screenName], normalized);
if (legacyChanged) { if (stateChanged) {
const next = _cloneDict(notificationStates); const next = _cloneDict(notificationStates);
next[screenName] = normalized; next[screenName] = normalized;
notificationStates = next; notificationStates = next;
@@ -475,7 +448,6 @@ Singleton {
return true; return true;
} }
// DankModal / DankLauncherV2Modal State
readonly property var emptyModalState: ({ readonly property var emptyModalState: ({
"visible": false, "visible": false,
"barSide": "bottom", "barSide": "bottom",
@@ -573,10 +545,6 @@ Singleton {
return updateModalState(screenName, state, ownerId); return updateModalState(screenName, state, ownerId);
} }
function setModalState(screenName, state) {
return updateModalState(screenName, state, null);
}
function clearModalState(screenName, ownerId) { function clearModalState(screenName, ownerId) {
if (!screenName) if (!screenName)
return false; return false;
@@ -617,7 +585,6 @@ Singleton {
"animY": nay "animY": nay
}); });
modalStates = next; modalStates = next;
_setSurfaceAnimation(screenName, "modal", ownerId, animX, animY);
return true; return true;
} }
@@ -641,7 +608,6 @@ Singleton {
"bodyH": nh "bodyH": nh
}); });
modalStates = next; modalStates = next;
_setSurfaceBody(screenName, "modal", ownerId, bodyX, bodyY, bodyW, bodyH);
return true; return true;
} }
@@ -682,9 +648,6 @@ Singleton {
return false; return false;
} }
// Prune state for screens that are no longer connected. Stale entries
// accumulate across hotplug cycles otherwise — Frame's per-screen
// FrameInstance doesn't notice when its peer dicts go orphan.
function _pruneToLiveScreens() { function _pruneToLiveScreens() {
const live = {}; const live = {};
const screens = Quickshell.screens || []; const screens = Quickshell.screens || [];
@@ -157,23 +157,3 @@ function same(a, b, threshold) {
&& a.omitEndConnector === b.omitEndConnector && a.omitEndConnector === b.omitEndConnector
&& a.dockRetractSide === b.dockRetractSide; && a.dockRetractSide === b.dockRetractSide;
} }
function toLegacyState(descriptor) {
var d = normalize(descriptor);
return {
"visible": d.visible,
"reveal": d.visible,
"barSide": d.barSide,
"bodyX": d.bodyRect.x,
"bodyY": d.bodyRect.y,
"bodyW": d.bodyRect.width,
"bodyH": d.bodyRect.height,
"animX": d.animationOffset.x,
"animY": d.animationOffset.y,
"slideX": d.animationOffset.x,
"slideY": d.animationOffset.y,
"screen": d.screenName,
"omitStartConnector": d.omitStartConnector,
"omitEndConnector": d.omitEndConnector
};
}
-3
View File
@@ -19,7 +19,6 @@ Item {
property color borderColor: "transparent" property color borderColor: "transparent"
property real borderWidth: 0 property real borderWidth: 0
// Rounded-rect geometry within the item; defaults fill the item.
property real sourceX: 0 property real sourceX: 0
property real sourceY: 0 property real sourceY: 0
property real sourceWidth: width property real sourceWidth: width
@@ -36,8 +35,6 @@ Item {
readonly property var _ambient: Theme.elevationAmbient(level) readonly property var _ambient: Theme.elevationAmbient(level)
readonly property real _pad: shadowEnabled ? Math.ceil(Math.max(shadowBlurPx + shadowSpreadPx + Math.max(Math.abs(shadowOffsetX), Math.abs(shadowOffsetY)), _ambient.blurPx + _ambient.spreadPx) + 2) : 0 readonly property real _pad: shadowEnabled ? Math.ceil(Math.max(shadowBlurPx + shadowSpreadPx + Math.max(Math.abs(shadowOffsetX), Math.abs(shadowOffsetY)), _ambient.blurPx + _ambient.spreadPx) + 2) : 0
// Fill + border + key/ambient shadows drawn analytically on one oversized
// quad — no FBO, no blur passes.
ShaderEffect { ShaderEffect {
anchors.fill: parent anchors.fill: parent
anchors.margins: -root._pad anchors.margins: -root._pad
+3 -9
View File
@@ -489,9 +489,6 @@ Singleton {
"hideOnTouch": false, "hideOnTouch": false,
"inactiveTimeout": 0 "inactiveTimeout": 0
}, },
"dwl": {
"cursorHideTimeout": 0
},
"mango": { "mango": {
"cursorHideTimeout": 0 "cursorHideTimeout": 0
} }
@@ -518,6 +515,8 @@ Singleton {
property bool notepadUseMonospace: true property bool notepadUseMonospace: true
property string notepadFontFamily: "" property string notepadFontFamily: ""
property real notepadFontSize: 14 property real notepadFontSize: 14
property real notificationSummaryFontSize: Spec.SPEC.notificationSummaryFontSize.def
property real notificationBodyFontSize: Spec.SPEC.notificationBodyFontSize.def
property bool notepadShowLineNumbers: false property bool notepadShowLineNumbers: false
property real notepadTransparencyOverride: -1 property real notepadTransparencyOverride: -1
property real notepadLastCustomTransparency: 0.7 property real notepadLastCustomTransparency: 0.7
@@ -698,6 +697,7 @@ Singleton {
property int notificationTimeoutNormal: 5000 property int notificationTimeoutNormal: 5000
property int notificationTimeoutCritical: 0 property int notificationTimeoutCritical: 0
property bool notificationCompactMode: false property bool notificationCompactMode: false
property bool notificationShowTimeoutBar: false
property bool notificationDedupeEnabled: true property bool notificationDedupeEnabled: true
property int notificationPopupPosition: SettingsData.Position.Top property int notificationPopupPosition: SettingsData.Position.Top
property int notificationAnimationSpeed: SettingsData.AnimationSpeed.Short property int notificationAnimationSpeed: SettingsData.AnimationSpeed.Short
@@ -1224,8 +1224,6 @@ Singleton {
NiriService.generateNiriLayoutConfig(); NiriService.generateNiriLayoutConfig();
if (CompositorService.isHyprland && typeof HyprlandService !== "undefined") if (CompositorService.isHyprland && typeof HyprlandService !== "undefined")
HyprlandService.generateLayoutConfig(); HyprlandService.generateLayoutConfig();
if (CompositorService.isDwl && typeof DwlService !== "undefined")
DwlService.generateLayoutConfig();
if (CompositorService.isMango && typeof MangoService !== "undefined") if (CompositorService.isMango && typeof MangoService !== "undefined")
MangoService.generateLayoutConfig(); MangoService.generateLayoutConfig();
} }
@@ -2451,10 +2449,6 @@ Singleton {
HyprlandService.generateCursorConfig(); HyprlandService.generateCursorConfig();
return; return;
} }
if (CompositorService.isDwl && typeof DwlService !== "undefined") {
DwlService.generateCursorConfig();
return;
}
if (CompositorService.isMango && typeof MangoService !== "undefined") { if (CompositorService.isMango && typeof MangoService !== "undefined") {
MangoService.generateCursorConfig(); MangoService.generateCursorConfig();
return; return;
-3
View File
@@ -911,9 +911,6 @@ Singleton {
} }
return Qt.rgba(r, g, b, alpha); return Qt.rgba(r, g, b, alpha);
} }
// Non-directional ambient layer of the M3 two-part shadow model (key +
// ambient). Derived from the key level so user intensity/opacity settings
// scale both layers together.
function elevationAmbient(level) { function elevationAmbient(level) {
const blur = (level && level.blurPx !== undefined) ? Math.max(0, level.blurPx) : 0; const blur = (level && level.blurPx !== undefined) ? Math.max(0, level.blurPx) : 0;
const alpha = ((level && level.alpha !== undefined) ? level.alpha : 0.3) * 0.5; const alpha = ((level && level.alpha !== undefined) ? level.alpha : 0.3) * 0.5;
+2 -2
View File
@@ -570,7 +570,7 @@ Singleton {
onExited: exitCode => { onExited: exitCode => {
const enabling = root.settingsRoot && root.settingsRoot.greeterAutoLogin; const enabling = root.settingsRoot && root.settingsRoot.greeterAutoLogin;
if (exitCode === 0) { if (exitCode === 0) {
ToastService.showWarning(enabling ? I18n.tr("Applying auto-login on startup") : I18n.tr("Disabling auto-login on startup"), "", "dms greeter sync --autologin", "greeter-autologin-sync"); ToastService.showWarning(enabling ? I18n.tr("Applying auto-login on startup...") : I18n.tr("Disabling auto-login on startup..."), "", "dms greeter sync --autologin", "greeter-autologin-sync");
root.greeterAutoLoginSyncProcess.running = true; root.greeterAutoLoginSyncProcess.running = true;
return; return;
} }
@@ -645,7 +645,7 @@ Singleton {
onExited: exitCode => { onExited: exitCode => {
const err = (root.authApplySudoProbeStderr || "").trim(); const err = (root.authApplySudoProbeStderr || "").trim();
if (exitCode === 0) { if (exitCode === 0) {
ToastService.showInfo(I18n.tr("Applying authentication changes"), "", "", "auth-sync"); ToastService.showInfo(I18n.tr("Applying authentication changes..."), "", "", "auth-sync");
root.authApplyProcess.running = true; root.authApplyProcess.running = true;
return; return;
} }
@@ -260,6 +260,8 @@ var SPEC = {
notepadUseMonospace: { def: true }, notepadUseMonospace: { def: true },
notepadFontFamily: { def: "" }, notepadFontFamily: { def: "" },
notepadFontSize: { def: 14 }, notepadFontSize: { def: 14 },
notificationSummaryFontSize: { def: 0 },
notificationBodyFontSize: { def: 0 },
notepadShowLineNumbers: { def: false }, notepadShowLineNumbers: { def: false },
notepadTransparencyOverride: { def: -1 }, notepadTransparencyOverride: { def: -1 },
notepadLastCustomTransparency: { def: 0.7 }, notepadLastCustomTransparency: { def: 0.7 },
@@ -406,6 +408,7 @@ var SPEC = {
notificationTimeoutNormal: { def: 5000 }, notificationTimeoutNormal: { def: 5000 },
notificationTimeoutCritical: { def: 0 }, notificationTimeoutCritical: { def: 0 },
notificationCompactMode: { def: false }, notificationCompactMode: { def: false },
notificationShowTimeoutBar: { def: false },
notificationDedupeEnabled: { def: true }, notificationDedupeEnabled: { def: true },
notificationPopupPosition: { def: 0 }, notificationPopupPosition: { def: 0 },
notificationAnimationSpeed: { def: 1 }, notificationAnimationSpeed: { def: 1 },
-3
View File
@@ -337,9 +337,6 @@ Item {
const focusedWs = I3.workspaces.values.find(ws => ws.focused === true); const focusedWs = I3.workspaces.values.find(ws => ws.focused === true);
return focusedWs?.monitor?.name || ""; return focusedWs?.monitor?.name || "";
} }
if (CompositorService.isDwl && DwlService.activeOutput) {
return DwlService.activeOutput;
}
if (CompositorService.isMango && MangoService.activeOutput) { if (CompositorService.isMango && MangoService.activeOutput) {
return MangoService.activeOutput; return MangoService.activeOutput;
} }
@@ -128,7 +128,7 @@ Item {
} }
StyledText { StyledText {
text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No recent clipboard entries found") : I18n.tr("Connecting to clipboard service") text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No recent clipboard entries found") : I18n.tr("Connecting to clipboard service...")
anchors.centerIn: parent anchors.centerIn: parent
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
@@ -149,8 +149,8 @@ Item {
listView: clipboardListView listView: clipboardListView
onCopyRequested: clipboardContent.modal.copyEntry(modelData) onCopyRequested: clipboardContent.modal.copyEntry(modelData)
onDeleteRequested: clipboardContent.modal.deleteEntry(modelData) onDeleteRequested: clipboardContent.modal.deleteEntry(modelData)
onPinRequested: clipboardContent.modal.pinEntry(modelData) onPinRequested: targetEntry => clipboardContent.modal.pinEntry(targetEntry)
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData) onUnpinRequested: targetEntry => clipboardContent.modal.unpinEntry(targetEntry)
onEditRequested: clipboardContent.modal.editEntry(modelData) onEditRequested: clipboardContent.modal.editEntry(modelData)
} }
} }
@@ -202,7 +202,7 @@ Item {
} }
StyledText { StyledText {
text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No saved clipboard entries") : I18n.tr("Connecting to clipboard service") text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No saved clipboard entries") : I18n.tr("Connecting to clipboard service...")
anchors.centerIn: parent anchors.centerIn: parent
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
@@ -223,8 +223,8 @@ Item {
listView: savedListView listView: savedListView
onCopyRequested: clipboardContent.modal.copyEntry(modelData) onCopyRequested: clipboardContent.modal.copyEntry(modelData)
onDeleteRequested: clipboardContent.modal.deletePinnedEntry(modelData) onDeleteRequested: clipboardContent.modal.deletePinnedEntry(modelData)
onPinRequested: clipboardContent.modal.pinEntry(modelData) onPinRequested: targetEntry => clipboardContent.modal.pinEntry(targetEntry)
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData) onUnpinRequested: targetEntry => clipboardContent.modal.unpinEntry(targetEntry)
onEditRequested: clipboardContent.modal.editEntry(modelData) onEditRequested: clipboardContent.modal.editEntry(modelData)
} }
} }
+17 -6
View File
@@ -15,13 +15,14 @@ Rectangle {
signal copyRequested signal copyRequested
signal deleteRequested signal deleteRequested
signal pinRequested signal pinRequested(var targetEntry)
signal unpinRequested signal unpinRequested(var targetEntry)
signal editRequested signal editRequested
readonly property string entryType: modal ? modal.getEntryType(entry) : "text" readonly property string entryType: modal ? modal.getEntryType(entry) : "text"
readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : "" readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : ""
readonly property bool hasPinnedDuplicate: !entry.pinned && ClipboardService.hashedPinnedEntry(entry.hash) readonly property var pinnedDuplicateEntry: !entry.pinned ? ClipboardService.getPinnedEntryByHash(entry.hash) : null
readonly property bool effectivePinned: entry.pinned || pinnedDuplicateEntry !== null
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
@@ -66,9 +67,19 @@ Rectangle {
DankActionButton { DankActionButton {
iconName: "push_pin" iconName: "push_pin"
iconSize: Theme.iconSize - 6 iconSize: Theme.iconSize - 6
iconColor: (entry.pinned || hasPinnedDuplicate) ? Theme.primary : Theme.surfaceText iconColor: effectivePinned ? Theme.primary : Theme.surfaceText
backgroundColor: (entry.pinned || hasPinnedDuplicate) ? Theme.primarySelected : "transparent" backgroundColor: effectivePinned ? Theme.primarySelected : "transparent"
onClicked: entry.pinned ? unpinRequested() : pinRequested() onClicked: {
if (entry.pinned) {
unpinRequested(entry);
return;
}
if (pinnedDuplicateEntry) {
unpinRequested(pinnedDuplicateEntry);
return;
}
pinRequested(entry);
}
} }
DankActionButton { DankActionButton {
@@ -58,6 +58,7 @@ DankModal {
readonly property bool clipboardAvailable: ClipboardService.clipboardAvailable readonly property bool clipboardAvailable: ClipboardService.clipboardAvailable
visible: false visible: false
keepContentLoaded: true
modalWidth: ClipboardConstants.modalWidth modalWidth: ClipboardConstants.modalWidth
modalHeight: ClipboardConstants.modalHeight modalHeight: ClipboardConstants.modalHeight
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
@@ -59,8 +59,13 @@ QtObject {
return; return;
} }
const selectedEntry = entries[ClipboardService.selectedIndex]; const selectedEntry = entries[ClipboardService.selectedIndex];
if (modal.activeTab === "saved") { if (selectedEntry.pinned) {
modal.unpinEntry(selectedEntry); modal.unpinEntry(selectedEntry);
return;
}
const pinnedDuplicate = ClipboardService.getPinnedEntryByHash(selectedEntry.hash);
if (pinnedDuplicate) {
modal.unpinEntry(pinnedDuplicate);
} else { } else {
modal.pinEntry(selectedEntry); modal.pinEntry(selectedEntry);
} }
+2 -15
View File
@@ -54,23 +54,12 @@ Item {
anchors.fill: parent anchors.fill: parent
} }
// One focus grab for every modal; on Hyprland this is what delivers // Hyprland OnDemand grab delivers keyboard focus to the modal content surface.
// keyboard focus to the OnDemand surface, identically in both modes.
// The clickCatcher is whitelisted so an outside click is delivered to
// it (closing the modal) instead of being consumed clearing the grab.
HyprlandFocusGrab { HyprlandFocusGrab {
windows: { windows: root.contentWindow ? [root.contentWindow] : []
const list = [];
if (root.contentWindow)
list.push(root.contentWindow);
if (root.clickCatcher)
list.push(root.clickCatcher);
return list;
}
active: KeyboardFocus.wantsGrab(root.shouldHaveFocus, root.customKeyboardFocus) active: KeyboardFocus.wantsGrab(root.shouldHaveFocus, root.customKeyboardFocus)
} }
readonly property var contentWindow: impl.item ? impl.item.contentWindow : null readonly property var contentWindow: impl.item ? impl.item.contentWindow : null
readonly property var clickCatcher: impl.item ? impl.item.clickCatcher : null
readonly property var effectiveScreen: impl.item ? impl.item.effectiveScreen : null readonly property var effectiveScreen: impl.item ? impl.item.effectiveScreen : null
readonly property real screenWidth: impl.item ? impl.item.screenWidth : 1920 readonly property real screenWidth: impl.item ? impl.item.screenWidth : 1920
readonly property real screenHeight: impl.item ? impl.item.screenHeight : 1080 readonly property real screenHeight: impl.item ? impl.item.screenHeight : 1080
@@ -113,8 +102,6 @@ Item {
} }
} }
// Defer Loader source-component swap until impl is fully closed; avoids
// tearing down a modal mid-animation when frame mode is toggled.
function _maybeResolveBackend() { function _maybeResolveBackend() {
if (_resolvedBackend === _desiredBackend) if (_resolvedBackend === _desiredBackend)
return; return;
+232 -321
View File
@@ -31,7 +31,6 @@ Item {
property bool closeOnBackgroundClick: true property bool closeOnBackgroundClick: true
property string animationType: "scale" property string animationType: "scale"
// Opposite side from the launcher by default; subclasses may override
property string preferredConnectedBarSide: SettingsData.frameModalEmergeSide property string preferredConnectedBarSide: SettingsData.frameModalEmergeSide
readonly property bool frameConnectedMode: SettingsData.frameEnabled && Theme.isConnectedEffect && !!effectiveScreen && SettingsData.isScreenInPreferences(effectiveScreen, SettingsData.frameScreenPreferences) readonly property bool frameConnectedMode: SettingsData.frameEnabled && Theme.isConnectedEffect && !!effectiveScreen && SettingsData.isScreenInPreferences(effectiveScreen, SettingsData.frameScreenPreferences)
@@ -87,16 +86,13 @@ Item {
property real frozenMotionOffsetX: 0 property real frozenMotionOffsetX: 0
property real frozenMotionOffsetY: 0 property real frozenMotionOffsetY: 0
readonly property alias contentWindow: contentWindow readonly property alias contentWindow: contentWindow
readonly property alias clickCatcher: clickCatcher
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
readonly property bool useBackground: false readonly property bool useBackground: false
readonly property bool useSingleWindow: CompositorService.isHyprland
signal opened signal opened
signal dialogClosed signal dialogClosed
signal backgroundClicked signal backgroundClicked
// Coalesce per-channel dirty bits; one ConnectedModeState write per tick.
Timer { Timer {
id: _syncTimer id: _syncTimer
interval: 0 interval: 0
@@ -244,22 +240,16 @@ Item {
const focusedScreen = CompositorService.getFocusedScreen(); const focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen) { if (focusedScreen) {
contentWindow.screen = focusedScreen; contentWindow.screen = focusedScreen;
if (!useSingleWindow)
clickCatcher.screen = focusedScreen;
} }
ModalManager.openModal(modalHandle); ModalManager.openModal(modalHandle);
if (Theme.isDirectionalEffect || root.useBackground) { if (Theme.isDirectionalEffect || root.useBackground) {
if (!useSingleWindow)
clickCatcher.visible = true;
contentWindow.visible = true; contentWindow.visible = true;
} }
Qt.callLater(() => { Qt.callLater(() => {
animationsEnabled = true; animationsEnabled = true;
shouldBeVisible = true; shouldBeVisible = true;
if (!useSingleWindow && !clickCatcher.visible)
clickCatcher.visible = true;
if (!contentWindow.visible) if (!contentWindow.visible)
contentWindow.visible = true; contentWindow.visible = true;
opened(); opened();
@@ -286,8 +276,6 @@ Item {
ModalManager.closeModal(modalHandle); ModalManager.closeModal(modalHandle);
closeTimer.stop(); closeTimer.stop();
contentWindow.visible = false; contentWindow.visible = false;
if (!useSingleWindow)
clickCatcher.visible = false;
dialogClosed(); dialogClosed();
Qt.callLater(() => animationsEnabled = true); Qt.callLater(() => animationsEnabled = true);
} }
@@ -326,8 +314,6 @@ Item {
const newScreen = CompositorService.getFocusedScreen(); const newScreen = CompositorService.getFocusedScreen();
if (newScreen) { if (newScreen) {
contentWindow.screen = newScreen; contentWindow.screen = newScreen;
if (!useSingleWindow)
clickCatcher.screen = newScreen;
} }
} }
} }
@@ -339,29 +325,12 @@ Item {
if (shouldBeVisible) if (shouldBeVisible)
return; return;
contentWindow.visible = false; contentWindow.visible = false;
if (!useSingleWindow)
clickCatcher.visible = false;
dialogClosed(); dialogClosed();
} }
} }
// shadowRenderPadding is zeroed when frame owns the chrome
// Wayland then clips any content translating past
readonly property var shadowLevel: Theme.elevationLevel3 readonly property var shadowLevel: Theme.elevationLevel3
readonly property real shadowFallbackOffset: 6 readonly property real shadowFallbackOffset: 6
readonly property real shadowRenderPadding: (!frameOwnsConnectedChrome && root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
readonly property real shadowMotionPadding: {
if (frameOwnsConnectedChrome)
return 0;
if (animationType === "slide")
return 30;
if (Theme.isDirectionalEffect)
return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.9);
if (Theme.isDepthEffect)
return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.35);
return Math.max(0, animationOffset);
}
readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
readonly property real alignedWidth: Theme.px(modalWidth, dpr) readonly property real alignedWidth: Theme.px(modalWidth, dpr)
readonly property real alignedHeight: Theme.px(modalHeight, dpr) readonly property real alignedHeight: Theme.px(modalHeight, dpr)
@@ -371,7 +340,6 @@ Item {
return SettingsData.frameEdgeInsetForSide(effectiveScreen, side); return SettingsData.frameEdgeInsetForSide(effectiveScreen, side);
} }
// frameEdgeInsetForSide is the full inset; do not add frameBarSize
readonly property real _connectedAlignedX: { readonly property real _connectedAlignedX: {
switch (resolvedConnectedBarSide) { switch (resolvedConnectedBarSide) {
case "top": case "top":
@@ -434,57 +402,6 @@ Item {
} }
})(), dpr) })(), dpr)
PanelWindow {
id: clickCatcher
visible: false
color: "transparent"
WlrLayershell.namespace: root.layerNamespace + ":clickcatcher"
WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
anchors {
top: true
left: true
right: true
bottom: true
}
mask: Region {
item: Rectangle {
x: root.alignedX
y: root.alignedY
width: root.alignedWidth
height: root.alignedHeight
}
intersection: Intersection.Xor
}
MouseArea {
anchors.fill: parent
enabled: !root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible
onClicked: root.backgroundClicked()
}
Rectangle {
anchors.fill: parent
z: -1
color: "black"
opacity: (!root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: opacity > 0
Behavior on opacity {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
NumberAnimation {
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
}
}
PanelWindow { PanelWindow {
id: contentWindow id: contentWindow
visible: false visible: false
@@ -494,8 +411,8 @@ Item {
targetWindow: contentWindow targetWindow: contentWindow
blurEnabled: root.effectiveBlurEnabled && !root.frameOwnsConnectedChrome blurEnabled: root.effectiveBlurEnabled && !root.frameOwnsConnectedChrome
readonly property real s: Math.min(1, modalContainer.scaleValue) readonly property real s: Math.min(1, modalContainer.scaleValue)
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr) blurX: connectedReveal.x + modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr)
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr) blurY: connectedReveal.y + modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
blurWidth: (root.shouldBeVisible && !root.frameOwnsConnectedChrome) ? modalContainer.width * s : 0 blurWidth: (root.shouldBeVisible && !root.frameOwnsConnectedChrome) ? modalContainer.width * s : 0
blurHeight: (root.shouldBeVisible && !root.frameOwnsConnectedChrome) ? modalContainer.height * s : 0 blurHeight: (root.shouldBeVisible && !root.frameOwnsConnectedChrome) ? modalContainer.height * s : 0
blurRadius: root.effectiveCornerRadius blurRadius: root.effectiveCornerRadius
@@ -514,23 +431,10 @@ Item {
anchors { anchors {
left: true left: true
top: true top: true
right: root.useSingleWindow right: true
bottom: root.useSingleWindow bottom: true
} }
readonly property real actualMarginLeft: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
readonly property real actualMarginTop: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
WlrLayershell.margins {
left: actualMarginLeft
top: actualMarginTop
right: 0
bottom: 0
}
implicitWidth: root.useSingleWindow ? 0 : root.alignedWidth + (shadowBuffer * 2)
implicitHeight: root.useSingleWindow ? 0 : root.alignedHeight + (shadowBuffer * 2)
onVisibleChanged: { onVisibleChanged: {
if (visible) if (visible)
return; return;
@@ -542,7 +446,7 @@ Item {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
enabled: root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible enabled: root.closeOnBackgroundClick && root.shouldBeVisible
z: -2 z: -2
onClicked: root.backgroundClicked() onClicked: root.backgroundClicked()
} }
@@ -551,7 +455,7 @@ Item {
anchors.fill: parent anchors.fill: parent
z: -1 z: -1
color: "black" color: "black"
opacity: (root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0 opacity: root.useBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: opacity > 0 visible: opacity > 0
Behavior on opacity { Behavior on opacity {
@@ -565,249 +469,256 @@ Item {
} }
Item { Item {
id: modalContainer id: connectedReveal
x: (root.useSingleWindow ? root.alignedX : (root.alignedX - contentWindow.actualMarginLeft)) + Theme.snap(animX, root.dpr) // Clip to final footprint while frame-owned chrome grows from the bar edge.
y: (root.useSingleWindow ? root.alignedY : (root.alignedY - contentWindow.actualMarginTop)) + Theme.snap(animY, root.dpr) x: root.alignedX
y: root.alignedY
width: root.alignedWidth width: root.alignedWidth
height: root.alignedHeight height: root.alignedHeight
clip: root.frameOwnsConnectedChrome
MouseArea {
anchors.fill: parent
enabled: root.useSingleWindow && root.shouldBeVisible
hoverEnabled: false
acceptedButtons: Qt.AllButtons
onPressed: mouse.accepted = true
onClicked: mouse.accepted = true
z: -1
}
readonly property bool slide: root.animationType === "slide"
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
readonly property real directionalTravel: Math.max(root.animationOffset, Math.max(root.alignedWidth, root.alignedHeight) * 0.8)
readonly property real depthTravel: Math.max(root.animationOffset * 0.8, 36)
readonly property real customAnchorX: root.alignedX + root.alignedWidth * 0.5
readonly property real customAnchorY: root.alignedY + root.alignedHeight * 0.5
readonly property real customDistLeft: customAnchorX
readonly property real customDistRight: root.screenWidth - customAnchorX
readonly property real customDistTop: customAnchorY
readonly property real customDistBottom: root.screenHeight - customAnchorY
// Connected emergence: travel from the resolved bar edge, matching DankPopout cadence.
readonly property real connectedEmergenceTravelX: Math.max(root.animationOffset, root.alignedWidth + Theme.spacingL)
readonly property real connectedEmergenceTravelY: Math.max(root.animationOffset, root.alignedHeight + Theme.spacingL)
readonly property real offsetX: {
if (root.frameOwnsConnectedChrome) {
switch (root.resolvedConnectedBarSide) {
case "left":
return -connectedEmergenceTravelX;
case "right":
return connectedEmergenceTravelX;
}
return 0;
}
if (slide && !directionalEffect && !depthEffect)
return 15;
if (directionalEffect) {
switch (root.positioning) {
case "top-right":
return 0;
case "custom":
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
return -directionalTravel;
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
return directionalTravel;
return 0;
default:
return 0;
}
}
if (depthEffect) {
switch (root.positioning) {
case "top-right":
return 0;
case "custom":
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
return -depthTravel;
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
return depthTravel;
return 0;
default:
return 0;
}
}
return 0;
}
readonly property real offsetY: {
if (root.frameOwnsConnectedChrome) {
switch (root.resolvedConnectedBarSide) {
case "top":
return -connectedEmergenceTravelY;
case "bottom":
return connectedEmergenceTravelY;
}
return 0;
}
if (slide && !directionalEffect && !depthEffect)
return -30;
if (directionalEffect) {
switch (root.positioning) {
case "top-right":
return -Math.max(directionalTravel * 0.65, 96);
case "custom":
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
return -directionalTravel;
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
return directionalTravel;
return 0;
default:
// Default to sliding down from top when centered
return -Math.max(directionalTravel, root.screenHeight * 0.24);
}
}
if (depthEffect) {
switch (root.positioning) {
case "top-right":
return -depthTravel * 0.75;
case "custom":
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
return -depthTravel;
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
return depthTravel;
return depthTravel * 0.45;
default:
return -depthTravel;
}
}
return root.animationOffset;
}
readonly property real computedScaleCollapsed: root.animationScaleCollapsed
// openProgress: 0 = closed (at frozenMotionOffset, scaleCollapsed), 1 = open (at 0, scale 1).
QtObject {
id: morph
property real openProgress: root.shouldBeVisible ? 1 : 0
Behavior on openProgress {
enabled: root.animationsEnabled
NumberAnimation {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
}
readonly property real animX: root.frozenMotionOffsetX * (1 - morph.openProgress)
readonly property real animY: root.frozenMotionOffsetY * (1 - morph.openProgress)
readonly property real scaleValue: computedScaleCollapsed + (1.0 - computedScaleCollapsed) * morph.openProgress
onAnimXChanged: if (root.frameOwnsConnectedChrome)
root._queueAnimSync()
onAnimYChanged: if (root.frameOwnsConnectedChrome)
root._queueAnimSync()
Item { Item {
id: contentContainer id: modalContainer
anchors.centerIn: parent x: Theme.snap(animX, root.dpr)
width: parent.width y: Theme.snap(animY, root.dpr)
height: parent.height
clip: false width: root.alignedWidth
height: root.alignedHeight
MouseArea {
anchors.fill: parent
enabled: root.shouldBeVisible
hoverEnabled: false
acceptedButtons: Qt.AllButtons
onPressed: mouse.accepted = true
onClicked: mouse.accepted = true
z: -1
}
readonly property bool slide: root.animationType === "slide"
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
readonly property real directionalTravel: Math.max(root.animationOffset, Math.max(root.alignedWidth, root.alignedHeight) * 0.8)
readonly property real depthTravel: Math.max(root.animationOffset * 0.8, 36)
readonly property real customAnchorX: root.alignedX + root.alignedWidth * 0.5
readonly property real customAnchorY: root.alignedY + root.alignedHeight * 0.5
readonly property real customDistLeft: customAnchorX
readonly property real customDistRight: root.screenWidth - customAnchorX
readonly property real customDistTop: customAnchorY
readonly property real customDistBottom: root.screenHeight - customAnchorY
readonly property real connectedEmergenceTravelX: Math.max(root.animationOffset, root.alignedWidth + Theme.spacingL)
readonly property real connectedEmergenceTravelY: Math.max(root.animationOffset, root.alignedHeight + Theme.spacingL)
readonly property real offsetX: {
if (root.frameOwnsConnectedChrome) {
switch (root.resolvedConnectedBarSide) {
case "left":
return -connectedEmergenceTravelX;
case "right":
return connectedEmergenceTravelX;
}
return 0;
}
if (slide && !directionalEffect && !depthEffect)
return 15;
if (directionalEffect) {
switch (root.positioning) {
case "top-right":
return 0;
case "custom":
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
return -directionalTravel;
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
return directionalTravel;
return 0;
default:
return 0;
}
}
if (depthEffect) {
switch (root.positioning) {
case "top-right":
return 0;
case "custom":
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
return -depthTravel;
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
return depthTravel;
return 0;
default:
return 0;
}
}
return 0;
}
readonly property real offsetY: {
if (root.frameOwnsConnectedChrome) {
switch (root.resolvedConnectedBarSide) {
case "top":
return -connectedEmergenceTravelY;
case "bottom":
return connectedEmergenceTravelY;
}
return 0;
}
if (slide && !directionalEffect && !depthEffect)
return -30;
if (directionalEffect) {
switch (root.positioning) {
case "top-right":
return -Math.max(directionalTravel * 0.65, 96);
case "custom":
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
return -directionalTravel;
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
return directionalTravel;
return 0;
default:
return -Math.max(directionalTravel, root.screenHeight * 0.24);
}
}
if (depthEffect) {
switch (root.positioning) {
case "top-right":
return -depthTravel * 0.75;
case "custom":
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
return -depthTravel;
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
return depthTravel;
return depthTravel * 0.45;
default:
return -depthTravel;
}
}
return root.animationOffset;
}
readonly property real computedScaleCollapsed: root.animationScaleCollapsed
QtObject {
id: morph
property real openProgress: root.shouldBeVisible ? 1 : 0
Behavior on openProgress {
enabled: root.animationsEnabled
NumberAnimation {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
}
readonly property real animX: root.frozenMotionOffsetX * (1 - morph.openProgress)
readonly property real animY: root.frozenMotionOffsetY * (1 - morph.openProgress)
readonly property real scaleValue: computedScaleCollapsed + (1.0 - computedScaleCollapsed) * morph.openProgress
onAnimXChanged: if (root.frameOwnsConnectedChrome)
root._queueAnimSync()
onAnimYChanged: if (root.frameOwnsConnectedChrome)
root._queueAnimSync()
Item { Item {
id: animatedContent id: contentContainer
anchors.fill: parent anchors.centerIn: parent
width: parent.width
height: parent.height
clip: false clip: false
property real publishedOpacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (root.shouldBeVisible ? 1 : 0) Item {
id: animatedContent
opacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (root.shouldBeVisible ? 1 : 0)
scale: modalContainer.scaleValue
transformOrigin: Item.Center
Behavior on opacity {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
NumberAnimation {
duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on publishedOpacity {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
NumberAnimation {
duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
ElevationShadow {
id: modalShadowLayer
anchors.fill: parent anchors.fill: parent
level: root.shadowLevel
fallbackOffset: root.shadowFallbackOffset
targetRadius: root.effectiveCornerRadius
targetColor: root.frameOwnsConnectedChrome ? "transparent" : root.effectiveBackgroundColor
borderColor: root.frameOwnsConnectedChrome ? "transparent" : root.effectiveBorderColor
borderWidth: root.frameOwnsConnectedChrome ? 0 : root.effectiveBorderWidth
shadowEnabled: !root.frameOwnsConnectedChrome && root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
}
Rectangle {
anchors.fill: parent
radius: root.effectiveCornerRadius
color: "transparent"
border.color: (root.connectedSurfaceOverride || root.frameOwnsConnectedChrome) ? "transparent" : BlurService.borderColor
border.width: (root.connectedSurfaceOverride || root.frameOwnsConnectedChrome) ? 0 : BlurService.borderWidth
z: 100
}
FocusScope {
anchors.fill: parent
focus: root.shouldBeVisible
clip: false clip: false
Item { property real publishedOpacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (root.shouldBeVisible ? 1 : 0)
id: directContentWrapper
opacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (root.shouldBeVisible ? 1 : 0)
scale: modalContainer.scaleValue
transformOrigin: Item.Center
Behavior on opacity {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
NumberAnimation {
duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on publishedOpacity {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect)
NumberAnimation {
duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
ElevationShadow {
id: modalShadowLayer
anchors.fill: parent anchors.fill: parent
visible: root.directContent !== null level: root.shadowLevel
focus: true fallbackOffset: root.shadowFallbackOffset
targetRadius: root.effectiveCornerRadius
targetColor: root.frameOwnsConnectedChrome ? "transparent" : root.effectiveBackgroundColor
borderColor: root.frameOwnsConnectedChrome ? "transparent" : root.effectiveBorderColor
borderWidth: root.frameOwnsConnectedChrome ? 0 : root.effectiveBorderWidth
shadowEnabled: !root.frameOwnsConnectedChrome && root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
}
Rectangle {
anchors.fill: parent
radius: root.effectiveCornerRadius
color: "transparent"
border.color: (root.connectedSurfaceOverride || root.frameOwnsConnectedChrome) ? "transparent" : BlurService.borderColor
border.width: (root.connectedSurfaceOverride || root.frameOwnsConnectedChrome) ? 0 : BlurService.borderWidth
z: 100
}
FocusScope {
anchors.fill: parent
focus: root.shouldBeVisible
clip: false clip: false
Component.onCompleted: { Item {
if (root.directContent) { id: directContentWrapper
root.directContent.parent = directContentWrapper; anchors.fill: parent
root.directContent.anchors.fill = directContentWrapper; visible: root.directContent !== null
Qt.callLater(() => root.directContent.forceActiveFocus()); focus: true
} clip: false
}
Connections { Component.onCompleted: {
target: root
function onDirectContentChanged() {
if (root.directContent) { if (root.directContent) {
root.directContent.parent = directContentWrapper; root.directContent.parent = directContentWrapper;
root.directContent.anchors.fill = directContentWrapper; root.directContent.anchors.fill = directContentWrapper;
Qt.callLater(() => root.directContent.forceActiveFocus()); Qt.callLater(() => root.directContent.forceActiveFocus());
} }
} }
Connections {
target: root
function onDirectContentChanged() {
if (root.directContent) {
root.directContent.parent = directContentWrapper;
root.directContent.anchors.fill = directContentWrapper;
Qt.callLater(() => root.directContent.forceActiveFocus());
}
}
}
} }
}
Loader { Loader {
id: contentLoader id: contentLoader
anchors.fill: parent anchors.fill: parent
active: root.directContent === null && (root.keepContentLoaded || root.shouldBeVisible || contentWindow.visible) active: root.directContent === null && (root.keepContentLoaded || root.shouldBeVisible || contentWindow.visible)
asynchronous: false asynchronous: false
focus: true focus: true
clip: false clip: false
visible: root.directContent === null visible: root.directContent === null
onLoaded: { onLoaded: {
if (item) { if (item) {
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
}
} }
} }
} }
@@ -205,6 +205,7 @@ Item {
id: clickCatcher id: clickCatcher
visible: false visible: false
color: "transparent" color: "transparent"
updatesEnabled: false
WlrLayershell.namespace: root.layerNamespace + ":clickcatcher" WlrLayershell.namespace: root.layerNamespace + ":clickcatcher"
WlrLayershell.layer: WlrLayershell.Top WlrLayershell.layer: WlrLayershell.Top
@@ -30,7 +30,6 @@ Item {
property string _pendingMode: "" property string _pendingMode: ""
readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose
// Animation state matches DankPopout/DankModal pattern
property bool animationsEnabled: true property bool animationsEnabled: true
property bool _motionActive: false property bool _motionActive: false
property real _frozenMotionX: 0 property real _frozenMotionX: 0
@@ -108,8 +107,6 @@ Item {
return SettingsData.frameEdgeInsetForSide(effectiveScreen, side); return SettingsData.frameEdgeInsetForSide(effectiveScreen, side);
} }
// frameEdgeInsetForSide is the full inset; do not add frameBarSize.
// Positions the modal flush to the emerge side, centered on the cross axis.
readonly property var _connectedModalPos: { readonly property var _connectedModalPos: {
const fallback = { const fallback = {
"x": (screenWidth - modalWidth) / 2, "x": (screenWidth - modalWidth) / 2,
@@ -175,8 +172,6 @@ Item {
readonly property int effectiveBorderWidth: connectedSurfaceOverride ? 0 : borderWidth readonly property int effectiveBorderWidth: connectedSurfaceOverride ? 0 : borderWidth
readonly property bool effectiveBlurEnabled: Theme.connectedSurfaceBlurEnabled readonly property bool effectiveBlurEnabled: Theme.connectedSurfaceBlurEnabled
// Shadow padding for the content window (render padding only, no motion padding).
// Zeroed when frame owns the chrome and Wayland clips past the bar edge
readonly property var shadowLevel: Theme.elevationLevel3 readonly property var shadowLevel: Theme.elevationLevel3
readonly property real shadowFallbackOffset: 6 readonly property real shadowFallbackOffset: 6
readonly property real shadowRenderPadding: (!frameOwnsConnectedChrome && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0 readonly property real shadowRenderPadding: (!frameOwnsConnectedChrome && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
@@ -203,29 +198,11 @@ Item {
} }
readonly property real contentSurfaceHeight: launcherArcExtenderActive ? _connectedChromeHeight : alignedHeight readonly property real contentSurfaceHeight: launcherArcExtenderActive ? _connectedChromeHeight : alignedHeight
// For directional/depth: window extends from screen top (content slides within) readonly property real _ccX: _connectedChromeX
// For standard: small window tightly around the modal + shadow padding readonly property real _ccY: _connectedChromeY
readonly property bool _needsExtendedWindow: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) || Theme.isDepthEffect
// Content window geometry
readonly property real _cwMarginLeft: Theme.snap(alignedX - shadowPad, dpr)
readonly property real _cwMarginTop: launcherArcExtenderActive ? _connectedChromeY : (_needsExtendedWindow ? 0 : Theme.snap(alignedY - shadowPad, dpr))
readonly property real _cwWidth: alignedWidth + shadowPad * 2
readonly property real _cwHeight: {
if (launcherArcExtenderActive)
return _connectedChromeHeight;
if (Theme.isDirectionalEffect && !Theme.isConnectedEffect)
return screenHeight + shadowPad;
if (Theme.isDepthEffect)
return alignedY + alignedHeight + shadowPad;
return alignedHeight + shadowPad * 2;
}
// Where the content container sits inside the content window
readonly property real _ccX: shadowPad
readonly property real _ccY: launcherArcExtenderActive ? 0 : (_needsExtendedWindow ? alignedY : shadowPad)
signal dialogClosed signal dialogClosed
// Coalesce per-channel dirty bits; one ConnectedModeState write per tick.
Timer { Timer {
id: _syncTimer id: _syncTimer
interval: 0 interval: 0
@@ -381,8 +358,6 @@ Item {
return; return;
contentVisible = true; contentVisible = true;
spotlightContent.closeTransientUi?.(); spotlightContent.closeTransientUi?.();
// NOTE: forceActiveFocus() is deliberately NOT called here.
// It is deferred to after animation starts to avoid compositor IPC stalls.
if (spotlightContent.searchField) { if (spotlightContent.searchField) {
spotlightContent.searchField.text = query; spotlightContent.searchField.text = query;
@@ -420,38 +395,29 @@ Item {
isClosing = false; isClosing = false;
openedFromOverview = false; openedFromOverview = false;
// Disable animations so the snap is instant
animationsEnabled = false; animationsEnabled = false;
// Freeze the collapsed offsets (they depend on height which could change)
_frozenMotionX = contentContainer ? contentContainer.collapsedMotionX : 0; _frozenMotionX = contentContainer ? contentContainer.collapsedMotionX : 0;
_frozenMotionY = contentContainer ? contentContainer.collapsedMotionY : (Theme.isDirectionalEffect ? Math.max(root.screenHeight - root._ccY + root.shadowPad, Theme.effectAnimOffset * 1.1) : -Theme.effectAnimOffset); _frozenMotionY = contentContainer ? contentContainer.collapsedMotionY : (Theme.isDirectionalEffect ? Math.max(root.screenHeight - root._ccY + root.shadowPad, Theme.effectAnimOffset * 1.1) : -Theme.effectAnimOffset);
var focusedScreen = CompositorService.getFocusedScreen(); var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen) { if (focusedScreen) {
backgroundWindow.screen = focusedScreen;
contentWindow.screen = focusedScreen; contentWindow.screen = focusedScreen;
} }
// _motionActive = false ensures motionX/Y snap to frozen collapsed position
_motionActive = false; _motionActive = false;
// Make windows visible but do NOT request keyboard focus yet
ModalManager.openModal(modalHandle); ModalManager.openModal(modalHandle);
spotlightOpen = true; spotlightOpen = true;
backgroundWindow.visible = true;
contentWindow.visible = true; contentWindow.visible = true;
// Load content and initialize (but no forceActiveFocus that's deferred)
_ensureContentLoadedAndInitialize(query || "", mode || ""); _ensureContentLoadedAndInitialize(query || "", mode || "");
// Frame 1: enable animations and trigger enter motion // Defer focus until after enter motion starts (avoids compositor IPC stalls).
Qt.callLater(() => { Qt.callLater(() => {
root.animationsEnabled = true; root.animationsEnabled = true;
root._motionActive = true; root._motionActive = true;
// Frame 2: request keyboard focus + activate search field
// Double-deferred to avoid compositor IPC competing with animation frames
Qt.callLater(() => { Qt.callLater(() => {
root.keyboardActive = true; root.keyboardActive = true;
if (root.spotlightContent && root.spotlightContent.searchField) if (root.spotlightContent && root.spotlightContent.searchField)
@@ -474,11 +440,9 @@ Item {
spotlightContent?.closeTransientUi?.(); spotlightContent?.closeTransientUi?.();
openedFromOverview = false; openedFromOverview = false;
isClosing = true; isClosing = true;
// For directional effects, defer contentVisible=false so content stays rendered during exit slide
if (!Theme.isDirectionalEffect) if (!Theme.isDirectionalEffect)
contentVisible = false; contentVisible = false;
// Trigger exit animation Behaviors will animate motionX/Y to frozen collapsed position
_motionActive = false; _motionActive = false;
keyboardActive = false; keyboardActive = false;
@@ -519,7 +483,6 @@ Item {
isClosing = false; isClosing = false;
contentVisible = false; contentVisible = false;
contentWindow.visible = false; contentWindow.visible = false;
backgroundWindow.visible = false;
if (root.unloadContentOnClose) if (root.unloadContentOnClose)
launcherContentLoader.active = false; launcherContentLoader.active = false;
dialogClosed(); dialogClosed();
@@ -588,7 +551,6 @@ Item {
root._releaseModalChrome(); root._releaseModalChrome();
root._windowEnabled = false; root._windowEnabled = false;
backgroundWindow.screen = newScreen;
contentWindow.screen = newScreen; contentWindow.screen = newScreen;
Qt.callLater(() => { Qt.callLater(() => {
root._windowEnabled = true; root._windowEnabled = true;
@@ -596,73 +558,6 @@ Item {
} }
} }
PanelWindow {
id: backgroundWindow
visible: false
color: "transparent"
readonly property real _topMargin: contentContainer.dockTop ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 0 ? Theme.px(42, root.dpr) : 0)
readonly property real _bottomMargin: contentContainer.dockBottom ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 1 ? Theme.px(42, root.dpr) : 0)
readonly property real _leftMargin: contentContainer.dockLeft ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 2 ? Theme.px(42, root.dpr) : 0)
readonly property real _rightMargin: contentContainer.dockRight ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 3 ? Theme.px(42, root.dpr) : 0)
WlrLayershell.namespace: "dms:spotlight:bg"
WlrLayershell.layer: root.effectiveLauncherLayer
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
WlrLayershell.margins {
top: backgroundWindow._topMargin
bottom: backgroundWindow._bottomMargin
left: backgroundWindow._leftMargin
right: backgroundWindow._rightMargin
}
anchors {
top: true
bottom: true
left: true
right: true
}
mask: Region {
item: (spotlightOpen || isClosing) ? bgFullScreenMask : null
Region {
item: bgContentHole
intersection: Intersection.Subtract
}
}
Item {
id: bgFullScreenMask
anchors.fill: parent
}
Item {
id: bgContentHole
visible: false
x: root._cwMarginLeft + contentContainer.x - backgroundWindow._leftMargin
y: root._cwMarginTop + contentContainer.y - backgroundWindow._topMargin
width: root.alignedWidth
height: root.contentSurfaceHeight
}
Rectangle {
id: backgroundDarken
anchors.fill: parent
color: "black"
opacity: 0
visible: false
}
MouseArea {
anchors.fill: parent
enabled: spotlightOpen
onClicked: root.hide()
}
}
PanelWindow { PanelWindow {
id: contentWindow id: contentWindow
visible: false visible: false
@@ -687,18 +582,26 @@ Item {
anchors { anchors {
left: true left: true
top: true top: true
right: true
bottom: true
} }
WlrLayershell.margins {
left: root._cwMarginLeft
top: root._cwMarginTop
}
implicitWidth: root._cwWidth
implicitHeight: root._cwHeight
mask: Region { mask: Region {
item: contentInputMask item: (root.spotlightOpen || root.isClosing) ? dismissArea : contentInputMask
Region {
item: (root.spotlightOpen || root.isClosing) ? contentInputMask : null
}
}
Item {
id: dismissArea
visible: false
anchors.fill: parent
anchors.topMargin: contentContainer.dockTop ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 0 ? Theme.px(42, root.dpr) : 0)
anchors.bottomMargin: contentContainer.dockBottom ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 1 ? Theme.px(42, root.dpr) : 0)
anchors.leftMargin: contentContainer.dockLeft ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 2 ? Theme.px(42, root.dpr) : 0)
anchors.rightMargin: contentContainer.dockRight ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 3 ? Theme.px(42, root.dpr) : 0)
} }
Item { Item {
@@ -710,16 +613,31 @@ Item {
height: root.contentSurfaceHeight height: root.contentSurfaceHeight
} }
MouseArea {
anchors.fill: dismissArea
enabled: root.spotlightOpen
z: -2
onClicked: root.hide()
}
Item { Item {
id: contentContainer id: contentContainer
// For directional/depth: contentContainer is at alignedY from window top (window starts at screen top)
// For standard: contentContainer is at shadowPad from window top (window starts near modal)
x: root._ccX x: root._ccX
y: root._ccY y: root._ccY
width: root.alignedWidth width: root.alignedWidth
height: root.contentSurfaceHeight height: root.contentSurfaceHeight
MouseArea {
anchors.fill: parent
enabled: root.spotlightOpen
hoverEnabled: false
acceptedButtons: Qt.AllButtons
onPressed: mouse.accepted = true
onClicked: mouse.accepted = true
z: -1
}
readonly property int dockEdge: typeof SettingsData !== "undefined" ? SettingsData.dockPosition : 1 readonly property int dockEdge: typeof SettingsData !== "undefined" ? SettingsData.dockPosition : 1
readonly property bool dockTop: dockEdge === 0 readonly property bool dockTop: dockEdge === 0
readonly property bool dockBottom: dockEdge === 1 readonly property bool dockBottom: dockEdge === 1
@@ -774,7 +692,6 @@ Item {
return -Math.max((root.shadowPad || 0) + Theme.effectAnimOffset, 40); return -Math.max((root.shadowPad || 0) + Theme.effectAnimOffset, 40);
} }
// openProgress: 0 = closed (at frozenMotion, scaleCollapsed), 1 = open (at 0, scale 1).
QtObject { QtObject {
id: morph id: morph
property real openProgress: root._motionActive ? 1 : 0 property real openProgress: root._motionActive ? 1 : 0
@@ -833,7 +750,6 @@ Item {
width: contentContainer.width width: contentContainer.width
height: contentContainer.height height: contentContainer.height
// Shadow mirrors contentWrapper position/scale/opacity
ElevationShadow { ElevationShadow {
id: launcherShadowLayer id: launcherShadowLayer
width: parent.width width: parent.width
@@ -851,7 +767,6 @@ Item {
shadowEnabled: !root.frameOwnsConnectedChrome && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" shadowEnabled: !root.frameOwnsConnectedChrome && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
} }
// contentWrapper moves inside static contentContainer DankPopout pattern
Item { Item {
id: contentWrapper id: contentWrapper
width: parent.width width: parent.width
@@ -84,14 +84,14 @@ Item {
readonly property real alignedX: Theme.snap(modalX, dpr) readonly property real alignedX: Theme.snap(modalX, dpr)
readonly property real alignedY: Theme.snap(modalY, dpr) readonly property real alignedY: Theme.snap(modalY, dpr)
// Extra headroom above the window for the slide-in animation // Extra headroom above the content for the slide-in animation
readonly property real _animHeadroom: 16 readonly property real _animHeadroom: 16
readonly property real windowX: Math.max(0, Theme.snap(alignedX - shadowPad, dpr)) readonly property real windowX: Math.max(0, Theme.snap(alignedX - shadowPad, dpr))
readonly property real windowY: Math.max(0, Theme.snap(alignedY - shadowPad - _animHeadroom, dpr)) readonly property real windowY: Math.max(0, Theme.snap(alignedY - shadowPad - _animHeadroom, dpr))
readonly property real contentX: Theme.snap(alignedX - windowX, dpr) readonly property real contentX: Theme.snap(alignedX - windowX, dpr)
readonly property real contentY: Theme.snap(alignedY - windowY, dpr) readonly property real contentY: Theme.snap(alignedY - windowY, dpr)
readonly property real windowWidth: alignedWidth + contentX + shadowPad
readonly property real _animatedContentH: Theme.snap(_contentImplicitH, dpr) readonly property real _animatedContentH: Theme.snap(_contentImplicitH, dpr)
readonly property real windowWidth: alignedWidth + contentX + shadowPad
readonly property real windowHeight: _animatedContentH + contentY + shadowPad + _animHeadroom readonly property real windowHeight: _animatedContentH + contentY + shadowPad + _animHeadroom
readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
@@ -114,6 +114,7 @@ Item {
} }
} }
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0 readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackgroundDarken
signal dialogClosed signal dialogClosed
@@ -267,8 +268,9 @@ Item {
PanelWindow { PanelWindow {
id: clickCatcher id: clickCatcher
screen: launcherWindow.screen screen: launcherWindow.screen
visible: (spotlightOpen || isClosing) && !root.useBackgroundDarken visible: (spotlightOpen || isClosing) && !root.useSingleWindow
color: "transparent" color: "transparent"
updatesEnabled: false
WlrLayershell.namespace: "dms:spotlight:clickcatcher" WlrLayershell.namespace: "dms:spotlight:clickcatcher"
WlrLayershell.layer: root.effectiveLauncherLayer WlrLayershell.layer: root.effectiveLauncherLayer
@@ -339,19 +341,19 @@ Item {
anchors { anchors {
top: true top: true
left: true left: true
right: root.useBackgroundDarken right: root.useSingleWindow
bottom: root.useBackgroundDarken bottom: root.useSingleWindow
} }
WlrLayershell.margins { WlrLayershell.margins {
left: root.useBackgroundDarken ? 0 : root.windowX left: root.useSingleWindow ? 0 : root.windowX
top: root.useBackgroundDarken ? 0 : root.windowY top: root.useSingleWindow ? 0 : root.windowY
right: 0 right: 0
bottom: 0 bottom: 0
} }
implicitWidth: root.useBackgroundDarken ? 0 : root.windowWidth implicitWidth: root.useSingleWindow ? 0 : root.windowWidth
implicitHeight: root.useBackgroundDarken ? 0 : root.windowHeight implicitHeight: root.useSingleWindow ? 0 : root.windowHeight
mask: Region { mask: Region {
item: inputMask item: inputMask
@@ -361,15 +363,15 @@ Item {
id: inputMask id: inputMask
visible: false visible: false
color: "transparent" color: "transparent"
x: root.useBackgroundDarken ? 0 : modalContainer.x x: root.useSingleWindow ? 0 : modalContainer.x
y: root.useBackgroundDarken ? 0 : modalContainer.y + modalContainer.slideOffset y: root.useSingleWindow ? 0 : modalContainer.y + modalContainer.slideOffset
width: root.useBackgroundDarken ? launcherWindow.width : root.alignedWidth width: root.useSingleWindow ? launcherWindow.width : root.alignedWidth
height: root.useBackgroundDarken ? launcherWindow.height : root._contentImplicitH height: root.useSingleWindow ? launcherWindow.height : root._contentImplicitH
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
enabled: root.useBackgroundDarken && spotlightOpen enabled: root.useSingleWindow && spotlightOpen
z: -2 z: -2
onClicked: root.hide() onClicked: root.hide()
} }
@@ -393,13 +395,23 @@ Item {
Item { Item {
id: modalContainer id: modalContainer
x: root.useBackgroundDarken ? root.alignedX : root.contentX x: root.useSingleWindow ? root.alignedX : root.contentX
y: root.useBackgroundDarken ? root.alignedY : root.contentY y: root.useSingleWindow ? root.alignedY : root.contentY
width: root.alignedWidth width: root.alignedWidth
height: root._animatedContentH height: root._animatedContentH
visible: _renderActive visible: _renderActive
z: 0 z: 0
MouseArea {
anchors.fill: parent
enabled: spotlightOpen
hoverEnabled: false
acceptedButtons: Qt.AllButtons
onPressed: mouse.accepted = true
onClicked: mouse.accepted = true
z: -1
}
property bool _renderActive: contentVisible property bool _renderActive: contentVisible
property real slideOffset: contentVisible ? 0 : -root._animHeadroom property real slideOffset: contentVisible ? 0 : -root._animHeadroom
@@ -80,6 +80,7 @@ Item {
readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
readonly property bool useBackgroundDarken: !SettingsData.frameEnabled && SettingsData.modalDarkenBackground readonly property bool useBackgroundDarken: !SettingsData.frameEnabled && SettingsData.modalDarkenBackground
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackgroundDarken
readonly property bool usesOverlayLayer: useBackgroundDarken || SettingsData.launcherUseOverlayLayer || triggerUsesOverlayLayer readonly property bool usesOverlayLayer: useBackgroundDarken || SettingsData.launcherUseOverlayLayer || triggerUsesOverlayLayer
readonly property var effectiveLauncherLayer: LayerShell.fromEnv("DMS_MODAL_LAYER", root.usesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top, { readonly property var effectiveLauncherLayer: LayerShell.fromEnv("DMS_MODAL_LAYER", root.usesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top, {
"allow": ["top", "overlay"], "allow": ["top", "overlay"],
@@ -303,8 +304,9 @@ Item {
PanelWindow { PanelWindow {
id: clickCatcher id: clickCatcher
screen: launcherWindow.screen screen: launcherWindow.screen
visible: (spotlightOpen || isClosing) && !root.useBackgroundDarken visible: (spotlightOpen || isClosing) && !root.useSingleWindow
color: "transparent" color: "transparent"
updatesEnabled: false
WlrLayershell.namespace: "dms:spotlight:clickcatcher" WlrLayershell.namespace: "dms:spotlight:clickcatcher"
WlrLayershell.layer: root.effectiveLauncherLayer WlrLayershell.layer: root.effectiveLauncherLayer
@@ -375,19 +377,19 @@ Item {
anchors { anchors {
top: true top: true
left: true left: true
right: root.useBackgroundDarken right: root.useSingleWindow
bottom: root.useBackgroundDarken bottom: root.useSingleWindow
} }
WlrLayershell.margins { WlrLayershell.margins {
left: root.useBackgroundDarken ? 0 : root.windowX left: root.useSingleWindow ? 0 : root.windowX
top: root.useBackgroundDarken ? 0 : root.windowY top: root.useSingleWindow ? 0 : root.windowY
right: 0 right: 0
bottom: 0 bottom: 0
} }
implicitWidth: root.useBackgroundDarken ? 0 : root.windowWidth implicitWidth: root.useSingleWindow ? 0 : root.windowWidth
implicitHeight: root.useBackgroundDarken ? 0 : root.windowHeight implicitHeight: root.useSingleWindow ? 0 : root.windowHeight
mask: Region { mask: Region {
item: launcherInputMask item: launcherInputMask
@@ -397,15 +399,15 @@ Item {
id: launcherInputMask id: launcherInputMask
visible: false visible: false
color: "transparent" color: "transparent"
x: root.useBackgroundDarken ? 0 : modalContainer.x x: root.useSingleWindow ? 0 : modalContainer.x
y: root.useBackgroundDarken ? 0 : modalContainer.y y: root.useSingleWindow ? 0 : modalContainer.y
width: root.useBackgroundDarken ? launcherWindow.width : modalContainer.width width: root.useSingleWindow ? launcherWindow.width : modalContainer.width
height: root.useBackgroundDarken ? launcherWindow.height : modalContainer.height height: root.useSingleWindow ? launcherWindow.height : modalContainer.height
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
enabled: root.useBackgroundDarken && spotlightOpen enabled: root.useSingleWindow && spotlightOpen
z: -2 z: -2
onClicked: root.hide() onClicked: root.hide()
} }
@@ -429,13 +431,23 @@ Item {
Item { Item {
id: modalContainer id: modalContainer
x: root.useBackgroundDarken ? root.alignedX : root.contentX x: root.useSingleWindow ? root.alignedX : root.contentX
y: root.useBackgroundDarken ? root.alignedY : root.contentY y: root.useSingleWindow ? root.alignedY : root.contentY
width: root.alignedWidth width: root.alignedWidth
height: root.alignedHeight height: root.alignedHeight
visible: _renderActive visible: _renderActive
z: 0 z: 0
MouseArea {
anchors.fill: parent
enabled: spotlightOpen
hoverEnabled: false
acceptedButtons: Qt.AllButtons
onPressed: mouse.accepted = true
onClicked: mouse.accepted = true
z: -1
}
property bool _renderActive: contentVisible property bool _renderActive: contentVisible
property real publishedScale: contentVisible ? 1 : 0.96 property real publishedScale: contentVisible ? 1 : 0.96
property real publishedOpacity: contentVisible ? 1 : 0 property real publishedOpacity: contentVisible ? 1 : 0
@@ -320,8 +320,6 @@ Item {
url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings"; url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings";
else if (CompositorService.isHyprland) else if (CompositorService.isHyprland)
url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings-1"; url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings-1";
else if (CompositorService.isDwl)
url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings-2";
else if (CompositorService.isMango) else if (CompositorService.isMango)
url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings-2"; url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings-2";
Qt.openUrlExternally(url); Qt.openUrlExternally(url);
@@ -130,7 +130,7 @@ Item {
title: I18n.tr("Multi-Monitor", "greeter feature card title") title: I18n.tr("Multi-Monitor", "greeter feature card title")
description: I18n.tr("Per-screen config", "greeter feature card description") description: I18n.tr("Per-screen config", "greeter feature card description")
onClicked: { onClicked: {
const hasDisplayConfig = CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango; const hasDisplayConfig = CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango;
PopoutService.openSettingsWithTab(hasDisplayConfig ? "display_config" : "display_widgets"); PopoutService.openSettingsWithTab(hasDisplayConfig ? "display_config" : "display_widgets");
} }
} }
+41 -6
View File
@@ -98,7 +98,7 @@ FocusScope {
visible: active visible: active
focus: active focus: active
sourceComponent: CompositorTab {} sourceComponent: WorkspacesTab {}
onActiveChanged: { onActiveChanged: {
if (active && item) if (active && item)
@@ -106,6 +106,44 @@ FocusScope {
} }
} }
Loader {
id: compositorLayoutLoader
anchors.fill: parent
active: root.currentIndex === 37
visible: active
focus: active
sourceComponent: CompositorLayoutTab {}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
Loader {
id: windowRulesLoader
property bool loadedOnce: false
anchors.fill: parent
active: root.currentIndex === 38 || loadedOnce
visible: root.currentIndex === 38 && status === Loader.Ready
focus: visible
asynchronous: true
sourceComponent: WindowRulesTab {
pageActive: root.currentIndex === 38
}
onLoaded: loadedOnce = true
}
DankSpinner {
anchors.centerIn: parent
visible: root.currentIndex === 38 && windowRulesLoader.status === Loader.Loading
}
Loader { Loader {
id: dankBarAppearanceLoader id: dankBarAppearanceLoader
anchors.fill: parent anchors.fill: parent
@@ -388,7 +426,7 @@ FocusScope {
} }
} }
Loader { Loader {
id: defaultAppsLoader id: defaultAppsLoader
anchors.fill: parent anchors.fill: parent
active: root.currentIndex === 34 active: root.currentIndex === 34
@@ -474,12 +512,9 @@ FocusScope {
} }
} }
StyledText { DankSpinner {
anchors.centerIn: parent anchors.centerIn: parent
visible: root.currentIndex === 22 && widgetsLoader.status === Loader.Loading visible: root.currentIndex === 22 && widgetsLoader.status === Loader.Loading
text: I18n.tr("Loading...", "loading indicator")
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeMedium
} }
Loader { Loader {
+24 -8
View File
@@ -102,6 +102,13 @@ Rectangle {
"icon": "volume_up", "icon": "volume_up",
"tabIndex": 15, "tabIndex": 15,
"soundsOnly": true "soundsOnly": true
},
{
"id": "compositor_layout",
"text": CompositorService.isNiri ? "niri" : (CompositorService.isHyprland ? "Hyprland" : "MangoWC"),
"icon": "crop_square",
"tabIndex": 37,
"layoutCapable": true
} }
] ]
}, },
@@ -128,6 +135,12 @@ Rectangle {
"icon": "widgets", "icon": "widgets",
"tabIndex": 22 "tabIndex": 22
}, },
{
"id": "workspaces",
"text": I18n.tr("Workspaces"),
"icon": "view_module",
"tabIndex": 4
},
{ {
"id": "frame", "id": "frame",
"text": I18n.tr("Frame"), "text": I18n.tr("Frame"),
@@ -188,12 +201,6 @@ Rectangle {
} }
] ]
}, },
{
"id": "compositor",
"text": I18n.tr("Compositor"),
"icon": "layers",
"tabIndex": 4
},
{ {
"id": "keybinds", "id": "keybinds",
"text": I18n.tr("Keyboard Shortcuts"), "text": I18n.tr("Keyboard Shortcuts"),
@@ -259,6 +266,13 @@ Rectangle {
"icon": "line_start", "icon": "line_start",
"tabIndex": 36, "tabIndex": 36,
"autostartOnly": true "autostartOnly": true
},
{
"id": "window_rules",
"text": I18n.tr("Window Rules"),
"icon": "select_window",
"tabIndex": 38,
"windowRulesCapable": true
} }
] ]
}, },
@@ -372,6 +386,8 @@ Rectangle {
return false; return false;
if (item.windowRulesCapable && !CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango) if (item.windowRulesCapable && !CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango)
return false; return false;
if (item.layoutCapable && !CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango)
return false;
if (item.niriOnly && !CompositorService.isNiri) if (item.niriOnly && !CompositorService.isNiri)
return false; return false;
if (item.clipboardOnly && (!DMSService.isConnected || DMSService.apiVersion < 23)) if (item.clipboardOnly && (!DMSService.isConnected || DMSService.apiVersion < 23))
@@ -544,8 +560,8 @@ Rectangle {
return -1; return -1;
var normalized = name.toLowerCase().replace(/[_\-\s]/g, ""); var normalized = name.toLowerCase().replace(/[_\-\s]/g, "");
if (normalized === "workspaces") if (normalized === "compositor")
normalized = "compositor"; normalized = "workspaces";
for (var i = 0; i < categoryStructure.length; i++) { for (var i = 0; i < categoryStructure.length; i++) {
var cat = categoryStructure[i]; var cat = categoryStructure[i];
@@ -60,7 +60,7 @@ Rectangle {
} }
Typography { Typography {
text: DgopService.uptime ? I18n.tr("up") + " " + DgopService.uptime.slice(3) : I18n.tr("Unknown") text: DgopService.uptime ? I18n.tr("up", "uptime prefix, e.g. 'up 4h 2m'") + " " + DgopService.uptime.slice(3) : I18n.tr("Unknown")
style: Typography.Style.Caption style: Typography.Style.Caption
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
} }
-4
View File
@@ -108,8 +108,6 @@ Item {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
focusedScreenName = focusedWs?.monitor?.name || ""; focusedScreenName = focusedWs?.monitor?.name || "";
} else if (CompositorService.isDwl && DwlService.activeOutput) {
focusedScreenName = DwlService.activeOutput;
} else if (CompositorService.isMango && MangoService.activeOutput) { } else if (CompositorService.isMango && MangoService.activeOutput) {
focusedScreenName = MangoService.activeOutput; focusedScreenName = MangoService.activeOutput;
} }
@@ -139,8 +137,6 @@ Item {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
focusedScreenName = focusedWs?.monitor?.name || ""; focusedScreenName = focusedWs?.monitor?.name || "";
} else if (CompositorService.isDwl && DwlService.activeOutput) {
focusedScreenName = DwlService.activeOutput;
} else if (CompositorService.isMango && MangoService.activeOutput) { } else if (CompositorService.isMango && MangoService.activeOutput) {
focusedScreenName = MangoService.activeOutput; focusedScreenName = MangoService.activeOutput;
} }
+10 -11
View File
@@ -29,7 +29,6 @@ Item {
readonly property real _frameEdgeFloorInset: (SettingsData.frameEnabled && _usesFrameBarChrome) ? Math.max(0, SettingsData.frameThickness - _edgeBaseMargin) : 0 readonly property real _frameEdgeFloorInset: (SettingsData.frameEnabled && _usesFrameBarChrome) ? Math.max(0, SettingsData.frameThickness - _edgeBaseMargin) : 0
readonly property bool _barIsVertical: _hasBarWindow ? barWindow.isVertical : false readonly property bool _barIsVertical: _hasBarWindow ? barWindow.isVertical : false
readonly property string _barScreenName: _hasBarWindow ? (barWindow.screenName || "") : "" readonly property string _barScreenName: _hasBarWindow ? (barWindow.screenName || "") : ""
readonly property var dwlSvc: CompositorService.isMango ? MangoService : DwlService
readonly property bool hasAdjacentTopBarLive: _hasBarWindow && barWindow.hasAdjacentTopBar readonly property bool hasAdjacentTopBarLive: _hasBarWindow && barWindow.hasAdjacentTopBar
readonly property bool hasAdjacentBottomBarLive: _hasBarWindow && barWindow.hasAdjacentBottomBar readonly property bool hasAdjacentBottomBarLive: _hasBarWindow && barWindow.hasAdjacentBottomBar
readonly property bool hasAdjacentLeftBarLive: _hasBarWindow && barWindow.hasAdjacentLeftBar readonly property bool hasAdjacentLeftBarLive: _hasBarWindow && barWindow.hasAdjacentLeftBar
@@ -190,16 +189,16 @@ Item {
} }
return monitorWorkspaces.sort((a, b) => a.id - b.id); return monitorWorkspaces.sort((a, b) => a.id - b.id);
} else if (CompositorService.isDwl || CompositorService.isMango) { } else if (CompositorService.isMango) {
if (!dwlSvc.available) { if (!MangoService.available) {
return [0]; return [0];
} }
if (SettingsData.dwlShowAllTags) { if (SettingsData.dwlShowAllTags) {
return Array.from({ return Array.from({
length: dwlSvc.tagCount length: MangoService.tagCount
}, (_, i) => i); }, (_, i) => i);
} }
return dwlSvc.getVisibleTags(screenName); return MangoService.getVisibleTags(screenName);
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const workspaces = I3.workspaces?.values || []; const workspaces = I3.workspaces?.values || [];
if (workspaces.length === 0) if (workspaces.length === 0)
@@ -235,13 +234,13 @@ Item {
const monitors = Hyprland.monitors?.values || []; const monitors = Hyprland.monitors?.values || [];
const currentMonitor = monitors.find(monitor => monitor.name === screenName); const currentMonitor = monitors.find(monitor => monitor.name === screenName);
return currentMonitor?.activeWorkspace?.id ?? 1; return currentMonitor?.activeWorkspace?.id ?? 1;
} else if (CompositorService.isDwl || CompositorService.isMango) { } else if (CompositorService.isMango) {
if (!dwlSvc.available) if (!MangoService.available)
return 0; return 0;
const outputState = dwlSvc.getOutputState(screenName); const outputState = MangoService.getOutputState(screenName);
if (!outputState || !outputState.tags) if (!outputState || !outputState.tags)
return 0; return 0;
const activeTags = dwlSvc.getActiveTags(screenName); const activeTags = MangoService.getActiveTags(screenName);
return activeTags.length > 0 ? activeTags[0] : 0; return activeTags.length > 0 ? activeTags[0] : 0;
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
if (!screenName || SettingsData.workspaceFollowFocus) { if (!screenName || SettingsData.workspaceFollowFocus) {
@@ -283,14 +282,14 @@ Item {
if (nextIndex !== validIndex) { if (nextIndex !== validIndex) {
HyprlandService.focusWorkspace(realWorkspaces[nextIndex].id); HyprlandService.focusWorkspace(realWorkspaces[nextIndex].id);
} }
} else if (CompositorService.isDwl || CompositorService.isMango) { } else if (CompositorService.isMango) {
const currentTag = getCurrentWorkspace(); const currentTag = getCurrentWorkspace();
const currentIndex = realWorkspaces.findIndex(tag => tag === currentTag); const currentIndex = realWorkspaces.findIndex(tag => tag === currentTag);
const validIndex = currentIndex === -1 ? 0 : currentIndex; const validIndex = currentIndex === -1 ? 0 : currentIndex;
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0); const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0);
if (nextIndex !== validIndex) { if (nextIndex !== validIndex) {
dwlSvc.switchToTag(_barScreenName, realWorkspaces[nextIndex]); MangoService.switchToTag(_barScreenName, realWorkspaces[nextIndex]);
} }
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const currentWs = getCurrentWorkspace(); const currentWs = getCurrentWorkspace();
@@ -10,9 +10,7 @@ DankPopout {
property var triggerScreen: null property var triggerScreen: null
// mango shares dwl's layout model; route to the right service. readonly property bool isMango: CompositorService.isMango
readonly property bool isDwlLike: CompositorService.isDwl || CompositorService.isMango
readonly property var dwlSvc: CompositorService.isMango ? MangoService : DwlService
function setTriggerPosition(x, y, width, section, screen, barPosition, barThickness, barSpacing, barConfig) { function setTriggerPosition(x, y, width, section, screen, barPosition, barThickness, barSpacing, barConfig) {
triggerX = x; triggerX = x;
@@ -37,8 +35,8 @@ DankPopout {
onScreenChanged: updateOutputState() onScreenChanged: updateOutputState()
function updateOutputState() { function updateOutputState() {
if (screen && root.dwlSvc.available) { if (screen && MangoService.available) {
outputState = root.dwlSvc.getOutputState(screen.name); outputState = MangoService.getOutputState(screen.name);
} else { } else {
outputState = null; outputState = null;
} }
@@ -84,7 +82,7 @@ DankPopout {
} }
Connections { Connections {
target: DwlService target: MangoService
function onStateChanged() { function onStateChanged() {
updateOutputState(); updateOutputState();
} }
@@ -219,7 +217,7 @@ DankPopout {
spacing: Theme.spacingS spacing: Theme.spacingS
Repeater { Repeater {
model: root.dwlSvc.layouts model: MangoService.layouts
delegate: Rectangle { delegate: Rectangle {
required property string modelData required property string modelData
@@ -273,11 +271,11 @@ DankPopout {
if (!root.triggerScreen) { if (!root.triggerScreen) {
return; return;
} }
if (!root.dwlSvc.available) { if (!MangoService.available) {
return; return;
} }
root.dwlSvc.setLayout(root.triggerScreen.name, index); MangoService.setLayout(root.triggerScreen.name, index);
root.close(); root.close();
} }
} }
+1 -1
View File
@@ -282,7 +282,7 @@ Loader {
"cpuTemp": dgopAvailable, "cpuTemp": dgopAvailable,
"gpuTemp": dgopAvailable, "gpuTemp": dgopAvailable,
"network_speed_monitor": dgopAvailable, "network_speed_monitor": dgopAvailable,
"layout": (CompositorService.isDwl && DwlService.dwlAvailable) || (CompositorService.isMango && MangoService.available) "layout": CompositorService.isMango && MangoService.available
}; };
return widgetVisibility[widgetId] ?? true; return widgetVisibility[widgetId] ?? true;
@@ -13,12 +13,11 @@ BasePill {
signal toggleLayoutPopup signal toggleLayoutPopup
// mango shares dwl's tag/layout model; route to the right service. // mango shares dwl's tag/layout model; route to the right service.
readonly property bool isDwlLike: CompositorService.isDwl || CompositorService.isMango readonly property bool isMango: CompositorService.isMango
readonly property var dwlSvc: CompositorService.isMango ? MangoService : DwlService
visible: layout.isDwlLike && layout.dwlSvc.available visible: layout.isMango && MangoService.available
property var outputState: parentScreen ? layout.dwlSvc.getOutputState(parentScreen.name) : null property var outputState: parentScreen ? MangoService.getOutputState(parentScreen.name) : null
property string currentLayoutSymbol: outputState?.layoutSymbol || "" property string currentLayoutSymbol: outputState?.layoutSymbol || ""
property int currentLayoutIndex: outputState?.layout || 0 property int currentLayoutIndex: outputState?.layout || 0
@@ -41,9 +40,9 @@ BasePill {
} }
Connections { Connections {
target: layout.dwlSvc target: MangoService
function onStateChanged() { function onStateChanged() {
outputState = parentScreen ? layout.dwlSvc.getOutputState(parentScreen.name) : null; outputState = parentScreen ? MangoService.getOutputState(parentScreen.name) : null;
} }
} }
@@ -101,13 +100,13 @@ BasePill {
} }
onRightClicked: { onRightClicked: {
if (!parentScreen || !layout.dwlSvc.available || layout.dwlSvc.layouts.length === 0) { if (!parentScreen || !MangoService.available || MangoService.layouts.length === 0) {
return; return;
} }
const currentIndex = layout.currentLayoutIndex; const currentIndex = layout.currentLayoutIndex;
const nextIndex = (currentIndex + 1) % layout.dwlSvc.layouts.length; const nextIndex = (currentIndex + 1) % MangoService.layouts.length;
layout.dwlSvc.setLayout(parentScreen.name, nextIndex); MangoService.setLayout(parentScreen.name, nextIndex);
} }
} }
@@ -112,8 +112,6 @@ BasePill {
property string currentLayout: { property string currentLayout: {
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
return NiriService.getCurrentKeyboardLayoutName(); return NiriService.getCurrentKeyboardLayoutName();
} else if (CompositorService.isDwl) {
return DwlService.currentKeyboardLayout;
} else if (CompositorService.isMango) { } else if (CompositorService.isMango) {
return MangoService.currentKeyboardLayout; return MangoService.currentKeyboardLayout;
} }
@@ -209,8 +207,6 @@ BasePill {
NiriService.cycleKeyboardLayout(); NiriService.cycleKeyboardLayout();
} else if (CompositorService.isHyprland) { } else if (CompositorService.isHyprland) {
Quickshell.execDetached(["hyprctl", "switchxkblayout", root.hyprlandKeyboard, "next"]); Quickshell.execDetached(["hyprctl", "switchxkblayout", root.hyprlandKeyboard, "next"]);
} else if (CompositorService.isDwl) {
Quickshell.execDetached(["mmsg", "dispatch", "switch_keyboard_layout"]);
} else if (CompositorService.isMango) { } else if (CompositorService.isMango) {
MangoService.cycleKeyboardLayout(); MangoService.cycleKeyboardLayout();
} }
@@ -55,7 +55,7 @@ BasePill {
} }
IconImage { IconImage {
visible: SettingsData.launcherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isLabwc) visible: SettingsData.launcherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isLabwc)
anchors.centerIn: parent anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
@@ -66,8 +66,6 @@ BasePill {
return "file://" + Theme.shellDir + "/assets/niri.svg"; return "file://" + Theme.shellDir + "/assets/niri.svg";
} else if (CompositorService.isHyprland) { } else if (CompositorService.isHyprland) {
return "file://" + Theme.shellDir + "/assets/hyprland.svg"; return "file://" + Theme.shellDir + "/assets/hyprland.svg";
} else if (CompositorService.isDwl) {
return "file://" + Theme.shellDir + "/assets/mango.png";
} else if (CompositorService.isMango) { } else if (CompositorService.isMango) {
return "file://" + Theme.shellDir + "/assets/mango.png"; return "file://" + Theme.shellDir + "/assets/mango.png";
} else if (CompositorService.isSway) { } else if (CompositorService.isSway) {
@@ -22,10 +22,7 @@ Item {
property var hyprlandOverviewLoader: null property var hyprlandOverviewLoader: null
property var parentScreen: null property var parentScreen: null
// mango shares dwl's tag model; route to the right service so one set of readonly property bool isMango: CompositorService.isMango
// branches serves both.
readonly property bool isDwlLike: CompositorService.isDwl || CompositorService.isMango
readonly property var dwlSvc: CompositorService.isMango ? MangoService : DwlService
readonly property real _leftMargin: { readonly property real _leftMargin: {
if (isVertical) if (isVertical)
@@ -80,9 +77,8 @@ Item {
return NiriService.currentOutput || root.screenName; return NiriService.currentOutput || root.screenName;
case "hyprland": case "hyprland":
return Hyprland.focusedWorkspace?.monitor?.name || root.screenName; return Hyprland.focusedWorkspace?.monitor?.name || root.screenName;
case "dwl":
case "mango": case "mango":
return root.dwlSvc.activeOutput || root.screenName; return MangoService.activeOutput || root.screenName;
case "sway": case "sway":
case "scroll": case "scroll":
case "miracle": case "miracle":
@@ -101,7 +97,6 @@ Item {
switch (CompositorService.compositor) { switch (CompositorService.compositor) {
case "niri": case "niri":
case "hyprland": case "hyprland":
case "dwl":
case "mango": case "mango":
case "sway": case "sway":
case "scroll": case "scroll":
@@ -128,7 +123,6 @@ Item {
return getNiriActiveWorkspace(); return getNiriActiveWorkspace();
case "hyprland": case "hyprland":
return getHyprlandActiveWorkspace(); return getHyprlandActiveWorkspace();
case "dwl":
case "mango": case "mango":
const activeTags = getDwlActiveTags(); const activeTags = getDwlActiveTags();
return activeTags.length > 0 ? activeTags[0] : -1; return activeTags.length > 0 ? activeTags[0] : -1;
@@ -141,7 +135,7 @@ Item {
} }
} }
property var dwlActiveTags: { property var dwlActiveTags: {
if (root.isDwlLike) { if (root.isMango) {
return getDwlActiveTags(); return getDwlActiveTags();
} }
return []; return [];
@@ -160,9 +154,6 @@ Item {
case "hyprland": case "hyprland":
baseList = getHyprlandWorkspaces(); baseList = getHyprlandWorkspaces();
break; break;
case "dwl":
baseList = getDwlTags();
break;
case "mango": case "mango":
if (root.mangoOverviewActive) if (root.mangoOverviewActive)
return []; return [];
@@ -302,7 +293,7 @@ Item {
} }
} else if (CompositorService.isHyprland) { } else if (CompositorService.isHyprland) {
targetWorkspaceId = ws.id !== undefined ? ws.id : ws; targetWorkspaceId = ws.id !== undefined ? ws.id : ws;
} else if (root.isDwlLike) { } else if (root.isMango) {
if (typeof ws !== "object" || ws.tag === undefined) { if (typeof ws !== "object" || ws.tag === undefined) {
return []; return [];
} }
@@ -322,8 +313,8 @@ Item {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
isActiveWs = focusedWs ? (focusedWs.num === targetWorkspaceId) : false; isActiveWs = focusedWs ? (focusedWs.num === targetWorkspaceId) : false;
} else if (root.isDwlLike) { } else if (root.isMango) {
const output = root.dwlSvc.getOutputState(root.effectiveScreenName); const output = MangoService.getOutputState(root.effectiveScreenName);
if (output && output.tags) { if (output && output.tags) {
const tag = output.tags.find(t => t.tag === targetWorkspaceId); const tag = output.tags.find(t => t.tag === targetWorkspaceId);
isActiveWs = tag ? (tag.state === 1) : false; isActiveWs = tag ? (tag.state === 1) : false;
@@ -411,7 +402,7 @@ Item {
"id": -1, "id": -1,
"name": "" "name": ""
}; };
} else if (root.isDwlLike) { } else if (root.isMango) {
placeholder = { placeholder = {
"tag": -1 "tag": -1
}; };
@@ -493,11 +484,11 @@ Item {
} }
function getDwlTags() { function getDwlTags() {
if (!root.dwlSvc.available) if (!MangoService.available)
return []; return [];
const targetScreen = root.effectiveScreenName; const targetScreen = root.effectiveScreenName;
const output = root.dwlSvc.getOutputState(targetScreen); const output = MangoService.getOutputState(targetScreen);
if (!output || !output.tags || output.tags.length === 0) if (!output || !output.tags || output.tags.length === 0)
return []; return [];
@@ -510,7 +501,7 @@ Item {
})); }));
} }
const visibleTagIndices = root.dwlSvc.getVisibleTags(targetScreen); const visibleTagIndices = MangoService.getVisibleTags(targetScreen);
return visibleTagIndices.map(tagIndex => { return visibleTagIndices.map(tagIndex => {
const tagData = output.tags.find(t => t.tag === tagIndex); const tagData = output.tags.find(t => t.tag === tagIndex);
return { return {
@@ -523,10 +514,10 @@ Item {
} }
function getDwlActiveTags() { function getDwlActiveTags() {
if (!root.dwlSvc.available) if (!MangoService.available)
return []; return [];
return root.dwlSvc.getActiveTags(root.effectiveScreenName); return MangoService.getActiveTags(root.effectiveScreenName);
} }
function getExtWorkspaceWorkspaces() { function getExtWorkspaceWorkspaces() {
@@ -577,7 +568,7 @@ Item {
return ws && ws.idx !== -1; return ws && ws.idx !== -1;
if (CompositorService.isHyprland) if (CompositorService.isHyprland)
return ws && ws.id !== -1; return ws && ws.id !== -1;
if (root.isDwlLike) if (root.isMango)
return ws && ws.tag !== -1; return ws && ws.tag !== -1;
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return ws && ws.num !== -1; return ws && ws.num !== -1;
@@ -605,10 +596,9 @@ Item {
HyprlandService.focusWorkspace(data.id); HyprlandService.focusWorkspace(data.id);
} }
break; break;
case "dwl":
case "mango": case "mango":
if (data.tag !== undefined) if (data.tag !== undefined)
root.dwlSvc.switchToTag(root.screenName, data.tag); MangoService.switchToTag(root.screenName, data.tag);
break; break;
case "sway": case "sway":
case "scroll": case "scroll":
@@ -694,7 +684,7 @@ Item {
} }
HyprlandService.focusWorkspace(realWorkspaces[nextIndex].id); HyprlandService.focusWorkspace(realWorkspaces[nextIndex].id);
} else if (root.isDwlLike) { } else if (root.isMango) {
const realWorkspaces = getRealWorkspaces(); const realWorkspaces = getRealWorkspaces();
if (realWorkspaces.length < 2) { if (realWorkspaces.length < 2) {
return; return;
@@ -708,7 +698,7 @@ Item {
return; return;
} }
root.dwlSvc.switchToTag(root.screenName, realWorkspaces[nextIndex].tag); MangoService.switchToTag(root.screenName, realWorkspaces[nextIndex].tag);
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const realWorkspaces = getRealWorkspaces(); const realWorkspaces = getRealWorkspaces();
if (realWorkspaces.length < 2) { if (realWorkspaces.length < 2) {
@@ -736,7 +726,7 @@ Item {
return (modelData?.idx !== undefined && modelData?.idx !== -1) ? modelData.idx : ""; return (modelData?.idx !== undefined && modelData?.idx !== -1) ? modelData.idx : "";
if (CompositorService.isHyprland) if (CompositorService.isHyprland)
return modelData?.id || ""; return modelData?.id || "";
if (root.isDwlLike) if (root.isMango)
return (modelData?.tag !== undefined) ? (modelData.tag + 1) : ""; return (modelData?.tag !== undefined) ? (modelData.tag + 1) : "";
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return modelData?.num || ""; return modelData?.num || "";
@@ -751,7 +741,7 @@ Item {
isPlaceholder = modelData?.idx === -1; isPlaceholder = modelData?.idx === -1;
} else if (CompositorService.isHyprland) { } else if (CompositorService.isHyprland) {
isPlaceholder = modelData?.id === -1; isPlaceholder = modelData?.id === -1;
} else if (root.isDwlLike) { } else if (root.isMango) {
isPlaceholder = modelData?.tag === -1; isPlaceholder = modelData?.tag === -1;
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
isPlaceholder = modelData?.num === -1; isPlaceholder = modelData?.num === -1;
@@ -786,7 +776,7 @@ Item {
return getWorkspaceIndexFallback(modelData, index); return getWorkspaceIndexFallback(modelData, index);
} }
readonly property bool hasNativeWorkspaceSupport: CompositorService.isNiri || CompositorService.isHyprland || root.isDwlLike || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle readonly property bool hasNativeWorkspaceSupport: CompositorService.isNiri || CompositorService.isHyprland || root.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
readonly property bool hasWorkspaces: getRealWorkspaces().length > 0 readonly property bool hasWorkspaces: getRealWorkspaces().length > 0
readonly property bool shouldShow: hasNativeWorkspaceSupport || (useExtWorkspace && hasWorkspaces) readonly property bool shouldShow: hasNativeWorkspaceSupport || (useExtWorkspace && hasWorkspaces)
@@ -1051,7 +1041,7 @@ Item {
return !!(modelData && modelData.idx === root.currentWorkspace); return !!(modelData && modelData.idx === root.currentWorkspace);
if (CompositorService.isHyprland) if (CompositorService.isHyprland)
return !!(modelData && modelData.id === root.currentWorkspace); return !!(modelData && modelData.id === root.currentWorkspace);
if (root.isDwlLike) if (root.isMango)
return !!(modelData && root.dwlActiveTags.includes(modelData.tag)); return !!(modelData && root.dwlActiveTags.includes(modelData.tag));
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return !!(modelData && modelData.num === root.currentWorkspace); return !!(modelData && modelData.num === root.currentWorkspace);
@@ -1060,7 +1050,7 @@ Item {
property bool isOccupied: { property bool isOccupied: {
if (CompositorService.isHyprland) if (CompositorService.isHyprland)
return Array.from(Hyprland.toplevels?.values || []).some(tl => tl.workspace?.id === modelData?.id); return Array.from(Hyprland.toplevels?.values || []).some(tl => tl.workspace?.id === modelData?.id);
if (root.isDwlLike) if (root.isMango)
return modelData.clients > 0; return modelData.clients > 0;
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
const workspace = NiriService.allWorkspaces.find(ws => ws.idx + 1 === modelData && ws.output === root.effectiveScreenName); const workspace = NiriService.allWorkspaces.find(ws => ws.idx + 1 === modelData && ws.output === root.effectiveScreenName);
@@ -1075,7 +1065,7 @@ Item {
return !!(modelData && modelData.idx === -1); return !!(modelData && modelData.idx === -1);
if (CompositorService.isHyprland) if (CompositorService.isHyprland)
return !!(modelData && modelData.id === -1); return !!(modelData && modelData.id === -1);
if (root.isDwlLike) if (root.isMango)
return !!(modelData && modelData.tag === -1); return !!(modelData && modelData.tag === -1);
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return !!(modelData && modelData.num === -1); return !!(modelData && modelData.num === -1);
@@ -1092,7 +1082,7 @@ Item {
return modelData?.urgent ?? false; return modelData?.urgent ?? false;
if (CompositorService.isNiri) if (CompositorService.isNiri)
return loadedIsUrgent; return loadedIsUrgent;
if (root.isDwlLike) if (root.isMango)
return modelData?.state === 2; return modelData?.state === 2;
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return loadedIsUrgent; return loadedIsUrgent;
@@ -1120,7 +1110,7 @@ Item {
targetWorkspaceId = modelData?.id; targetWorkspaceId = modelData?.id;
} else if (CompositorService.isHyprland) { } else if (CompositorService.isHyprland) {
targetWorkspaceId = modelData?.id; targetWorkspaceId = modelData?.id;
} else if (root.isDwlLike) { } else if (root.isMango) {
targetWorkspaceId = modelData?.tag; targetWorkspaceId = modelData?.tag;
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
targetWorkspaceId = modelData?.num; targetWorkspaceId = modelData?.num;
@@ -1383,8 +1373,8 @@ Item {
} }
} else if (CompositorService.isHyprland && modelData?.id) { } else if (CompositorService.isHyprland && modelData?.id) {
HyprlandService.focusWorkspace(modelData.id); HyprlandService.focusWorkspace(modelData.id);
} else if (root.isDwlLike && modelData?.tag !== undefined) { } else if (root.isMango && modelData?.tag !== undefined) {
root.dwlSvc.switchToTag(root.screenName, modelData.tag); MangoService.switchToTag(root.screenName, modelData.tag);
} else if ((CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) && modelData?.num) { } else if ((CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) && modelData?.num) {
try { try {
I3.dispatch(`workspace number ${modelData.num}`); I3.dispatch(`workspace number ${modelData.num}`);
@@ -1395,8 +1385,8 @@ Item {
NiriService.toggleOverview(); NiriService.toggleOverview();
} else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) { } else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen; root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen;
} else if (root.isDwlLike && modelData?.tag !== undefined) { } else if (root.isMango && modelData?.tag !== undefined) {
root.dwlSvc.toggleTag(root.screenName, modelData.tag); MangoService.toggleTag(root.screenName, modelData.tag);
} }
} }
} }
@@ -1420,7 +1410,7 @@ Item {
wsData = modelData || null; wsData = modelData || null;
} else if (CompositorService.isHyprland) { } else if (CompositorService.isHyprland) {
wsData = modelData; wsData = modelData;
} else if (root.isDwlLike) { } else if (root.isMango) {
wsData = modelData; wsData = modelData;
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
wsData = modelData; wsData = modelData;
@@ -1434,7 +1424,7 @@ Item {
} }
if (SettingsData.showWorkspaceApps) { if (SettingsData.showWorkspaceApps) {
if (root.isDwlLike || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { if (root.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
delegateRoot.loadedIcons = root.getWorkspaceIcons(modelData); delegateRoot.loadedIcons = root.getWorkspaceIcons(modelData);
} else if (CompositorService.isNiri) { } else if (CompositorService.isNiri) {
delegateRoot.loadedIcons = root.getWorkspaceIcons(isPlaceholder ? null : modelData); delegateRoot.loadedIcons = root.getWorkspaceIcons(isPlaceholder ? null : modelData);
@@ -1994,8 +1984,8 @@ Item {
} }
} }
Connections { Connections {
target: root.dwlSvc target: MangoService
enabled: root.isDwlLike enabled: root.isMango
function onStateChanged() { function onStateChanged() {
delegateRoot.updateAllData(); delegateRoot.updateAllData();
} }
@@ -183,7 +183,7 @@ Rectangle {
text: { text: {
const dateStr = Qt.formatDate(selectedDate, "MMM d"); const dateStr = Qt.formatDate(selectedDate, "MMM d");
if (selectedDateEvents && selectedDateEvents.length > 0) { if (selectedDateEvents && selectedDateEvents.length > 0) {
const eventCount = selectedDateEvents.length === 1 ? I18n.tr("1 task") : selectedDateEvents.length + " " + I18n.tr("tasks"); const eventCount = selectedDateEvents.length === 1 ? I18n.tr("1 task", "task count next to a date") : I18n.tr("%1 tasks", "task count next to a date, %1 is the number of tasks").arg(selectedDateEvents.length);
return dateStr + " • " + eventCount; return dateStr + " • " + eventCount;
} }
return dateStr; return dateStr;
@@ -775,7 +775,7 @@ Rectangle {
width: parent.width width: parent.width
text: { text: {
if (!modelData || modelData.allDay) { if (!modelData || modelData.allDay) {
return I18n.tr("All day"); return I18n.tr("All day", "calendar task with no specific time");
} else if (modelData.start && modelData.end) { } else if (modelData.start && modelData.end) {
const timeFormat = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP"; const timeFormat = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP";
const startTime = Qt.formatTime(modelData.start, timeFormat); const startTime = Qt.formatTime(modelData.start, timeFormat);
@@ -950,9 +950,8 @@ Rectangle {
selectByMouse: true selectByMouse: true
clip: true clip: true
// Hint placeholder text
Text { Text {
text: I18n.tr("Add a task...") text: I18n.tr("Add a task...", "placeholder in the new-task input field")
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
visible: !taskInput.text && !taskInput.activeFocus visible: !taskInput.text && !taskInput.activeFocus
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
@@ -67,9 +67,6 @@ Card {
return I18n.tr("on Niri"); return I18n.tr("on Niri");
if (CompositorService.isHyprland) if (CompositorService.isHyprland)
return I18n.tr("on Hyprland"); return I18n.tr("on Hyprland");
// technically they might not be on mangowc, but its what we support in the docs
if (CompositorService.isDwl)
return I18n.tr("on MangoWC");
if (CompositorService.isMango) if (CompositorService.isMango)
return I18n.tr("on MangoWC"); return I18n.tr("on MangoWC");
if (CompositorService.isSway) if (CompositorService.isSway)
@@ -101,9 +98,7 @@ Card {
} }
StyledText { StyledText {
text: DgopService.shortUptime text: DgopService.shortUptime ? I18n.tr("up", "uptime prefix, e.g. 'up 4h 2m'") + DgopService.shortUptime.slice(2) : I18n.tr("up", "uptime prefix, e.g. 'up 4h 2m'")
? I18n.tr("up") + DgopService.shortUptime.slice(2)
: I18n.tr("up")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -20,17 +20,25 @@ Card {
spacing: Theme.spacingS spacing: Theme.spacingS
visible: !WeatherService.weather.available visible: !WeatherService.weather.available
DankSpinner {
size: 24
visible: WeatherService.weather.loading
anchors.horizontalCenter: parent.horizontalCenter
}
DankIcon { DankIcon {
name: "cloud_off" name: "cloud_off"
size: 24 size: 24
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
visible: !WeatherService.weather.loading
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
} }
StyledText { StyledText {
text: WeatherService.weather.loading ? I18n.tr("Loading...") : I18n.tr("No Weather") text: I18n.tr("No Weather")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: !WeatherService.weather.loading
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
} }
+29 -9
View File
@@ -747,16 +747,36 @@ Variants {
onHeightChanged: dock._syncDockChromeState() onHeightChanged: dock._syncDockChromeState()
} }
ConnectedShape { Item {
id: dockConnectedChrome
visible: Theme.isConnectedEffect && dock.reveal && !SettingsData.connectedFrameModeActive visible: Theme.isConnectedEffect && dock.reveal && !SettingsData.connectedFrameModeActive
barSide: dock.connectedBarSide readonly property real extraLeft: dock.isVertical ? 0 : Theme.connectedCornerRadius
bodyWidth: dockBackground.width readonly property real extraTop: dock.isVertical ? Theme.connectedCornerRadius : 0
bodyHeight: dockBackground.height readonly property real bodyRadius: dock.surfaceRadius
connectorRadius: Theme.connectedCornerRadius readonly property bool barTop: dock.connectedBarSide === "top"
surfaceRadius: dock.surfaceRadius readonly property bool barBottom: dock.connectedBarSide === "bottom"
fillColor: dock.surfaceColor readonly property bool barLeft: dock.connectedBarSide === "left"
x: dockBackground.x - bodyX readonly property bool barRight: dock.connectedBarSide === "right"
y: dockBackground.y - bodyY
x: dockBackground.x - extraLeft
y: dockBackground.y - extraTop
width: dockBackground.width + extraLeft * 2
height: dockBackground.height + extraTop * 2
ShaderEffect {
anchors.fill: parent
fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/connected_chrome.frag.qsb")
property real widthPx: width
property real heightPx: height
property vector4d surfaceColor: Qt.vector4d(dock.surfaceColor.r, dock.surfaceColor.g, dock.surfaceColor.b, dock.surfaceColor.a)
property vector4d shadowColor: Qt.vector4d(0, 0, 0, 0)
property vector4d shadowParam: Qt.vector4d(0, 0, 0, 0)
property vector4d ambientParam: Qt.vector4d(0, 0, 0, 0)
property vector4d bodyRect: Qt.vector4d(dockConnectedChrome.extraLeft, dockConnectedChrome.extraTop, dockBackground.width, dockBackground.height)
property vector4d cornerRadius: Qt.vector4d(dockConnectedChrome.barTop || dockConnectedChrome.barLeft ? 0 : dockConnectedChrome.bodyRadius, dockConnectedChrome.barTop || dockConnectedChrome.barRight ? 0 : dockConnectedChrome.bodyRadius, dockConnectedChrome.barBottom || dockConnectedChrome.barRight ? 0 : dockConnectedChrome.bodyRadius, dockConnectedChrome.barBottom || dockConnectedChrome.barLeft ? 0 : dockConnectedChrome.bodyRadius)
property vector4d edgeParam: Qt.vector4d(dockConnectedChrome.barTop ? 0 : (dockConnectedChrome.barBottom ? 1 : (dockConnectedChrome.barLeft ? 2 : 3)), Theme.connectedCornerRadius, 0, 0)
}
} }
Shape { Shape {
@@ -236,7 +236,7 @@ Item {
} }
IconImage { IconImage {
visible: SettingsData.dockLauncherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isLabwc) visible: SettingsData.dockLauncherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isLabwc)
anchors.centerIn: parent anchors.centerIn: parent
width: actualIconSize + SettingsData.dockLauncherLogoSizeOffset width: actualIconSize + SettingsData.dockLauncherLogoSizeOffset
height: actualIconSize + SettingsData.dockLauncherLogoSizeOffset height: actualIconSize + SettingsData.dockLauncherLogoSizeOffset
@@ -247,8 +247,6 @@ Item {
return "file://" + Theme.shellDir + "/assets/niri.svg"; return "file://" + Theme.shellDir + "/assets/niri.svg";
} else if (CompositorService.isHyprland) { } else if (CompositorService.isHyprland) {
return "file://" + Theme.shellDir + "/assets/hyprland.svg"; return "file://" + Theme.shellDir + "/assets/hyprland.svg";
} else if (CompositorService.isDwl) {
return "file://" + Theme.shellDir + "/assets/mango.png";
} else if (CompositorService.isMango) { } else if (CompositorService.isMango) {
return "file://" + Theme.shellDir + "/assets/mango.png"; return "file://" + Theme.shellDir + "/assets/mango.png";
} else if (CompositorService.isSway) { } else if (CompositorService.isSway) {
+1 -4
View File
@@ -3,10 +3,7 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import qs.Common import qs.Common
// Frame perimeter ring: the full window rectangle with a rounded-rectangle // Frame perimeter ring with rounded cutout (SDF).
// cutout, rendered as a signed-distance field with analytic antialiasing.
// One primitive: no full-output mask textures, no corner double-blend, crisp
// edges at any scale without an FBO.
Item { Item {
id: root id: root
+33 -71
View File
@@ -49,10 +49,6 @@ PanelWindow {
readonly property var _dockDescriptor: ConnectedModeState.surfaceDescriptor(win._screenName, "dock") readonly property var _dockDescriptor: ConnectedModeState.surfaceDescriptor(win._screenName, "dock")
readonly property var _notifDescriptor: ConnectedModeState.surfaceDescriptor(win._screenName, "notification") readonly property var _notifDescriptor: ConnectedModeState.surfaceDescriptor(win._screenName, "notification")
readonly property var _modalDescriptor: ConnectedModeState.surfaceDescriptor(win._screenName, "modal") readonly property var _modalDescriptor: ConnectedModeState.surfaceDescriptor(win._screenName, "modal")
readonly property var _popoutState: ConnectedModeState.legacySurfaceState(win._screenName, "popout")
readonly property var _dockState: ConnectedModeState.legacySurfaceState(win._screenName, "dock")
readonly property var _notifState: ConnectedModeState.legacySurfaceState(win._screenName, "notification")
readonly property var _modalState: ConnectedModeState.legacySurfaceState(win._screenName, "modal")
readonly property bool _connectedActive: CompositorService.usesConnectedFrameChromeForScreen(win.targetScreen) readonly property bool _connectedActive: CompositorService.usesConnectedFrameChromeForScreen(win.targetScreen)
readonly property string _barSide: { readonly property string _barSide: {
@@ -68,7 +64,7 @@ PanelWindow {
readonly property real _ccr: Theme.connectedCornerRadius readonly property real _ccr: Theme.connectedCornerRadius
readonly property bool _popoutHorizontal: SurfaceGeometry.isHorizontal(win._popoutDescriptor.barSide) readonly property bool _popoutHorizontal: SurfaceGeometry.isHorizontal(win._popoutDescriptor.barSide)
readonly property bool _modalHorizontal: ConnectorGeometry.isHorizontal(win._modalState.barSide) readonly property bool _modalHorizontal: SurfaceGeometry.isHorizontal(win._modalDescriptor.barSide)
readonly property var _popoutBodyGeometry: SurfaceGeometry.animatedBodyRect(win._popoutDescriptor, win._dpr) readonly property var _popoutBodyGeometry: SurfaceGeometry.animatedBodyRect(win._popoutDescriptor, win._dpr)
readonly property var _modalBodyGeometry: SurfaceGeometry.animatedBodyRect(win._modalDescriptor, win._dpr) readonly property var _modalBodyGeometry: SurfaceGeometry.animatedBodyRect(win._modalDescriptor, win._dpr)
readonly property var _notifBodyGeometry: SurfaceGeometry.bodyRect(win._notifDescriptor, win._dpr) readonly property var _notifBodyGeometry: SurfaceGeometry.bodyRect(win._notifDescriptor, win._dpr)
@@ -86,18 +82,16 @@ PanelWindow {
readonly property real _dockConnectorRadiusValue: { readonly property real _dockConnectorRadiusValue: {
if (!_dockBodyBlurAnchor._active) if (!_dockBodyBlurAnchor._active)
return win._ccr; return win._ccr;
const thickness = (win._dockState.barSide === "left" || win._dockState.barSide === "right") ? _dockBodyBlurAnchor.width : _dockBodyBlurAnchor.height; const thickness = SurfaceGeometry.isVertical(win._dockDescriptor.barSide) ? _dockBodyBlurAnchor.width : _dockBodyBlurAnchor.height;
const bodyRadius = win._dockBodyBlurRadiusValue; const bodyRadius = win._dockBodyBlurRadiusValue;
const maxConnectorRadius = Math.max(0, thickness - bodyRadius - win._seamOverlap); const maxConnectorRadius = Math.max(0, thickness - bodyRadius - win._seamOverlap);
return Math.max(0, Math.min(win._ccr, bodyRadius, maxConnectorRadius)); return Math.max(0, Math.min(win._ccr, bodyRadius, maxConnectorRadius));
} }
readonly property real _notifSideUnderlapValue: ConnectorGeometry.isVertical(win._notifState.barSide) ? win._seamOverlap : 0 readonly property real _notifSideUnderlapValue: SurfaceGeometry.isVertical(win._notifDescriptor.barSide) ? win._seamOverlap : 0
readonly property real _notifStartUnderlapValue: win._notifState.omitStartConnector ? win._seamOverlap : 0 readonly property real _notifStartUnderlapValue: win._notifDescriptor.omitStartConnector ? win._seamOverlap : 0
readonly property real _notifEndUnderlapValue: win._notifState.omitEndConnector ? win._seamOverlap : 0 readonly property real _notifEndUnderlapValue: win._notifDescriptor.omitEndConnector ? win._seamOverlap : 0
// Theme.snap rounds to integer pixel: equal rounded values suppress
// downstream Changed during sub-pixel morph jitter.
readonly property var _popoutRadii: SurfaceGeometry.connectorRadii(win._popoutDescriptor, win._popoutBodyGeometry, win._ccr, win._surfaceRadius, win._dpr, false) readonly property var _popoutRadii: SurfaceGeometry.connectorRadii(win._popoutDescriptor, win._popoutBodyGeometry, win._ccr, win._surfaceRadius, win._dpr, false)
readonly property real _effectivePopoutCcr: win._popoutRadii.near readonly property real _effectivePopoutCcr: win._popoutRadii.near
readonly property real _effectivePopoutFarCcr: win._popoutRadii.far readonly property real _effectivePopoutFarCcr: win._popoutRadii.far
@@ -129,12 +123,8 @@ PanelWindow {
readonly property real _surfaceRadius: Theme.connectedSurfaceRadius readonly property real _surfaceRadius: Theme.connectedSurfaceRadius
readonly property real _seamOverlap: Theme.hairline(win._dpr) readonly property real _seamOverlap: Theme.hairline(win._dpr)
readonly property bool _disableLayer: Quickshell.env("DMS_DISABLE_LAYER") === "true" || Quickshell.env("DMS_DISABLE_LAYER") === "1" readonly property bool _disableLayer: Quickshell.env("DMS_DISABLE_LAYER") === "true" || Quickshell.env("DMS_DISABLE_LAYER") === "1"
// Both elevation states render through the SDF shader; this only toggles
// the shadow term inside it.
readonly property bool _elevationShadow: win._connectedActive && Theme.elevationEnabled && !win._disableLayer readonly property bool _elevationShadow: win._connectedActive && Theme.elevationEnabled && !win._disableLayer
// Active surfaces packed into four fixed SDF-shader slots. Each near (bar) // Pack active connected surfaces into four fixed SDF slots (near edges clamp to cutout).
// edge is clamped to the cutout edge so the smooth-min connector attaches
// there; the per-corner smin radius is that corner's junction fillet.
readonly property var _sdfSlots: { readonly property var _sdfSlots: {
const T = win.cutoutTopInset; const T = win.cutoutTopInset;
const L = win.cutoutLeftInset; const L = win.cutoutLeftInset;
@@ -162,16 +152,7 @@ PanelWindow {
const s = src[i]; const s = src[i];
const b = clampNear(s.side, s.body); const b = clampNear(s.side, s.body);
const active = b.width > 0 && b.height > 0 ? 1 : 0; const active = b.width > 0 && b.height > 0 ? 1 : 0;
// Map start/end (left/top, right/bottom) onto corners
// (tl,tr,br,bl): bar-side corners take their near connector
// fillet, far corners always take the far fillet so a body
// meeting a perpendicular border joins with an arc (smin is
// inert when nothing is within k). A bar-side corner is sharp
// where its connector is present; an omitted connector makes
// its far corner sharp instead (the far-cap join).
const sc = s.radii.startCr, ec = s.radii.endCr; const sc = s.radii.startCr, ec = s.radii.endCr;
// Clamp the far fillet to the body extent so it cannot flare
// back across a shallow body into the bar mid-animation.
const extent = (s.side === "top" || s.side === "bottom") ? b.height : b.width; const extent = (s.side === "top" || s.side === "bottom") ? b.height : b.width;
const fc = Math.min(s.radii.farCr, extent); const fc = Math.min(s.radii.farCr, extent);
const omitS = s.radii.farStartCr > 0; const omitS = s.radii.farStartCr > 0;
@@ -179,9 +160,6 @@ PanelWindow {
const bodyR = s.radii.surfaceRadius; const bodyR = s.radii.surfaceRadius;
const nearS = omitS ? bodyR : 0, nearE = omitE ? bodyR : 0; const nearS = omitS ? bodyR : 0, nearE = omitE ? bodyR : 0;
const farS = omitS ? 0 : bodyR, farE = omitE ? 0 : bodyR; const farS = omitS ? 0 : bodyR, farE = omitE ? 0 : bodyR;
// An omitted bar-side corner sits flush against the border, so
// it keeps a nonzero fillet (a zero k hard-joins the coincident
// edges and shows a half-coverage hairline along the seam).
const kS = omitS ? fc : sc, kE = omitE ? fc : ec; const kS = omitS ? fc : sc, kE = omitE ? fc : ec;
let ks, cr; let ks, cr;
if (s.side === "top") { if (s.side === "top") {
@@ -225,8 +203,6 @@ PanelWindow {
return Math.max(0, Math.min(requested, maxRadius)); return Math.max(0, Math.min(requested, maxRadius));
} }
// Pins every surface blur region to zero while frame blur cannot be
// consumed, so animations stop dirtying the region tree.
readonly property bool _blurSurfacesActive: BlurService.enabled && SettingsData.frameBlurEnabled && win._frameActive readonly property bool _blurSurfacesActive: BlurService.enabled && SettingsData.frameBlurEnabled && win._frameActive
readonly property int _blurCutoutCompensation: SettingsData.frameOpacity <= 0.2 ? 1 : 0 readonly property int _blurCutoutCompensation: SettingsData.frameOpacity <= 0.2 ? 1 : 0
readonly property int _blurCutoutLeft: Math.max(0, win.cutoutLeftInset - win._blurCutoutCompensation) readonly property int _blurCutoutLeft: Math.max(0, win.cutoutLeftInset - win._blurCutoutCompensation)
@@ -239,9 +215,6 @@ PanelWindow {
return Math.max(0, Math.min(requested, maxRadius)); return Math.max(0, Math.min(requested, maxRadius));
} }
// Blur regions bind rounded integer pixels directly: equal rounded values
// suppress Changed during sub-pixel motion, and inactive children pin to
// all-zero so the region build skips them.
QtObject { QtObject {
id: _notifBodyBlurAnchor id: _notifBodyBlurAnchor
@@ -259,7 +232,6 @@ PanelWindow {
width: win._windowRegionWidth width: win._windowRegionWidth
height: win._windowRegionHeight height: win._windowRegionHeight
// Frame cutout (always active when frame is on)
Region { Region {
id: _blurCutout id: _blurCutout
intersection: Intersection.Subtract intersection: Intersection.Subtract
@@ -284,7 +256,7 @@ PanelWindow {
Region { Region {
id: _popoutBodyBlurCap id: _popoutBodyBlurCap
readonly property string _side: win._popoutState.barSide readonly property string _side: win._popoutDescriptor.barSide
readonly property real _capThickness: win._popoutBlurCapThickness() readonly property real _capThickness: win._popoutBlurCapThickness()
readonly property bool _active: _popoutBodyBlurAnchor._active && _capThickness > 0 && _popoutBodyBlurAnchor.width > 0 && _popoutBodyBlurAnchor.height > 0 readonly property bool _active: _popoutBodyBlurAnchor._active && _capThickness > 0 && _popoutBodyBlurAnchor.width > 0 && _popoutBodyBlurAnchor.height > 0
readonly property int _capWidth: (_side === "left" || _side === "right") ? Math.round(Math.min(_capThickness, _popoutBodyBlurAnchor.width)) : _popoutBodyBlurAnchor.width readonly property int _capWidth: (_side === "left" || _side === "right") ? Math.round(Math.min(_capThickness, _popoutBodyBlurAnchor.width)) : _popoutBodyBlurAnchor.width
@@ -311,7 +283,7 @@ PanelWindow {
id: _popoutLeftConnectorCutout id: _popoutLeftConnectorCutout
readonly property bool _active: _popoutLeftConnectorBlurAnchor.width > 0 && _popoutLeftConnectorBlurAnchor.height > 0 readonly property bool _active: _popoutLeftConnectorBlurAnchor.width > 0 && _popoutLeftConnectorBlurAnchor.height > 0
readonly property string _arcCorner: ConnectorGeometry.arcCorner(win._popoutState.barSide, "left") readonly property string _arcCorner: ConnectorGeometry.arcCorner(win._popoutDescriptor.barSide, "left")
readonly property real _radius: win._popoutConnectorRadiusLeft readonly property real _radius: win._popoutConnectorRadiusLeft
intersection: Intersection.Subtract intersection: Intersection.Subtract
@@ -338,7 +310,7 @@ PanelWindow {
id: _popoutRightConnectorCutout id: _popoutRightConnectorCutout
readonly property bool _active: _popoutRightConnectorBlurAnchor.width > 0 && _popoutRightConnectorBlurAnchor.height > 0 readonly property bool _active: _popoutRightConnectorBlurAnchor.width > 0 && _popoutRightConnectorBlurAnchor.height > 0
readonly property string _arcCorner: ConnectorGeometry.arcCorner(win._popoutState.barSide, "right") readonly property string _arcCorner: ConnectorGeometry.arcCorner(win._popoutDescriptor.barSide, "right")
readonly property real _radius: win._popoutConnectorRadiusRight readonly property real _radius: win._popoutConnectorRadiusRight
intersection: Intersection.Subtract intersection: Intersection.Subtract
@@ -389,8 +361,8 @@ PanelWindow {
id: _popoutFarStartConnectorCutout id: _popoutFarStartConnectorCutout
readonly property bool _active: _popoutFarStartConnectorBlurAnchor.width > 0 && _popoutFarStartConnectorBlurAnchor.height > 0 readonly property bool _active: _popoutFarStartConnectorBlurAnchor.width > 0 && _popoutFarStartConnectorBlurAnchor.height > 0
readonly property string _barSide: win._farConnectorBarSide(win._popoutState.barSide, "left") readonly property string _barSide: win._farConnectorBarSide(win._popoutDescriptor.barSide, "left")
readonly property string _placement: win._farConnectorPlacement(win._popoutState.barSide, "left") readonly property string _placement: win._farConnectorPlacement(win._popoutDescriptor.barSide, "left")
readonly property string _arcCorner: ConnectorGeometry.arcCorner(_barSide, _placement) readonly property string _arcCorner: ConnectorGeometry.arcCorner(_barSide, _placement)
readonly property real _radius: win._effectivePopoutFarStartCcr readonly property real _radius: win._effectivePopoutFarStartCcr
@@ -418,8 +390,8 @@ PanelWindow {
id: _popoutFarEndConnectorCutout id: _popoutFarEndConnectorCutout
readonly property bool _active: _popoutFarEndConnectorBlurAnchor.width > 0 && _popoutFarEndConnectorBlurAnchor.height > 0 readonly property bool _active: _popoutFarEndConnectorBlurAnchor.width > 0 && _popoutFarEndConnectorBlurAnchor.height > 0
readonly property string _barSide: win._farConnectorBarSide(win._popoutState.barSide, "right") readonly property string _barSide: win._farConnectorBarSide(win._popoutDescriptor.barSide, "right")
readonly property string _placement: win._farConnectorPlacement(win._popoutState.barSide, "right") readonly property string _placement: win._farConnectorPlacement(win._popoutDescriptor.barSide, "right")
readonly property string _arcCorner: ConnectorGeometry.arcCorner(_barSide, _placement) readonly property string _arcCorner: ConnectorGeometry.arcCorner(_barSide, _placement)
readonly property real _radius: win._effectivePopoutFarEndCcr readonly property real _radius: win._effectivePopoutFarEndCcr
@@ -446,7 +418,7 @@ PanelWindow {
Region { Region {
id: _dockBodyBlurCap id: _dockBodyBlurCap
readonly property string _side: win._dockState.barSide readonly property string _side: win._dockDescriptor.barSide
readonly property bool _active: _dockBodyBlurAnchor._active && _dockBodyBlurAnchor.width > 0 && _dockBodyBlurAnchor.height > 0 readonly property bool _active: _dockBodyBlurAnchor._active && _dockBodyBlurAnchor.width > 0 && _dockBodyBlurAnchor.height > 0
readonly property int _capWidth: (_side === "left" || _side === "right") ? Math.round(Math.min(win._dockConnectorRadiusValue, _dockBodyBlurAnchor.width)) : _dockBodyBlurAnchor.width readonly property int _capWidth: (_side === "left" || _side === "right") ? Math.round(Math.min(win._dockConnectorRadiusValue, _dockBodyBlurAnchor.width)) : _dockBodyBlurAnchor.width
readonly property int _capHeight: (_side === "top" || _side === "bottom") ? Math.round(Math.min(win._dockConnectorRadiusValue, _dockBodyBlurAnchor.height)) : _dockBodyBlurAnchor.height readonly property int _capHeight: (_side === "top" || _side === "bottom") ? Math.round(Math.min(win._dockConnectorRadiusValue, _dockBodyBlurAnchor.height)) : _dockBodyBlurAnchor.height
@@ -471,7 +443,7 @@ PanelWindow {
id: _dockLeftConnectorCutout id: _dockLeftConnectorCutout
readonly property bool _active: _dockLeftConnectorBlurAnchor.width > 0 && _dockLeftConnectorBlurAnchor.height > 0 readonly property bool _active: _dockLeftConnectorBlurAnchor.width > 0 && _dockLeftConnectorBlurAnchor.height > 0
readonly property string _arcCorner: ConnectorGeometry.arcCorner(win._dockState.barSide, "left") readonly property string _arcCorner: ConnectorGeometry.arcCorner(win._dockDescriptor.barSide, "left")
intersection: Intersection.Subtract intersection: Intersection.Subtract
radius: win._dockConnectorRadiusValue radius: win._dockConnectorRadiusValue
@@ -496,7 +468,7 @@ PanelWindow {
id: _dockRightConnectorCutout id: _dockRightConnectorCutout
readonly property bool _active: _dockRightConnectorBlurAnchor.width > 0 && _dockRightConnectorBlurAnchor.height > 0 readonly property bool _active: _dockRightConnectorBlurAnchor.width > 0 && _dockRightConnectorBlurAnchor.height > 0
readonly property string _arcCorner: ConnectorGeometry.arcCorner(win._dockState.barSide, "right") readonly property string _arcCorner: ConnectorGeometry.arcCorner(win._dockDescriptor.barSide, "right")
intersection: Intersection.Subtract intersection: Intersection.Subtract
radius: win._dockConnectorRadiusValue radius: win._dockConnectorRadiusValue
@@ -522,7 +494,7 @@ PanelWindow {
Region { Region {
id: _notifBodyBlurCap id: _notifBodyBlurCap
readonly property string _side: win._notifState.barSide readonly property string _side: win._notifDescriptor.barSide
readonly property real _capRadius: win._effectiveNotifMaxCcr readonly property real _capRadius: win._effectiveNotifMaxCcr
readonly property bool _active: _notifBodySceneBlurAnchor._active && _notifBodySceneBlurAnchor.width > 0 && _notifBodySceneBlurAnchor.height > 0 && _capRadius > 0 readonly property bool _active: _notifBodySceneBlurAnchor._active && _notifBodySceneBlurAnchor.width > 0 && _notifBodySceneBlurAnchor.height > 0 && _capRadius > 0
readonly property int _capWidth: (_side === "left" || _side === "right") ? Math.round(Math.min(_capRadius, _notifBodySceneBlurAnchor.width)) : _notifBodySceneBlurAnchor.width readonly property int _capWidth: (_side === "left" || _side === "right") ? Math.round(Math.min(_capRadius, _notifBodySceneBlurAnchor.width)) : _notifBodySceneBlurAnchor.width
@@ -549,7 +521,7 @@ PanelWindow {
id: _notifLeftConnectorCutout id: _notifLeftConnectorCutout
readonly property bool _active: _notifLeftConnectorBlurAnchor.width > 0 && _notifLeftConnectorBlurAnchor.height > 0 readonly property bool _active: _notifLeftConnectorBlurAnchor.width > 0 && _notifLeftConnectorBlurAnchor.height > 0
readonly property string _arcCorner: ConnectorGeometry.arcCorner(win._notifState.barSide, "left") readonly property string _arcCorner: ConnectorGeometry.arcCorner(win._notifDescriptor.barSide, "left")
readonly property real _radius: win._notifConnectorRadiusLeft readonly property real _radius: win._notifConnectorRadiusLeft
intersection: Intersection.Subtract intersection: Intersection.Subtract
@@ -576,7 +548,7 @@ PanelWindow {
id: _notifRightConnectorCutout id: _notifRightConnectorCutout
readonly property bool _active: _notifRightConnectorBlurAnchor.width > 0 && _notifRightConnectorBlurAnchor.height > 0 readonly property bool _active: _notifRightConnectorBlurAnchor.width > 0 && _notifRightConnectorBlurAnchor.height > 0
readonly property string _arcCorner: ConnectorGeometry.arcCorner(win._notifState.barSide, "right") readonly property string _arcCorner: ConnectorGeometry.arcCorner(win._notifDescriptor.barSide, "right")
readonly property real _radius: win._notifConnectorRadiusRight readonly property real _radius: win._notifConnectorRadiusRight
intersection: Intersection.Subtract intersection: Intersection.Subtract
@@ -627,8 +599,8 @@ PanelWindow {
id: _notifFarStartConnectorCutout id: _notifFarStartConnectorCutout
readonly property bool _active: _notifFarStartConnectorBlurAnchor.width > 0 && _notifFarStartConnectorBlurAnchor.height > 0 readonly property bool _active: _notifFarStartConnectorBlurAnchor.width > 0 && _notifFarStartConnectorBlurAnchor.height > 0
readonly property string _barSide: win._farConnectorBarSide(win._notifState.barSide, "left") readonly property string _barSide: win._farConnectorBarSide(win._notifDescriptor.barSide, "left")
readonly property string _placement: win._farConnectorPlacement(win._notifState.barSide, "left") readonly property string _placement: win._farConnectorPlacement(win._notifDescriptor.barSide, "left")
readonly property string _arcCorner: ConnectorGeometry.arcCorner(_barSide, _placement) readonly property string _arcCorner: ConnectorGeometry.arcCorner(_barSide, _placement)
readonly property real _radius: win._effectiveNotifFarStartCcr readonly property real _radius: win._effectiveNotifFarStartCcr
@@ -656,8 +628,8 @@ PanelWindow {
id: _notifFarEndConnectorCutout id: _notifFarEndConnectorCutout
readonly property bool _active: _notifFarEndConnectorBlurAnchor.width > 0 && _notifFarEndConnectorBlurAnchor.height > 0 readonly property bool _active: _notifFarEndConnectorBlurAnchor.width > 0 && _notifFarEndConnectorBlurAnchor.height > 0
readonly property string _barSide: win._farConnectorBarSide(win._notifState.barSide, "right") readonly property string _barSide: win._farConnectorBarSide(win._notifDescriptor.barSide, "right")
readonly property string _placement: win._farConnectorPlacement(win._notifState.barSide, "right") readonly property string _placement: win._farConnectorPlacement(win._notifDescriptor.barSide, "right")
readonly property string _arcCorner: ConnectorGeometry.arcCorner(_barSide, _placement) readonly property string _arcCorner: ConnectorGeometry.arcCorner(_barSide, _placement)
readonly property real _radius: win._effectiveNotifFarEndCcr readonly property real _radius: win._effectiveNotifFarEndCcr
@@ -684,7 +656,7 @@ PanelWindow {
Region { Region {
id: _modalBodyBlurCap id: _modalBodyBlurCap
readonly property string _side: win._modalState.barSide readonly property string _side: win._modalDescriptor.barSide
readonly property real _capThickness: win._modalBlurCapThickness() readonly property real _capThickness: win._modalBlurCapThickness()
readonly property bool _active: _modalBodyBlurAnchor._active && _capThickness > 0 && _modalBodyBlurAnchor.width > 0 && _modalBodyBlurAnchor.height > 0 readonly property bool _active: _modalBodyBlurAnchor._active && _capThickness > 0 && _modalBodyBlurAnchor.width > 0 && _modalBodyBlurAnchor.height > 0
readonly property int _capWidth: (_side === "left" || _side === "right") ? Math.round(Math.min(_capThickness, _modalBodyBlurAnchor.width)) : _modalBodyBlurAnchor.width readonly property int _capWidth: (_side === "left" || _side === "right") ? Math.round(Math.min(_capThickness, _modalBodyBlurAnchor.width)) : _modalBodyBlurAnchor.width
@@ -711,7 +683,7 @@ PanelWindow {
id: _modalLeftConnectorCutout id: _modalLeftConnectorCutout
readonly property bool _active: _modalLeftConnectorBlurAnchor.width > 0 && _modalLeftConnectorBlurAnchor.height > 0 readonly property bool _active: _modalLeftConnectorBlurAnchor.width > 0 && _modalLeftConnectorBlurAnchor.height > 0
readonly property string _arcCorner: ConnectorGeometry.arcCorner(win._modalState.barSide, "left") readonly property string _arcCorner: ConnectorGeometry.arcCorner(win._modalDescriptor.barSide, "left")
readonly property real _radius: win._modalConnectorRadiusLeft readonly property real _radius: win._modalConnectorRadiusLeft
intersection: Intersection.Subtract intersection: Intersection.Subtract
@@ -738,7 +710,7 @@ PanelWindow {
id: _modalRightConnectorCutout id: _modalRightConnectorCutout
readonly property bool _active: _modalRightConnectorBlurAnchor.width > 0 && _modalRightConnectorBlurAnchor.height > 0 readonly property bool _active: _modalRightConnectorBlurAnchor.width > 0 && _modalRightConnectorBlurAnchor.height > 0
readonly property string _arcCorner: ConnectorGeometry.arcCorner(win._modalState.barSide, "right") readonly property string _arcCorner: ConnectorGeometry.arcCorner(win._modalDescriptor.barSide, "right")
readonly property real _radius: win._modalConnectorRadiusRight readonly property real _radius: win._modalConnectorRadiusRight
intersection: Intersection.Subtract intersection: Intersection.Subtract
@@ -789,8 +761,8 @@ PanelWindow {
id: _modalFarStartConnectorCutout id: _modalFarStartConnectorCutout
readonly property bool _active: _modalFarStartConnectorBlurAnchor.width > 0 && _modalFarStartConnectorBlurAnchor.height > 0 readonly property bool _active: _modalFarStartConnectorBlurAnchor.width > 0 && _modalFarStartConnectorBlurAnchor.height > 0
readonly property string _barSide: win._farConnectorBarSide(win._modalState.barSide, "left") readonly property string _barSide: win._farConnectorBarSide(win._modalDescriptor.barSide, "left")
readonly property string _placement: win._farConnectorPlacement(win._modalState.barSide, "left") readonly property string _placement: win._farConnectorPlacement(win._modalDescriptor.barSide, "left")
readonly property string _arcCorner: ConnectorGeometry.arcCorner(_barSide, _placement) readonly property string _arcCorner: ConnectorGeometry.arcCorner(_barSide, _placement)
readonly property real _radius: win._effectiveModalFarStartCcr readonly property real _radius: win._effectiveModalFarStartCcr
@@ -818,8 +790,8 @@ PanelWindow {
id: _modalFarEndConnectorCutout id: _modalFarEndConnectorCutout
readonly property bool _active: _modalFarEndConnectorBlurAnchor.width > 0 && _modalFarEndConnectorBlurAnchor.height > 0 readonly property bool _active: _modalFarEndConnectorBlurAnchor.width > 0 && _modalFarEndConnectorBlurAnchor.height > 0
readonly property string _barSide: win._farConnectorBarSide(win._modalState.barSide, "right") readonly property string _barSide: win._farConnectorBarSide(win._modalDescriptor.barSide, "right")
readonly property string _placement: win._farConnectorPlacement(win._modalState.barSide, "right") readonly property string _placement: win._farConnectorPlacement(win._modalDescriptor.barSide, "right")
readonly property string _arcCorner: ConnectorGeometry.arcCorner(_barSide, _placement) readonly property string _arcCorner: ConnectorGeometry.arcCorner(_barSide, _placement)
readonly property real _radius: win._effectiveModalFarEndCcr readonly property real _radius: win._effectiveModalFarEndCcr
@@ -833,9 +805,8 @@ PanelWindow {
} }
} }
// Notif body scene rect, accounting for start/end/side underlaps per bar orientation.
function _notifBodyScene() { function _notifBodyScene() {
const isHoriz = ConnectorGeometry.isHorizontal(win._notifState.barSide); const isHoriz = SurfaceGeometry.isHorizontal(win._notifDescriptor.barSide);
const start = win._notifStartUnderlapValue; const start = win._notifStartUnderlapValue;
const end = win._notifEndUnderlapValue; const end = win._notifEndUnderlapValue;
const side = win._notifSideUnderlapValue; const side = win._notifSideUnderlapValue;
@@ -848,7 +819,7 @@ PanelWindow {
}; };
} }
return { return {
"x": _notifBodyBlurAnchor.x - (win._notifState.barSide === "left" ? side : 0), "x": _notifBodyBlurAnchor.x - (win._notifDescriptor.barSide === "left" ? side : 0),
"y": _notifBodyBlurAnchor.y - start, "y": _notifBodyBlurAnchor.y - start,
"width": _notifBodyBlurAnchor.width + side, "width": _notifBodyBlurAnchor.width + side,
"height": _notifBodyBlurAnchor.height + start + end "height": _notifBodyBlurAnchor.height + start + end
@@ -865,13 +836,10 @@ PanelWindow {
return Math.max(0, Math.min(win._effectivePopoutMaxCcr, extent - win._surfaceRadius)); return Math.max(0, Math.min(win._effectivePopoutMaxCcr, extent - win._surfaceRadius));
} }
// Active connected surfaces fed to the unified silhouette path. Raw animated
// body rects (no seam/fill overlap); the builder anchors each to the cutout
// edge. Connector radii use the same per-surface helpers as the blur regions.
function _unifiedSurfaces() { function _unifiedSurfaces() {
const arr = []; const arr = [];
const p = win._popoutBodyGeometry; const p = win._popoutBodyGeometry;
if (win._popoutDescriptor.visible && win._popoutState.screen === win._screenName && p.width > 0 && p.height > 0) if (win._popoutDescriptor.visible && win._popoutDescriptor.screenName === win._screenName && p.width > 0 && p.height > 0)
arr.push({ arr.push({
"side": win._popoutDescriptor.barSide, "side": win._popoutDescriptor.barSide,
"body": {"x": p.x, "y": p.y, "width": p.width, "height": p.height}, "body": {"x": p.x, "y": p.y, "width": p.width, "height": p.height},
@@ -975,8 +943,6 @@ PanelWindow {
} catch (e) {} } catch (e) {}
} }
// Coalesce bursts of settings-change signals into a single _buildBlur() call
// on the next event loop tick.
DeferredAction { DeferredAction {
id: blurRebuildAction id: blurRebuildAction
onTriggered: win._runBlurRebuild() onTriggered: win._runBlurRebuild()
@@ -1100,10 +1066,6 @@ PanelWindow {
cutoutRadius: win.cutoutRadius cutoutRadius: win.cutoutRadius
} }
// The entire connected silhouette (frame ring + every active chrome) as one
// SDF in a fragment shader. Analytic fwidth AA crisp at any scale, no FBO;
// the smooth-min radius is the connector. The elevation shadow is derived
// from the same distance field, so elevation-on needs no grouping layer.
ShaderEffect { ShaderEffect {
anchors.fill: parent anchors.fill: parent
visible: win._connectedActive visible: win._connectedActive
+68 -2
View File
@@ -753,9 +753,46 @@ Item {
} }
} }
TextInput { FocusScope {
id: passwordField id: passwordField
property string text: root.passwordBuffer
property int cursorPosition: text.length
signal accepted()
function clampCursorPosition() {
cursorPosition = Math.max(0, Math.min(cursorPosition, text.length));
}
function clear() {
text = "";
cursorPosition = 0;
}
function insertText(value) {
if (value.length === 0)
return;
clampCursorPosition();
text = text.slice(0, cursorPosition) + value + text.slice(cursorPosition);
cursorPosition += value.length;
}
function backspace() {
clampCursorPosition();
if (cursorPosition === 0)
return;
text = text.slice(0, cursorPosition - 1) + text.slice(cursorPosition);
cursorPosition -= 1;
}
function isPrintableText(value) {
if (value.length === 0)
return false;
const code = value.charCodeAt(0);
return code >= 0x20 && code !== 0x7f;
}
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: lockIconContainer.width + Theme.spacingM * 2 anchors.leftMargin: lockIconContainer.width + Theme.spacingM * 2
anchors.rightMargin: { anchors.rightMargin: {
@@ -781,7 +818,6 @@ Item {
focus: true focus: true
enabled: !demoMode enabled: !demoMode
activeFocusOnTab: !demoMode activeFocusOnTab: !demoMode
echoMode: parent.showPassword ? TextInput.Normal : TextInput.Password
onTextChanged: { onTextChanged: {
if (!demoMode) { if (!demoMode) {
root.passwordBuffer = text; root.passwordBuffer = text;
@@ -809,6 +845,8 @@ Item {
return; return;
} }
clear(); clear();
event.accepted = true;
return;
} }
if (pam.passwd.active) { if (pam.passwd.active) {
@@ -816,6 +854,23 @@ Item {
event.accepted = true; event.accepted = true;
return; return;
} }
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
accepted();
event.accepted = true;
return;
}
if (event.key === Qt.Key_Backspace) {
backspace();
event.accepted = true;
return;
}
if (isPrintableText(event.text)) {
insertText(event.text);
event.accepted = true;
}
} }
Component.onCompleted: { Component.onCompleted: {
@@ -849,6 +904,17 @@ Item {
}); });
} }
} }
Connections {
target: root
function onPasswordBufferChanged() {
if (passwordField.text === root.passwordBuffer)
return;
passwordField.text = root.passwordBuffer;
passwordField.cursorPosition = passwordField.text.length;
}
}
} }
KeyboardController { KeyboardController {
@@ -721,6 +721,51 @@ PanelWindow {
} }
} }
// Timeout progress bar: drains as the dismiss timer runs; inset by
// the corner radius and frozen while hovered or during exit.
Rectangle {
id: timeoutBar
readonly property bool active: SettingsData.notificationShowTimeoutBar && notificationData && notificationData.timer && notificationData.timer.interval > 0
property real progress: 1
readonly property real surfaceRadius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
visible: active && progress > 0
anchors.left: parent.left
anchors.leftMargin: surfaceRadius
anchors.bottom: parent.bottom
width: Math.max(0, parent.width - surfaceRadius * 2) * progress
height: Math.max(2, Theme.snap(3, win.dpr))
radius: height / 2
z: 50
opacity: 0.9
color: notificationData && notificationData.urgency === NotificationUrgency.Critical ? Theme.error : Theme.primary
NumberAnimation {
id: progressAnim
target: timeoutBar
property: "progress"
from: 1
to: 0
duration: (notificationData && notificationData.timer && notificationData.timer.interval > 0) ? notificationData.timer.interval : 5000
running: timeoutBar.active && notificationData && notificationData.timer && notificationData.timer.running && !win.exiting
easing.type: Easing.Linear
}
// Reset to full on every (re)start, including an in-place
// restart on a deduped notification (running stays true, so the
// bound animation alone wouldn't re-fire).
Connections {
target: timeoutBar.active ? notificationData.timer : null
function onRunningChanged() {
if (notificationData && notificationData.timer && notificationData.timer.running && !win.exiting) {
timeoutBar.progress = 1;
progressAnim.restart();
}
}
}
}
LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true LayoutMirroring.childrenInherit: true
@@ -871,10 +916,11 @@ PanelWindow {
} }
} }
StyledText { StyledText {
text: notificationData ? (notificationData.summary || "") : "" text: notificationData ? (notificationData.summary || "") : ""
color: Theme.surfaceText color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium font.pixelSize: SettingsData.notificationSummaryFontSize || Theme.fontSizeMedium
font.weight: Font.Medium font.weight: Font.Medium
width: parent.width width: parent.width
elide: Text.ElideRight elide: Text.ElideRight
@@ -890,7 +936,7 @@ PanelWindow {
text: notificationData ? (notificationData.htmlBody || "") : "" text: notificationData ? (notificationData.htmlBody || "") : ""
textFormat: Text.StyledText textFormat: Text.StyledText
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall font.pixelSize: SettingsData.notificationBodyFontSize || Theme.fontSizeSmall
width: parent.width width: parent.width
elide: descriptionExpanded ? Text.ElideNone : Text.ElideRight elide: descriptionExpanded ? Text.ElideNone : Text.ElideRight
horizontalAlignment: Text.AlignLeft horizontalAlignment: Text.AlignLeft
+10 -10
View File
@@ -15,7 +15,7 @@ Item {
property bool isSway: CompositorService.isSway property bool isSway: CompositorService.isSway
property bool isScroll: CompositorService.isScroll property bool isScroll: CompositorService.isScroll
property bool isMiracle: CompositorService.isMiracle property bool isMiracle: CompositorService.isMiracle
property bool isDwl: CompositorService.isDwl || CompositorService.isMango property bool isMango: CompositorService.isMango
property bool isLabwc: CompositorService.isLabwc property bool isLabwc: CompositorService.isLabwc
property string compositorName: { property string compositorName: {
@@ -27,7 +27,7 @@ Item {
return "scroll"; return "scroll";
if (isMiracle) if (isMiracle)
return "miracle"; return "miracle";
if (isDwl) if (isMango)
return "mangowc"; return "mangowc";
if (isLabwc) if (isLabwc)
return "labwc"; return "labwc";
@@ -43,7 +43,7 @@ Item {
return "/assets/sway.svg"; return "/assets/sway.svg";
if (isMiracle) if (isMiracle)
return "/assets/miraclewm.svg"; return "/assets/miraclewm.svg";
if (isDwl) if (isMango)
return "/assets/mango.png"; return "/assets/mango.png";
if (isLabwc) if (isLabwc)
return "/assets/labwc.png"; return "/assets/labwc.png";
@@ -59,7 +59,7 @@ Item {
return "https://github.com/dawsers/scroll"; return "https://github.com/dawsers/scroll";
if (isMiracle) if (isMiracle)
return "https://github.com/miracle-wm-org/miracle-wm"; return "https://github.com/miracle-wm-org/miracle-wm";
if (isDwl) if (isMango)
return "https://github.com/DreamMaoMao/mangowc"; return "https://github.com/DreamMaoMao/mangowc";
if (isLabwc) if (isLabwc)
return "https://labwc.github.io/"; return "https://labwc.github.io/";
@@ -75,7 +75,7 @@ Item {
return I18n.tr("Scroll GitHub"); return I18n.tr("Scroll GitHub");
if (isMiracle) if (isMiracle)
return I18n.tr("Scroll GitHub"); return I18n.tr("Scroll GitHub");
if (isDwl) if (isMango)
return I18n.tr("mangowc GitHub"); return I18n.tr("mangowc GitHub");
if (isLabwc) if (isLabwc)
return I18n.tr("LabWC Website"); return I18n.tr("LabWC Website");
@@ -88,7 +88,7 @@ Item {
property string compositorDiscordUrl: { property string compositorDiscordUrl: {
if (isHyprland) if (isHyprland)
return "https://discord.com/invite/hQ9XvMUjjr"; return "https://discord.com/invite/hQ9XvMUjjr";
if (isDwl) if (isMango)
return "https://discord.gg/CPjbDxesh5"; return "https://discord.gg/CPjbDxesh5";
return ""; return "";
} }
@@ -96,7 +96,7 @@ Item {
property string compositorDiscordTooltip: { property string compositorDiscordTooltip: {
if (isHyprland) if (isHyprland)
return I18n.tr("Hyprland Discord Server"); return I18n.tr("Hyprland Discord Server");
if (isDwl) if (isMango)
return I18n.tr("mangowc Discord Server"); return I18n.tr("mangowc Discord Server");
return ""; return "";
} }
@@ -107,9 +107,9 @@ Item {
property string ircUrl: "https://web.libera.chat/gamja/?channels=#labwc" property string ircUrl: "https://web.libera.chat/gamja/?channels=#labwc"
property string ircTooltip: I18n.tr("LabWC IRC Channel") property string ircTooltip: I18n.tr("LabWC IRC Channel")
property bool showMatrix: isNiri && !isHyprland && !isSway && !isScroll && !isMiracle && !isDwl && !isLabwc property bool showMatrix: isNiri && !isHyprland && !isSway && !isScroll && !isMiracle && !isMango && !isLabwc
property bool showCompositorDiscord: isHyprland || isDwl property bool showCompositorDiscord: isHyprland || isMango
property bool showReddit: isNiri && !isHyprland && !isSway && !isScroll && !isMiracle && !isDwl && !isLabwc property bool showReddit: isNiri && !isHyprland && !isSway && !isScroll && !isMiracle && !isMango && !isLabwc
property bool showIrc: isLabwc property bool showIrc: isLabwc
DankFlickable { DankFlickable {
+1 -1
View File
@@ -722,7 +722,7 @@ Item {
SettingsCard { SettingsCard {
width: parent.width width: parent.width
iconName: "system_tray" iconName: "handyman"
title: I18n.tr("Tray Icon Fix") title: I18n.tr("Tray Icon Fix")
visible: DesktopService.isSystemd visible: DesktopService.isSystemd
@@ -145,7 +145,7 @@ Item {
tags: ["hyprland", "gaps", "override"] tags: ["hyprland", "gaps", "override"]
settingKey: "hyprlandLayoutGapsOverride" settingKey: "hyprlandLayoutGapsOverride"
text: I18n.tr("Window Gaps") text: I18n.tr("Window Gaps")
description: I18n.tr("Space between windows (gaps_in and gaps_out)") description: I18n.tr("Space between windows") + " (gaps_in/gaps_out)"
visible: SettingsData.hyprlandLayoutGapsOverride >= 0 visible: SettingsData.hyprlandLayoutGapsOverride >= 0
value: Math.max(0, SettingsData.hyprlandLayoutGapsOverride) value: Math.max(0, SettingsData.hyprlandLayoutGapsOverride)
minimum: 0 minimum: 0
@@ -159,7 +159,7 @@ Item {
tags: ["hyprland", "radius", "override", "rounding"] tags: ["hyprland", "radius", "override", "rounding"]
settingKey: "hyprlandLayoutRadiusOverrideEnabled" settingKey: "hyprlandLayoutRadiusOverrideEnabled"
text: I18n.tr("Override Corner Radius") text: I18n.tr("Override Corner Radius")
description: I18n.tr("Use custom window rounding instead of theme radius") description: I18n.tr("Use custom window radius instead of theme radius")
checked: SettingsData.hyprlandLayoutRadiusOverride >= 0 checked: SettingsData.hyprlandLayoutRadiusOverride >= 0
onToggled: checked => { onToggled: checked => {
if (checked) { if (checked) {
@@ -173,8 +173,8 @@ Item {
SettingsSliderRow { SettingsSliderRow {
tags: ["hyprland", "radius", "override", "rounding"] tags: ["hyprland", "radius", "override", "rounding"]
settingKey: "hyprlandLayoutRadiusOverride" settingKey: "hyprlandLayoutRadiusOverride"
text: I18n.tr("Window Rounding") text: I18n.tr("Window Corner Radius")
description: I18n.tr("Rounded corners for windows (decoration.rounding)") description: I18n.tr("Rounded corners for windows") + " (decoration.rounding)"
visible: SettingsData.hyprlandLayoutRadiusOverride >= 0 visible: SettingsData.hyprlandLayoutRadiusOverride >= 0
value: Math.max(0, SettingsData.hyprlandLayoutRadiusOverride) value: Math.max(0, SettingsData.hyprlandLayoutRadiusOverride)
minimum: 0 minimum: 0
@@ -203,7 +203,7 @@ Item {
tags: ["hyprland", "border", "override"] tags: ["hyprland", "border", "override"]
settingKey: "hyprlandLayoutBorderSize" settingKey: "hyprlandLayoutBorderSize"
text: I18n.tr("Border Size") text: I18n.tr("Border Size")
description: I18n.tr("Width of window border (general.border_size)") description: I18n.tr("Width of window border") + " (general.border_size)"
visible: SettingsData.hyprlandLayoutBorderSize >= 0 visible: SettingsData.hyprlandLayoutBorderSize >= 0
value: Math.max(0, SettingsData.hyprlandLayoutBorderSize) value: Math.max(0, SettingsData.hyprlandLayoutBorderSize)
minimum: 0 minimum: 0
@@ -229,7 +229,7 @@ Item {
title: I18n.tr("MangoWC Layout Overrides") title: I18n.tr("MangoWC Layout Overrides")
settingKey: "mangoLayout" settingKey: "mangoLayout"
iconName: "crop_square" iconName: "crop_square"
visible: CompositorService.isDwl || CompositorService.isMango visible: CompositorService.isMango
SettingsToggleRow { SettingsToggleRow {
tags: ["mangowc", "mango", "gaps", "override"] tags: ["mangowc", "mango", "gaps", "override"]
@@ -251,7 +251,7 @@ Item {
tags: ["mangowc", "mango", "gaps", "override"] tags: ["mangowc", "mango", "gaps", "override"]
settingKey: "mangoLayoutGapsOverride" settingKey: "mangoLayoutGapsOverride"
text: I18n.tr("Window Gaps") text: I18n.tr("Window Gaps")
description: I18n.tr("Space between windows (gappih/gappiv/gappoh/gappov)") description: I18n.tr("Space between windows") + " (gappih/gappiv/gappoh/gappov)"
visible: SettingsData.mangoLayoutGapsOverride >= 0 visible: SettingsData.mangoLayoutGapsOverride >= 0
value: Math.max(0, SettingsData.mangoLayoutGapsOverride) value: Math.max(0, SettingsData.mangoLayoutGapsOverride)
minimum: 0 minimum: 0
@@ -280,7 +280,7 @@ Item {
tags: ["mangowc", "mango", "radius", "override"] tags: ["mangowc", "mango", "radius", "override"]
settingKey: "mangoLayoutRadiusOverride" settingKey: "mangoLayoutRadiusOverride"
text: I18n.tr("Window Corner Radius") text: I18n.tr("Window Corner Radius")
description: I18n.tr("Rounded corners for windows (border_radius)") description: I18n.tr("Rounded corners for windows") + " (border_radius)"
visible: SettingsData.mangoLayoutRadiusOverride >= 0 visible: SettingsData.mangoLayoutRadiusOverride >= 0
value: Math.max(0, SettingsData.mangoLayoutRadiusOverride) value: Math.max(0, SettingsData.mangoLayoutRadiusOverride)
minimum: 0 minimum: 0
@@ -309,7 +309,7 @@ Item {
tags: ["mangowc", "mango", "border", "override"] tags: ["mangowc", "mango", "border", "override"]
settingKey: "mangoLayoutBorderSize" settingKey: "mangoLayoutBorderSize"
text: I18n.tr("Border Size") text: I18n.tr("Border Size")
description: I18n.tr("Width of window border (borderpx)") description: I18n.tr("Width of window border") + " (borderpx)"
visible: SettingsData.mangoLayoutBorderSize >= 0 visible: SettingsData.mangoLayoutBorderSize >= 0
value: Math.max(0, SettingsData.mangoLayoutBorderSize) value: Math.max(0, SettingsData.mangoLayoutBorderSize)
minimum: 0 minimum: 0
@@ -1,167 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property int subTabIndex: 0
readonly property var workspaceSections: ({
"workspaceSettings": true,
"showWorkspaceIndex": true,
"showWorkspaceName": true,
"showWorkspacePadding": true,
"showWorkspaceApps": true,
"groupWorkspaceApps": true,
"groupActiveWorkspaceApps": true,
"workspaceActiveAppHighlightEnabled": true,
"workspaceFollowFocus": true,
"showOccupiedWorkspacesOnly": true,
"reverseScrolling": true,
"workspaceDragReorder": true,
"dwlShowAllTags": true,
"workspaceIcons": true
})
readonly property var layoutSections: ({
"niriLayout": true,
"niriLayoutGapsOverrideEnabled": true,
"niriLayoutGapsOverride": true,
"niriLayoutRadiusOverrideEnabled": true,
"niriLayoutRadiusOverride": true,
"niriLayoutBorderSizeEnabled": true,
"niriLayoutBorderSize": true,
"hyprlandLayout": true,
"hyprlandLayoutGapsOverrideEnabled": true,
"hyprlandLayoutGapsOverride": true,
"hyprlandLayoutRadiusOverrideEnabled": true,
"hyprlandLayoutRadiusOverride": true,
"hyprlandLayoutBorderSizeEnabled": true,
"hyprlandLayoutBorderSize": true,
"hyprlandResizeOnBorder": true,
"mangoLayout": true,
"mangoLayoutGapsOverrideEnabled": true,
"mangoLayoutGapsOverride": true,
"mangoLayoutRadiusOverrideEnabled": true,
"mangoLayoutRadiusOverride": true,
"mangoLayoutBorderSizeEnabled": true,
"mangoLayoutBorderSize": true
})
function routeSearchTarget(target) {
if (!target)
return;
if (workspaceSections[target]) {
subTabIndex = 0;
} else if (layoutSections[target]) {
subTabIndex = 1;
} else if (target === "windowRules" || target.startsWith("windowRule")) {
subTabIndex = 2;
}
}
Component.onCompleted: routeSearchTarget(SettingsSearchService.targetSection)
Connections {
target: SettingsSearchService
function onTargetSectionChanged() {
root.routeSearchTarget(SettingsSearchService.targetSection);
}
}
ColumnLayout {
anchors.fill: parent
spacing: 0
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 60
color: "transparent"
DankTabBar {
id: compositorTabBar
width: Math.min(500, parent.width - Theme.spacingL * 2)
height: 45
anchors.centerIn: parent
model: [
{
"text": I18n.tr("Workspaces"),
"icon": "view_module"
},
{
"text": I18n.tr("Window Layout"),
"icon": "crop_square"
},
{
"text": I18n.tr("Window Rules"),
"icon": "select_window"
}
]
currentIndex: root.subTabIndex
onTabClicked: index => root.subTabIndex = index
}
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
y: compositorTabBar.y + compositorTabBar.height + 10
width: compositorTabBar.width
height: 1
color: Theme.surface
opacity: 0.56
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
Loader {
anchors.fill: parent
active: root.subTabIndex === 0
visible: active
sourceComponent: WorkspacesTab {}
}
Loader {
anchors.fill: parent
active: root.subTabIndex === 1
visible: active
sourceComponent: CompositorLayoutTab {}
}
Loader {
id: windowRulesLoader
property bool loadedOnce: false
anchors.fill: parent
active: root.subTabIndex === 2 || loadedOnce
visible: root.subTabIndex === 2 && status === Loader.Ready
asynchronous: true
sourceComponent: WindowRulesTab {
pageActive: root.subTabIndex === 2
}
onLoaded: loadedOnce = true
}
StyledText {
anchors.centerIn: parent
visible: root.subTabIndex === 2 && windowRulesLoader.status === Loader.Loading
text: I18n.tr("Loading...", "loading indicator")
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeMedium
}
}
}
}
+8 -2
View File
@@ -41,8 +41,8 @@ Item {
SettingsData.barConfigs; SettingsData.barConfigs;
const index = SettingsData.barConfigs.findIndex(config => config.id === selectedBarId); const index = SettingsData.barConfigs.findIndex(config => config.id === selectedBarId);
if (index < 0) if (index < 0)
return I18n.tr("Bar"); return I18n.tr("Bar", "fallback name for an unnamed bar");
return SettingsData.barConfigs[index].name || I18n.tr("Bar %1").arg(index + 1); return SettingsData.barConfigs[index].name || I18n.tr("Bar %1", "numbered name for an unnamed bar, %1 is its position").arg(index + 1);
} }
property bool selectedBarIsVertical: { property bool selectedBarIsVertical: {
@@ -1754,6 +1754,9 @@ Item {
text: I18n.tr("Y Axis") text: I18n.tr("Y Axis")
description: I18n.tr("Action performed when scrolling vertically on the bar") description: I18n.tr("Action performed when scrolling vertically on the bar")
model: CompositorService.isNiri ? [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")] : [I18n.tr("None"), I18n.tr("Workspace")] model: CompositorService.isNiri ? [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")] : [I18n.tr("None"), I18n.tr("Workspace")]
buttonPadding: Theme.spacingS
minButtonWidth: 44
textSize: Theme.fontSizeSmall
currentIndex: { currentIndex: {
switch (selectedBarConfig?.scrollYBehavior || "workspace") { switch (selectedBarConfig?.scrollYBehavior || "workspace") {
case "none": case "none":
@@ -1792,6 +1795,9 @@ Item {
description: I18n.tr("Action performed when scrolling horizontally on the bar") description: I18n.tr("Action performed when scrolling horizontally on the bar")
visible: CompositorService.isNiri visible: CompositorService.isNiri
model: [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")] model: [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")]
buttonPadding: Theme.spacingS
minButtonWidth: 44
textSize: Theme.fontSizeSmall
currentIndex: { currentIndex: {
switch (selectedBarConfig?.scrollXBehavior || "column") { switch (selectedBarConfig?.scrollXBehavior || "column") {
case "none": case "none":
@@ -1023,7 +1023,6 @@ Singleton {
return parseNiriOutputs(content); return parseNiriOutputs(content);
case "hyprland": case "hyprland":
return parseHyprlandOutputs(content); return parseHyprlandOutputs(content);
case "dwl":
case "mango": case "mango":
return parseMangoOutputs(content); return parseMangoOutputs(content);
default: default:
@@ -1362,7 +1361,6 @@ Singleton {
"grepPattern": "dms.outputs", "grepPattern": "dms.outputs",
"includeLine": "require(\"dms.outputs\")" "includeLine": "require(\"dms.outputs\")"
}; };
case "dwl":
case "mango": case "mango":
return { return {
"configFile": configDir + "/mango/config.conf", "configFile": configDir + "/mango/config.conf",
@@ -1377,7 +1375,7 @@ Singleton {
function checkIncludeStatus() { function checkIncludeStatus() {
const compositor = CompositorService.compositor; const compositor = CompositorService.compositor;
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "dwl" && compositor !== "mango") { if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "mango") {
includeStatus = { includeStatus = {
"exists": false, "exists": false,
"included": false, "included": false,
@@ -1388,8 +1386,7 @@ Singleton {
} }
const filename = (compositor === "niri") ? "outputs.kdl" : ((compositor === "hyprland") ? "outputs.lua" : "outputs.conf"); const filename = (compositor === "niri") ? "outputs.kdl" : ((compositor === "hyprland") ? "outputs.lua" : "outputs.conf");
// mango and dwl both use outputs.conf under ~/.config/mango const compositorArg = (compositor === "mango") ? "mangowc" : compositor;
const compositorArg = (compositor === "dwl" || compositor === "mango") ? "mangowc" : compositor;
checkingInclude = true; checkingInclude = true;
Proc.runCommand("check-outputs-include", ["dms", "config", "resolve-include", compositorArg, filename], (output, exitCode) => { Proc.runCommand("check-outputs-include", ["dms", "config", "resolve-include", compositorArg, filename], (output, exitCode) => {
@@ -1589,9 +1586,6 @@ Singleton {
case "mango": case "mango":
MangoService.generateOutputsConfig(outputsData, finish); MangoService.generateOutputsConfig(outputsData, finish);
break; break;
case "dwl":
DwlService.generateOutputsConfig(outputsData, finish);
break;
default: default:
WlrOutputService.applyOutputsConfig(outputsData, outputs); WlrOutputService.applyOutputsConfig(outputsData, outputs);
finish(true); finish(true);
@@ -317,7 +317,7 @@ StyledRect {
DankToggle { DankToggle {
width: parent.width width: parent.width
text: I18n.tr("Variable Refresh Rate") text: I18n.tr("Variable Refresh Rate")
visible: root.isConnected && !root.isDisabled && !CompositorService.isDwl && !CompositorService.isMango && !CompositorService.isHyprland && !CompositorService.isNiri && (DisplayConfigState.outputs[root.outputName]?.vrr_supported ?? false) visible: root.isConnected && !root.isDisabled && !CompositorService.isMango && !CompositorService.isHyprland && !CompositorService.isNiri && (DisplayConfigState.outputs[root.outputName]?.vrr_supported ?? false)
checked: { checked: {
const pendingVrr = DisplayConfigState.getPendingValue(root.outputName, "vrr"); const pendingVrr = DisplayConfigState.getPendingValue(root.outputName, "vrr");
if (pendingVrr !== undefined) if (pendingVrr !== undefined)
@@ -500,7 +500,7 @@ Item {
Column { Column {
id: displayFormatColumn id: displayFormatColumn
visible: !CompositorService.isDwl && !CompositorService.isMango visible: !CompositorService.isMango
spacing: Theme.spacingXS spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
-2
View File
@@ -282,8 +282,6 @@ Item {
modes.push("niri"); modes.push("niri");
} else if (CompositorService.isHyprland) { } else if (CompositorService.isHyprland) {
modes.push("Hyprland"); modes.push("Hyprland");
} else if (CompositorService.isDwl) {
modes.push("mango");
} else if (CompositorService.isMango) { } else if (CompositorService.isMango) {
modes.push("mango"); modes.push("mango");
} else if (CompositorService.isSway) { } else if (CompositorService.isSway) {
+3
View File
@@ -205,6 +205,9 @@ Item {
tags: ["frame", "border", "color", "theme", "primary", "surface", "default"] tags: ["frame", "border", "color", "theme", "primary", "surface", "default"]
text: I18n.tr("Border Color") text: I18n.tr("Border Color")
model: [I18n.tr("Default"), I18n.tr("Primary"), I18n.tr("Surface"), I18n.tr("Custom")] model: [I18n.tr("Default"), I18n.tr("Primary"), I18n.tr("Surface"), I18n.tr("Custom")]
buttonPadding: Theme.spacingS
minButtonWidth: 44
textSize: Theme.fontSizeSmall
currentIndex: { currentIndex: {
const fc = SettingsData.frameColor; const fc = SettingsData.frameColor;
if (!fc || fc === "default") if (!fc || fc === "default")
+4 -4
View File
@@ -151,7 +151,7 @@ Item {
function runGreeterInstallAction() { function runGreeterInstallAction() {
root.greeterPendingAction = !root.greeterInstalled ? "install" : !root.greeterEnabled ? "activate" : "uninstall"; root.greeterPendingAction = !root.greeterInstalled ? "install" : !root.greeterEnabled ? "activate" : "uninstall";
greeterStatusText = I18n.tr("Opening terminal: ") + root.greeterActionLabel + ""; greeterStatusText = I18n.tr("Opening terminal: ") + root.greeterActionLabel + "...";
greeterInstallActionRunning = true; greeterInstallActionRunning = true;
greeterInstallActionProcess.running = true; greeterInstallActionProcess.running = true;
} }
@@ -188,7 +188,7 @@ Item {
greeterSudoProbeStderr = ""; greeterSudoProbeStderr = "";
greeterTerminalFallbackStderr = ""; greeterTerminalFallbackStderr = "";
greeterTerminalFallbackFromPrecheck = false; greeterTerminalFallbackFromPrecheck = false;
greeterStatusText = I18n.tr("Checking whether sudo authentication is needed"); greeterStatusText = I18n.tr("Checking whether sudo authentication is needed...");
greeterSyncRunning = true; greeterSyncRunning = true;
greeterSudoProbeProcess.running = true; greeterSudoProbeProcess.running = true;
} }
@@ -327,7 +327,7 @@ Item {
onExited: exitCode => { onExited: exitCode => {
const err = (root.greeterSudoProbeStderr || "").trim(); const err = (root.greeterSudoProbeStderr || "").trim();
if (exitCode === 0) { if (exitCode === 0) {
root.greeterStatusText = I18n.tr("Running greeter sync"); root.greeterStatusText = I18n.tr("Running greeter sync...");
greeterSyncProcess.running = true; greeterSyncProcess.running = true;
return; return;
} }
@@ -468,7 +468,7 @@ Item {
id: statusTextArea id: statusTextArea
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingM anchors.margins: Theme.spacingM
text: root.greeterStatusRunning ? I18n.tr("Checking", "greeter status loading") : (root.greeterStatusText || I18n.tr("Click Refresh to check status.", "greeter status placeholder")) text: root.greeterStatusRunning ? I18n.tr("Checking...", "greeter status loading") : (root.greeterStatusText || I18n.tr("Click Refresh to check status.", "greeter status placeholder"))
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.family: "monospace" font.family: "monospace"
color: root.greeterStatusRunning ? Theme.surfaceVariantText : Theme.surfaceText color: root.greeterStatusRunning ? Theme.surfaceVariantText : Theme.surfaceText
@@ -304,8 +304,6 @@ Item {
modes.push("niri"); modes.push("niri");
} else if (CompositorService.isHyprland) { } else if (CompositorService.isHyprland) {
modes.push("Hyprland"); modes.push("Hyprland");
} else if (CompositorService.isDwl) {
modes.push("mango");
} else if (CompositorService.isMango) { } else if (CompositorService.isMango) {
modes.push("mango"); modes.push("mango");
} else if (CompositorService.isSway) { } else if (CompositorService.isSway) {
+6 -102
View File
@@ -643,41 +643,9 @@ Item {
height: NetworkService.networkWiredInfoLoading ? 40 : 0 height: NetworkService.networkWiredInfoLoading ? 40 : 0
visible: NetworkService.networkWiredInfoLoading visible: NetworkService.networkWiredInfoLoading
Row { DankSpinner {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingS size: 20
DankIcon {
id: wiredLoadIcon
name: "sync"
size: 16
color: Theme.surfaceVariantText
SequentialAnimation {
running: NetworkService.networkWiredInfoLoading
loops: Animation.Infinite
OpacityAnimator {
target: wiredLoadIcon
to: 0.3
duration: 400
easing.type: Easing.InOutQuad
}
OpacityAnimator {
target: wiredLoadIcon
to: 1.0
duration: 400
easing.type: Easing.InOutQuad
}
onRunningChanged: if (!running)
wiredLoadIcon.opacity = 1.0
}
}
StyledText {
text: I18n.tr("Loading...")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
} }
} }
} }
@@ -1360,41 +1328,9 @@ Item {
height: NetworkService.networkInfoLoading ? 40 : 0 height: NetworkService.networkInfoLoading ? 40 : 0
visible: NetworkService.networkInfoLoading visible: NetworkService.networkInfoLoading
Row { DankSpinner {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingS size: 20
DankIcon {
id: wifiInfoLoadIcon
name: "sync"
size: 16
color: Theme.surfaceVariantText
SequentialAnimation {
running: NetworkService.networkInfoLoading
loops: Animation.Infinite
OpacityAnimator {
target: wifiInfoLoadIcon
to: 0.3
duration: 400
easing.type: Easing.InOutQuad
}
OpacityAnimator {
target: wifiInfoLoadIcon
to: 1.0
duration: 400
easing.type: Easing.InOutQuad
}
onRunningChanged: if (!running)
wifiInfoLoadIcon.opacity = 1.0
}
}
StyledText {
text: I18n.tr("Loading...")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
} }
} }
@@ -1849,41 +1785,9 @@ Item {
height: VPNService.configLoading ? 40 : 0 height: VPNService.configLoading ? 40 : 0
visible: VPNService.configLoading visible: VPNService.configLoading
Row { DankSpinner {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingS size: 20
DankIcon {
id: vpnLoadIcon
name: "sync"
size: 16
color: Theme.surfaceVariantText
SequentialAnimation {
running: VPNService.configLoading
loops: Animation.Infinite
OpacityAnimator {
target: vpnLoadIcon
to: 0.3
duration: 400
easing.type: Easing.InOutQuad
}
OpacityAnimator {
target: vpnLoadIcon
to: 1.0
duration: 400
easing.type: Easing.InOutQuad
}
onRunningChanged: if (!running)
vpnLoadIcon.opacity = 1.0
}
}
StyledText {
text: I18n.tr("Loading...")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
} }
} }
@@ -200,12 +200,40 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXL spacing: Theme.spacingXL
SettingsCard { SettingsCard {
width: parent.width width: parent.width
iconName: "notifications" iconName: "notifications"
title: I18n.tr("Notification Popups") title: I18n.tr("Notification Popups")
settingKey: "notificationPopups" settingKey: "notificationPopups"
// Font size selectors for summary and body
SettingsDropdownRow {
settingKey: "notificationSummaryFontSize"
tags: ["notification", "font", "summary", "size"]
text: I18n.tr("Summary Font Size")
description: I18n.tr("Set the font size for notification summary text")
options: [I18n.tr("Unset"), "10", "12", "14", "16", "18"]
currentValue: (SettingsData.notificationSummaryFontSize || I18n.tr("Unset")).toString()
onValueChanged: value => {
SettingsData.set("notificationSummaryFontSize", Number(value === I18n.tr("Unset") ? 0 : value));
SettingsData.sendTestNotifications();
}
}
SettingsDropdownRow {
settingKey: "notificationBodyFontSize"
tags: ["notification", "font", "body", "size"]
text: I18n.tr("Body Font Size")
description: I18n.tr("Set the font size for notification body text (htmlBody)")
options: [I18n.tr("Unset"), "10", "12", "14", "16", "18"]
currentValue: (SettingsData.notificationBodyFontSize || I18n.tr("Unset")).toString()
onValueChanged: value => {
SettingsData.set("notificationBodyFontSize", Number(value === I18n.tr("Unset") ? 0 : value));
SettingsData.sendTestNotifications();
}
}
SettingsDropdownRow { SettingsDropdownRow {
settingKey: "notificationPopupPosition" settingKey: "notificationPopupPosition"
tags: ["notification", "popup", "position", "screen", "location"] tags: ["notification", "popup", "position", "screen", "location"]
@@ -273,6 +301,15 @@ Item {
onToggled: checked => SettingsData.set("notificationCompactMode", checked) onToggled: checked => SettingsData.set("notificationCompactMode", checked)
} }
SettingsToggleRow {
settingKey: "notificationShowTimeoutBar"
tags: ["notification", "timeout", "progress", "bar", "timer", "countdown"]
text: I18n.tr("Timeout Progress Bar")
description: I18n.tr("Show a bar that drains as the popup's auto-dismiss timer runs")
checked: SettingsData.notificationShowTimeoutBar
onToggled: checked => SettingsData.set("notificationShowTimeoutBar", checked)
}
SettingsToggleRow { SettingsToggleRow {
settingKey: "notificationDedupeEnabled" settingKey: "notificationDedupeEnabled"
tags: ["notification", "duplicate", "dedupe", "stack", "coalesce", "repeat"] tags: ["notification", "duplicate", "dedupe", "stack", "coalesce", "repeat"]
+34 -35
View File
@@ -33,11 +33,31 @@ FloatingWindow {
} }
readonly property var sortChipOptions: [ readonly property var sortChipOptions: [
{ id: "installed", label: I18n.tr("Installed", "plugin browser filter chip"), toggle: true }, {
{ id: "default", label: I18n.tr("Default", "plugin browser sort option"), toggle: false }, id: "installed",
{ id: "name", label: I18n.tr("Name", "plugin browser sort option"), toggle: false }, label: I18n.tr("Installed", "plugin browser filter chip"),
{ id: "author", label: I18n.tr("Contributor", "plugin browser sort option"), toggle: false }, toggle: true
{ id: "category", label: I18n.tr("Category", "plugin browser sort option"), toggle: false } },
{
id: "default",
label: I18n.tr("Default", "plugin browser sort option"),
toggle: false
},
{
id: "name",
label: I18n.tr("Name", "plugin browser sort option"),
toggle: false
},
{
id: "author",
label: I18n.tr("Contributor", "plugin browser sort option"),
toggle: false
},
{
id: "category",
label: I18n.tr("Category", "plugin browser sort option"),
toggle: false
}
] ]
function normalizedSortMode(mode) { function normalizedSortMode(mode) {
@@ -107,11 +127,13 @@ FloatingWindow {
counts[cat] = (counts[cat] || 0) + 1; counts[cat] = (counts[cat] || 0) + 1;
} }
var keys = Object.keys(counts).sort(); var keys = Object.keys(counts).sort();
var options = [{ var options = [
key: "all", {
label: I18n.tr("All", "plugin browser category filter"), key: "all",
count: plugins.length label: I18n.tr("All", "plugin browser category filter"),
}]; count: plugins.length
}
];
for (var j = 0; j < keys.length; j++) { for (var j = 0; j < keys.length; j++) {
var key = keys[j]; var key = keys[j];
options.push({ options.push({
@@ -726,32 +748,9 @@ FloatingWindow {
anchors.fill: parent anchors.fill: parent
visible: root.isLoading visible: root.isLoading
Column { DankSpinner {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingM running: root.isLoading
DankIcon {
name: "sync"
size: 48
color: Theme.primary
anchors.horizontalCenter: parent.horizontalCenter
smoothTransform: root.isLoading
RotationAnimator on rotation {
from: 0
to: -360
duration: 1000
loops: Animation.Infinite
running: root.isLoading
}
}
StyledText {
text: I18n.tr("Loading...", "loading indicator")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
} }
} }
+2 -25
View File
@@ -335,32 +335,9 @@ FloatingWindow {
anchors.fill: parent anchors.fill: parent
visible: root.isLoading visible: root.isLoading
Column { DankSpinner {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingM running: root.isLoading
DankIcon {
name: "sync"
size: 48
color: Theme.primary
anchors.horizontalCenter: parent.horizontalCenter
smoothTransform: root.isLoading
RotationAnimator on rotation {
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
running: root.isLoading
}
}
StyledText {
text: I18n.tr("Loading...", "loading indicator")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
} }
} }
+5 -12
View File
@@ -48,7 +48,6 @@ Item {
"grepPattern": "dms.cursor", "grepPattern": "dms.cursor",
"includeLine": "require(\"dms.cursor\")" "includeLine": "require(\"dms.cursor\")"
}; };
case "dwl":
case "mango": case "mango":
return { return {
"configFile": configDir + "/mango/config.conf", "configFile": configDir + "/mango/config.conf",
@@ -63,7 +62,7 @@ Item {
function checkCursorIncludeStatus() { function checkCursorIncludeStatus() {
const compositor = CompositorService.compositor; const compositor = CompositorService.compositor;
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "dwl" && compositor !== "mango") { if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "mango") {
cursorIncludeStatus = { cursorIncludeStatus = {
"exists": false, "exists": false,
"included": false, "included": false,
@@ -74,7 +73,7 @@ Item {
} }
const filename = (compositor === "niri") ? "cursor.kdl" : ((compositor === "hyprland") ? "cursor.lua" : "cursor.conf"); const filename = (compositor === "niri") ? "cursor.kdl" : ((compositor === "hyprland") ? "cursor.lua" : "cursor.conf");
const compositorArg = (compositor === "dwl" || compositor === "mango") ? "mangowc" : compositor; const compositorArg = (compositor === "mango") ? "mangowc" : compositor;
checkingCursorInclude = true; checkingCursorInclude = true;
Proc.runCommand("check-cursor-include", ["dms", "config", "resolve-include", compositorArg, filename], (output, exitCode) => { Proc.runCommand("check-cursor-include", ["dms", "config", "resolve-include", compositorArg, filename], (output, exitCode) => {
@@ -194,7 +193,7 @@ Item {
themeColorsTab.templateDetection = JSON.parse(output.trim()); themeColorsTab.templateDetection = JSON.parse(output.trim());
} catch (e) {} } catch (e) {}
}); });
if (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango) if (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango)
checkCursorIncludeStatus(); checkCursorIncludeStatus();
} }
@@ -2016,7 +2015,7 @@ Item {
title: I18n.tr("Cursor Theme") title: I18n.tr("Cursor Theme")
settingKey: "cursorTheme" settingKey: "cursorTheme"
iconName: "mouse" iconName: "mouse"
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango
Column { Column {
width: parent.width width: parent.width
@@ -2181,8 +2180,6 @@ Item {
return SettingsData.cursorSettings.niri?.hideAfterInactiveMs || 0; return SettingsData.cursorSettings.niri?.hideAfterInactiveMs || 0;
if (CompositorService.isHyprland) if (CompositorService.isHyprland)
return SettingsData.cursorSettings.hyprland?.inactiveTimeout || 0; return SettingsData.cursorSettings.hyprland?.inactiveTimeout || 0;
if (CompositorService.isDwl)
return SettingsData.cursorSettings.dwl?.cursorHideTimeout || 0;
if (CompositorService.isMango) if (CompositorService.isMango)
return SettingsData.cursorSettings.mango?.cursorHideTimeout || 0; return SettingsData.cursorSettings.mango?.cursorHideTimeout || 0;
return 0; return 0;
@@ -2201,10 +2198,6 @@ Item {
if (!updated.hyprland) if (!updated.hyprland)
updated.hyprland = {}; updated.hyprland = {};
updated.hyprland.inactiveTimeout = newValue; updated.hyprland.inactiveTimeout = newValue;
} else if (CompositorService.isDwl) {
if (!updated.dwl)
updated.dwl = {};
updated.dwl.cursorHideTimeout = newValue;
} else if (CompositorService.isMango) { } else if (CompositorService.isMango) {
if (!updated.mango) if (!updated.mango)
updated.mango = {}; updated.mango = {};
@@ -2688,7 +2681,7 @@ Item {
spacing: Theme.spacingS spacing: Theme.spacingS
DankIcon { DankIcon {
name: "folder" name: "settings"
size: 16 size: 16
color: Theme.primary color: Theme.primary
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
+2 -2
View File
@@ -139,7 +139,7 @@ Item {
} }
StyledText { StyledText {
text: UsersService.refreshing ? I18n.tr("Refreshing") : "" text: UsersService.refreshing ? I18n.tr("Refreshing...") : ""
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -474,7 +474,7 @@ Item {
spacing: Theme.spacingM spacing: Theme.spacingM
DankButton { DankButton {
text: root.operationPending ? I18n.tr("Working") : I18n.tr("Create User") text: root.operationPending ? I18n.tr("Working...") : I18n.tr("Create User")
iconName: "person_add" iconName: "person_add"
backgroundColor: Theme.primary backgroundColor: Theme.primary
textColor: Theme.primaryText textColor: Theme.primaryText
@@ -1271,6 +1271,7 @@ Item {
tags: ["blur", "layer", "niri", "compositor"] tags: ["blur", "layer", "niri", "compositor"]
title: I18n.tr("Blur Wallpaper Layer") title: I18n.tr("Blur Wallpaper Layer")
settingKey: "blurWallpaper" settingKey: "blurWallpaper"
iconName: "blur_on"
visible: CompositorService.isNiri visible: CompositorService.isNiri
SettingsToggleRow { SettingsToggleRow {
@@ -330,7 +330,7 @@ FloatingWindow {
delegate: Rectangle { delegate: Rectangle {
width: widgetList.width width: widgetList.width
height: 60 height: Math.max(60, textColumn.implicitHeight + 24)
radius: Theme.cornerRadius radius: Theme.cornerRadius
property bool isSelected: root.keyboardNavigationActive && index === root.selectedIndex property bool isSelected: root.keyboardNavigationActive && index === root.selectedIndex
color: isSelected ? Theme.withAlpha(Theme.primary, root.blurActive ? 0.22 : 0.16) : widgetArea.containsMouse ? Theme.withAlpha(Theme.primary, root.blurActive ? 0.14 : 0.08) : Theme.withAlpha(Theme.surfaceVariant, root.rowAlpha) color: isSelected ? Theme.withAlpha(Theme.primary, root.blurActive ? 0.22 : 0.16) : widgetArea.containsMouse ? Theme.withAlpha(Theme.primary, root.blurActive ? 0.14 : 0.08) : Theme.withAlpha(Theme.surfaceVariant, root.rowAlpha)
@@ -351,9 +351,10 @@ FloatingWindow {
} }
Column { Column {
id: textColumn
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: 2 spacing: 2
width: parent.width - Theme.iconSize - Theme.spacingM * 3 width: parent.width - Theme.iconSize * 2 - Theme.spacingM * 4 + 4
StyledText { StyledText {
text: modelData.text text: modelData.text
@@ -362,6 +363,7 @@ FloatingWindow {
color: Theme.surfaceText color: Theme.surfaceText
elide: Text.ElideRight elide: Text.ElideRight
width: parent.width width: parent.width
wrapMode: Text.WordWrap
} }
StyledText { StyledText {
+3 -3
View File
@@ -37,10 +37,10 @@ Item {
{ {
"id": "layout", "id": "layout",
"text": I18n.tr("Layout"), "text": I18n.tr("Layout"),
"description": I18n.tr("Display and switch DWL layouts"), "description": I18n.tr("Display and switch MangoWC layouts"),
"icon": "view_quilt", "icon": "view_quilt",
"enabled": (CompositorService.isDwl && DwlService.dwlAvailable) || (CompositorService.isMango && MangoService.available), "enabled": CompositorService.isMango && MangoService.available,
"warning": CompositorService.isMango ? (!MangoService.available ? I18n.tr("DWL service not available") : undefined) : (!CompositorService.isDwl ? I18n.tr("Requires DWL compositor") : (!DwlService.dwlAvailable ? I18n.tr("DWL service not available") : undefined)) "warning": !CompositorService.isMango ? I18n.tr("Requires MangoWC compositor") : (!MangoService.available ? I18n.tr("Mango service not available") : undefined)
}, },
{ {
"id": "launcherButton", "id": "launcherButton",
@@ -90,7 +90,7 @@ Column {
property real originalY: y property real originalY: y
width: itemsList.width width: itemsList.width
height: 70 height: Math.max(70, textColumn.implicitHeight + 32)
z: held ? 2 : 1 z: held ? 2 : 1
Rectangle { Rectangle {
@@ -123,6 +123,7 @@ Column {
} }
Column { Column {
id: textColumn
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Theme.spacingM * 3 + 40 + Theme.iconSize anchors.leftMargin: Theme.spacingM * 3 + 40 + Theme.iconSize
anchors.right: actionButtons.left anchors.right: actionButtons.left
@@ -137,6 +138,7 @@ Column {
color: modelData.enabled ? Theme.surfaceText : Theme.outline color: modelData.enabled ? Theme.surfaceText : Theme.outline
elide: Text.ElideRight elide: Text.ElideRight
width: parent.width width: parent.width
wrapMode: Text.WordWrap
} }
StyledText { StyledText {
@@ -51,7 +51,7 @@ SettingsCard {
SettingsButtonGroupRow { SettingsButtonGroupRow {
text: I18n.tr("Occupied Color") text: I18n.tr("Occupied Color")
model: ["none", "sec", "s", "sc", "sch", "schh"] model: ["none", "sec", "s", "sc", "sch", "schh"]
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango
buttonHeight: 22 buttonHeight: 22
minButtonWidth: 36 minButtonWidth: 36
buttonPadding: Theme.spacingS buttonPadding: Theme.spacingS
@@ -87,7 +87,7 @@ SettingsCard {
height: 1 height: 1
color: Theme.outline color: Theme.outline
opacity: 0.15 opacity: 0.15
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango
} }
SettingsButtonGroupRow { SettingsButtonGroupRow {
@@ -124,12 +124,12 @@ SettingsCard {
height: 1 height: 1
color: Theme.outline color: Theme.outline
opacity: 0.15 opacity: 0.15
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
} }
SettingsButtonGroupRow { SettingsButtonGroupRow {
text: I18n.tr("Urgent Color") text: I18n.tr("Urgent Color")
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
model: ["err", "pri", "sec", "s", "sc"] model: ["err", "pri", "sec", "s", "sc"]
buttonHeight: 22 buttonHeight: 22
minButtonWidth: 36 minButtonWidth: 36
@@ -153,7 +153,7 @@ Item {
text: I18n.tr("Follow Monitor Focus") text: I18n.tr("Follow Monitor Focus")
description: I18n.tr("Show workspaces of the currently focused monitor") description: I18n.tr("Show workspaces of the currently focused monitor")
checked: SettingsData.workspaceFollowFocus checked: SettingsData.workspaceFollowFocus
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
onToggled: checked => SettingsData.set("workspaceFollowFocus", checked) onToggled: checked => SettingsData.set("workspaceFollowFocus", checked)
} }
@@ -193,7 +193,7 @@ Item {
text: I18n.tr("Show All Tags") text: I18n.tr("Show All Tags")
description: I18n.tr("Show all 9 tags instead of only occupied tags") description: I18n.tr("Show all 9 tags instead of only occupied tags")
checked: SettingsData.dwlShowAllTags checked: SettingsData.dwlShowAllTags
visible: CompositorService.isDwl || CompositorService.isMango visible: CompositorService.isMango
onToggled: checked => SettingsData.set("dwlShowAllTags", checked) onToggled: checked => SettingsData.set("dwlShowAllTags", checked)
} }
} }
+7 -3
View File
@@ -388,11 +388,15 @@ Singleton {
return "text"; return "text";
} }
function hashedPinnedEntry(entryHash) { function getPinnedEntryByHash(entryHash) {
if (!entryHash) { if (!entryHash) {
return false; return null;
} }
return pinnedEntries.some(pinnedEntry => pinnedEntry.hash === entryHash); return internalEntries.find(entry => entry.pinned && entry.hash === entryHash) || null;
}
function hashedPinnedEntry(entryHash) {
return getPinnedEntryByHash(entryHash) !== null;
} }
onClipboardAvailableChanged: { onClipboardAvailableChanged: {
+11 -106
View File
@@ -15,7 +15,6 @@ Singleton {
property bool isHyprland: false property bool isHyprland: false
property bool isNiri: false property bool isNiri: false
property bool isDwl: false
property bool isMango: false property bool isMango: false
property bool isSway: false property bool isSway: false
property bool isScroll: false property bool isScroll: false
@@ -97,12 +96,6 @@ Singleton {
return hyprlandMonitor.scale; return hyprlandMonitor.scale;
} }
if (isDwl && screen) {
const dwlScale = DwlService.getOutputScale(screen.name);
if (dwlScale !== undefined && dwlScale > 0)
return dwlScale;
}
if (isMango && screen) { if (isMango && screen) {
const mangoScale = MangoService.getOutputScale(screen.name); const mangoScale = MangoService.getOutputScale(screen.name);
if (mangoScale !== undefined && mangoScale > 0) if (mangoScale !== undefined && mangoScale > 0)
@@ -121,9 +114,7 @@ Singleton {
else if (isSway || isScroll || isMiracle) { else if (isSway || isScroll || isMiracle) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
screenName = focusedWs?.monitor?.name || ""; screenName = focusedWs?.monitor?.name || "";
} else if (isDwl && DwlService.activeOutput) } else if (isMango && MangoService.activeOutput)
screenName = DwlService.activeOutput;
else if (isMango && MangoService.activeOutput)
screenName = MangoService.activeOutput; screenName = MangoService.activeOutput;
if (!screenName) if (!screenName)
@@ -192,19 +183,9 @@ Singleton {
Qt.callLater(() => { Qt.callLater(() => {
NiriService.generateNiriLayoutConfig(); NiriService.generateNiriLayoutConfig();
HyprlandService.generateLayoutConfig(); HyprlandService.generateLayoutConfig();
DwlService.generateLayoutConfig();
}); });
} }
Connections {
target: DwlService
function onStateChanged() {
if (isDwl && !isHyprland && !isNiri) {
scheduleSort();
}
}
}
Connections { Connections {
target: MangoService target: MangoService
function onStateChanged() { function onStateChanged() {
@@ -271,13 +252,7 @@ Singleton {
function _specialWorkspaceNameFromMonitor(monitor) { function _specialWorkspaceNameFromMonitor(monitor) {
if (!monitor) if (!monitor)
return ""; return "";
const candidates = [ const candidates = [monitor.activeSpecialWorkspace?.name, monitor.specialWorkspace?.name, monitor.lastIpcObject?.specialWorkspace?.name, monitor.lastIpcObject?.specialWorkspace, monitor.lastIpcObject?.activeSpecialWorkspace?.name];
monitor.activeSpecialWorkspace?.name,
monitor.specialWorkspace?.name,
monitor.lastIpcObject?.specialWorkspace?.name,
monitor.lastIpcObject?.specialWorkspace,
monitor.lastIpcObject?.activeSpecialWorkspace?.name
];
for (let i = 0; i < candidates.length; i++) { for (let i = 0; i < candidates.length; i++) {
const normalized = _normalizeSpecialWorkspaceName(candidates[i]); const normalized = _normalizeSpecialWorkspaceName(candidates[i]);
if (normalized) if (normalized)
@@ -860,7 +835,6 @@ Singleton {
Qt.callLater(() => { Qt.callLater(() => {
NiriService.generateNiriLayoutConfig(); NiriService.generateNiriLayoutConfig();
HyprlandService.generateLayoutConfig(); HyprlandService.generateLayoutConfig();
DwlService.generateLayoutConfig();
MangoService.generateLayoutConfig(); MangoService.generateLayoutConfig();
}); });
} }
@@ -870,7 +844,6 @@ Singleton {
if (mangoSignature && mangoSignature.length > 0) { if (mangoSignature && mangoSignature.length > 0) {
isHyprland = false; isHyprland = false;
isNiri = false; isNiri = false;
isDwl = false;
isMango = true; isMango = true;
isSway = false; isSway = false;
isScroll = false; isScroll = false;
@@ -884,7 +857,6 @@ Singleton {
if (hyprlandSignature && hyprlandSignature.length > 0 && !niriSocket && !swaySocket && !scrollSocket && !miracleSocket && !labwcPid) { if (hyprlandSignature && hyprlandSignature.length > 0 && !niriSocket && !swaySocket && !scrollSocket && !miracleSocket && !labwcPid) {
isHyprland = true; isHyprland = true;
isNiri = false; isNiri = false;
isDwl = false;
isMango = false; isMango = false;
isSway = false; isSway = false;
isScroll = false; isScroll = false;
@@ -900,7 +872,6 @@ Singleton {
if (exitCode === 0) { if (exitCode === 0) {
isNiri = true; isNiri = true;
isHyprland = false; isHyprland = false;
isDwl = false;
isMango = false; isMango = false;
isSway = false; isSway = false;
isScroll = false; isScroll = false;
@@ -919,7 +890,6 @@ Singleton {
if (exitCode === 0) { if (exitCode === 0) {
isNiri = false; isNiri = false;
isHyprland = false; isHyprland = false;
isDwl = false;
isSway = true; isSway = true;
isScroll = false; isScroll = false;
isMiracle = false; isMiracle = false;
@@ -936,7 +906,6 @@ Singleton {
if (exitCode === 0) { if (exitCode === 0) {
isNiri = false; isNiri = false;
isHyprland = false; isHyprland = false;
isDwl = false;
isMango = false; isMango = false;
isSway = false; isSway = false;
isScroll = false; isScroll = false;
@@ -954,7 +923,6 @@ Singleton {
if (exitCode === 0) { if (exitCode === 0) {
isNiri = false; isNiri = false;
isHyprland = false; isHyprland = false;
isDwl = false;
isMango = false; isMango = false;
isSway = false; isSway = false;
isScroll = true; isScroll = true;
@@ -970,7 +938,6 @@ Singleton {
if (labwcPid && labwcPid.length > 0) { if (labwcPid && labwcPid.length > 0) {
isHyprland = false; isHyprland = false;
isNiri = false; isNiri = false;
isDwl = false;
isMango = false; isMango = false;
isSway = false; isSway = false;
isScroll = false; isScroll = false;
@@ -981,45 +948,15 @@ Singleton {
return; return;
} }
if (DMSService.dmsAvailable) { isHyprland = false;
Qt.callLater(checkForDwl); isNiri = false;
} else { isMango = false;
isHyprland = false; isSway = false;
isNiri = false; isScroll = false;
isDwl = false; isMiracle = false;
isMango = false; isLabwc = false;
isSway = false; compositor = "unknown";
isScroll = false; log.warn("No compositor detected");
isMiracle = false;
isLabwc = false;
compositor = "unknown";
log.warn("No compositor detected");
}
}
Connections {
target: DMSService
function onCapabilitiesReceived() {
if (!isHyprland && !isNiri && !isDwl && !isMango && !isLabwc) {
checkForDwl();
}
}
}
function checkForDwl() {
if (isMango)
return;
if (DMSService.apiVersion >= 12 && DMSService.capabilities.includes("dwl")) {
isHyprland = false;
isNiri = false;
isDwl = true;
isSway = false;
isScroll = false;
isMiracle = false;
isLabwc = false;
compositor = "dwl";
log.info("Detected DWL via DMS capability");
}
} }
function powerOffMonitors() { function powerOffMonitors() {
@@ -1027,8 +964,6 @@ Singleton {
return NiriService.powerOffMonitors(); return NiriService.powerOffMonitors();
if (isHyprland) if (isHyprland)
return HyprlandService.dpmsOff(); return HyprlandService.dpmsOff();
if (isDwl)
return _dwlPowerOffMonitors();
if (isMango) if (isMango)
return MangoService.powerOffMonitors(); return MangoService.powerOffMonitors();
if (isSway || isScroll || isMiracle) { if (isSway || isScroll || isMiracle) {
@@ -1048,8 +983,6 @@ Singleton {
return NiriService.powerOnMonitors(); return NiriService.powerOnMonitors();
if (isHyprland) if (isHyprland)
return HyprlandService.dpmsOn(); return HyprlandService.dpmsOn();
if (isDwl)
return _dwlPowerOnMonitors();
if (isMango) if (isMango)
return MangoService.powerOnMonitors(); return MangoService.powerOnMonitors();
if (isSway || isScroll || isMiracle) { if (isSway || isScroll || isMiracle) {
@@ -1063,32 +996,4 @@ Singleton {
} }
log.warn("Cannot power on monitors, unknown compositor"); log.warn("Cannot power on monitors, unknown compositor");
} }
function _dwlPowerOffMonitors() {
if (!Quickshell.screens || Quickshell.screens.length === 0) {
log.warn("No screens available for DWL power off");
return;
}
for (let i = 0; i < Quickshell.screens.length; i++) {
const screen = Quickshell.screens[i];
if (screen && screen.name) {
Quickshell.execDetached(["mmsg", "dispatch", "disable_monitor," + screen.name]);
}
}
}
function _dwlPowerOnMonitors() {
if (!Quickshell.screens || Quickshell.screens.length === 0) {
log.warn("No screens available for DWL power on");
return;
}
for (let i = 0; i < Quickshell.screens.length; i++) {
const screen = Quickshell.screens[i];
if (screen && screen.name) {
Quickshell.execDetached(["mmsg", "dispatch", "enable_monitor," + screen.name]);
}
}
}
} }
+3 -6
View File
@@ -49,7 +49,6 @@ Singleton {
signal capabilitiesReceived signal capabilitiesReceived
signal credentialsRequest(var data) signal credentialsRequest(var data)
signal bluetoothPairingRequest(var data) signal bluetoothPairingRequest(var data)
signal dwlStateUpdate(var data)
signal brightnessStateUpdate(var data) signal brightnessStateUpdate(var data)
signal brightnessDeviceUpdate(var device) signal brightnessDeviceUpdate(var device)
signal wlrOutputStateUpdate(var data) signal wlrOutputStateUpdate(var data)
@@ -68,7 +67,7 @@ Singleton {
property bool screensaverInhibited: false property bool screensaverInhibited: false
property var screensaverInhibitors: [] property var screensaverInhibitors: []
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "theme.auto", "bluetooth", "bluetooth.pairing", "dwl", "brightness", "wlroutput", "evdev", "browser", "dbus", "clipboard", "location", "sysupdate"] property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "theme.auto", "bluetooth", "bluetooth.pairing", "brightness", "wlroutput", "evdev", "browser", "dbus", "clipboard", "location", "sysupdate"]
Component.onCompleted: { Component.onCompleted: {
if (socketPath && socketPath.length > 0) { if (socketPath && socketPath.length > 0) {
@@ -286,7 +285,7 @@ Singleton {
function removeSubscription(service) { function removeSubscription(service) {
if (activeSubscriptions.includes("all")) { if (activeSubscriptions.includes("all")) {
const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "dwl", "brightness", "browser", "location"]; const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "brightness", "browser", "location"];
const filtered = allServices.filter(s => s !== service); const filtered = allServices.filter(s => s !== service);
subscribe(filtered); subscribe(filtered);
} else { } else {
@@ -308,7 +307,7 @@ Singleton {
excludeServices = [excludeServices]; excludeServices = [excludeServices];
} }
const allServices = ["network", "loginctl", "freedesktop", "gamma", "theme.auto", "bluetooth", "cups", "dwl", "brightness", "browser", "dbus", "location"]; const allServices = ["network", "loginctl", "freedesktop", "gamma", "theme.auto", "bluetooth", "cups", "brightness", "browser", "dbus", "location"];
const filtered = allServices.filter(s => !excludeServices.includes(s)); const filtered = allServices.filter(s => !excludeServices.includes(s));
subscribe(filtered); subscribe(filtered);
} }
@@ -354,8 +353,6 @@ Singleton {
bluetoothPairingRequest(data); bluetoothPairingRequest(data);
} else if (service === "cups") { } else if (service === "cups") {
cupsStateUpdate(data); cupsStateUpdate(data);
} else if (service === "dwl") {
dwlStateUpdate(data);
} else if (service === "brightness") { } else if (service === "brightness") {
brightnessStateUpdate(data); brightnessStateUpdate(data);
} else if (service === "brightness.update") { } else if (service === "brightness.update") {
-461
View File
@@ -1,461 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
Singleton {
id: root
readonly property var log: Log.scoped("DwlService")
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
readonly property string mangoDmsDir: configDir + "/mango/dms"
readonly property string outputsPath: mangoDmsDir + "/outputs.conf"
readonly property string layoutPath: mangoDmsDir + "/layout.conf"
readonly property string cursorPath: mangoDmsDir + "/cursor.conf"
property int _lastGapValue: -1
property bool dwlAvailable: false
// Alias so consumers can treat DwlService/MangoService uniformly via `.available`.
readonly property bool available: dwlAvailable
property var outputs: ({})
property var tagCount: 9
property var layouts: []
property string activeOutput: ""
property var outputScales: ({})
property string currentKeyboardLayout: {
if (!outputs || !activeOutput)
return "";
const output = outputs[activeOutput];
return (output && output.kbLayout) || "";
}
signal stateChanged
Connections {
target: SettingsData
function onBarConfigsChanged() {
if (!CompositorService.isDwl)
return;
const newGaps = Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4));
if (newGaps === root._lastGapValue)
return;
root._lastGapValue = newGaps;
generateLayoutConfig();
}
}
Connections {
target: CompositorService
function onIsDwlChanged() {
if (CompositorService.isDwl)
generateLayoutConfig();
}
}
Connections {
target: DMSService
function onCapabilitiesReceived() {
checkCapabilities();
}
function onConnectionStateChanged() {
if (DMSService.isConnected) {
checkCapabilities();
} else {
dwlAvailable = false;
}
}
function onDwlStateUpdate(data) {
if (dwlAvailable) {
handleStateUpdate(data);
}
}
}
Component.onCompleted: {
if (DMSService.dmsAvailable)
checkCapabilities();
if (dwlAvailable)
refreshOutputScales();
if (CompositorService.isDwl)
Qt.callLater(generateLayoutConfig);
}
function checkCapabilities() {
if (!DMSService.capabilities || !Array.isArray(DMSService.capabilities)) {
dwlAvailable = false;
return;
}
const hasDwl = DMSService.capabilities.includes("dwl");
if (hasDwl && !dwlAvailable) {
dwlAvailable = true;
log.info("DWL capability detected");
requestState();
refreshOutputScales();
} else if (!hasDwl) {
dwlAvailable = false;
}
}
function requestState() {
if (!DMSService.isConnected || !dwlAvailable) {
return;
}
DMSService.sendRequest("dwl.getState", null, response => {
if (response.result) {
handleStateUpdate(response.result);
}
});
}
function handleStateUpdate(state) {
outputs = state.outputs || {};
tagCount = state.tagCount || 9;
layouts = state.layouts || [];
activeOutput = state.activeOutput || "";
stateChanged();
}
function setTags(outputName, tagmask, toggleTagset) {
if (!DMSService.isConnected || !dwlAvailable) {
return;
}
DMSService.sendRequest("dwl.setTags", {
"output": outputName,
"tagmask": tagmask,
"toggleTagset": toggleTagset
}, response => {
if (response.error) {
log.warn("setTags error:", response.error);
}
});
}
function setClientTags(outputName, andTags, xorTags) {
if (!DMSService.isConnected || !dwlAvailable) {
return;
}
DMSService.sendRequest("dwl.setClientTags", {
"output": outputName,
"andTags": andTags,
"xorTags": xorTags
}, response => {
if (response.error) {
log.warn("setClientTags error:", response.error);
}
});
}
function setLayout(outputName, index) {
if (!DMSService.isConnected || !dwlAvailable) {
return;
}
DMSService.sendRequest("dwl.setLayout", {
"output": outputName,
"index": index
}, response => {
if (response.error) {
log.warn("setLayout error:", response.error);
}
});
}
function getOutputState(outputName) {
if (!outputs || !outputs[outputName]) {
return null;
}
return outputs[outputName];
}
function getActiveTags(outputName) {
const output = getOutputState(outputName);
if (!output || !output.tags) {
return [];
}
return output.tags.filter(tag => tag.state === 1).map(tag => tag.tag);
}
function getTagsWithClients(outputName) {
const output = getOutputState(outputName);
if (!output || !output.tags) {
return [];
}
return output.tags.filter(tag => tag.clients > 0).map(tag => tag.tag);
}
function getUrgentTags(outputName) {
const output = getOutputState(outputName);
if (!output || !output.tags) {
return [];
}
return output.tags.filter(tag => tag.state === 2).map(tag => tag.tag);
}
function switchToTag(outputName, tagIndex) {
const tagmask = 1 << tagIndex;
setTags(outputName, tagmask, 0);
}
function toggleTag(outputName, tagIndex) {
const output = getOutputState(outputName);
if (!output || !output.tags) {
log.debug("toggleTag: no output or tags for", outputName);
return;
}
let currentMask = 0;
output.tags.forEach(tag => {
if (tag.state === 1) {
currentMask |= (1 << tag.tag);
}
});
const clickedMask = 1 << tagIndex;
const newMask = currentMask ^ clickedMask;
log.debug("toggleTag:", outputName, "tag:", tagIndex, "currentMask:", currentMask.toString(2), "clickedMask:", clickedMask.toString(2), "newMask:", newMask.toString(2));
if (newMask === 0) {
log.debug("toggleTag: newMask is 0, switching to tag", tagIndex);
setTags(outputName, 1 << tagIndex, 0);
} else {
log.debug("toggleTag: setting combined mask", newMask);
setTags(outputName, newMask, 0);
}
}
function quit() {
Quickshell.execDetached(["mmsg", "dispatch", "quit"]);
}
Process {
id: scaleQueryProcess
command: ["mmsg", "get", "all-monitors"]
running: false
stdout: StdioCollector {
onStreamFinished: {
try {
const newScales = {};
const data = JSON.parse(text.trim());
const monitors = data.monitors || [];
for (const mon of monitors) {
if (mon.name && typeof mon.scale === "number" && mon.scale > 0) {
newScales[mon.name] = mon.scale;
}
}
outputScales = newScales;
} catch (e) {
log.warn("Failed to parse mmsg output:", e);
}
}
}
onExited: exitCode => {
if (exitCode !== 0) {
log.warn("mmsg failed with exit code:", exitCode);
}
}
}
function refreshOutputScales() {
if (!dwlAvailable)
return;
scaleQueryProcess.running = true;
}
function getOutputScale(outputName) {
return outputScales[outputName];
}
function getVisibleTags(outputName) {
const output = getOutputState(outputName);
if (!output || !output.tags) {
return [];
}
const visibleTags = new Set();
output.tags.forEach(tag => {
if (tag.state === 1 || tag.clients > 0) {
visibleTags.add(tag.tag);
}
});
return Array.from(visibleTags).sort((a, b) => a - b);
}
function generateOutputsConfig(outputsData, callback) {
if (!outputsData || Object.keys(outputsData).length === 0) {
if (callback)
callback(false);
return;
}
let lines = ["# Auto-generated by DMS - do not edit manually", ""];
for (const outputName in outputsData) {
const output = outputsData[outputName];
if (!output)
continue;
let width = 1920;
let height = 1080;
let refreshRate = 60;
if (output.modes && output.current_mode !== undefined) {
const mode = output.modes[output.current_mode];
if (mode) {
width = mode.width || 1920;
height = mode.height || 1080;
refreshRate = Math.round((mode.refresh_rate || 60000) / 1000);
}
}
const x = output.logical?.x ?? 0;
const y = output.logical?.y ?? 0;
const scale = output.logical?.scale ?? 1.0;
const transform = transformToMango(output.logical?.transform ?? "Normal");
const vrr = output.vrr_enabled ? 1 : 0;
const rule = ["name:^" + outputName + "$", "width:" + width, "height:" + height, "refresh:" + refreshRate, "x:" + x, "y:" + y, "scale:" + scale, "rr:" + transform, "vrr:" + vrr].join(",");
lines.push("monitorrule=" + rule);
}
lines.push("");
const content = lines.join("\n");
Proc.runCommand("mango-write-outputs", ["sh", "-c", `mkdir -p "${mangoDmsDir}" && cat > "${outputsPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
if (exitCode !== 0) {
log.warn("Failed to write outputs config:", output);
if (callback)
callback(false);
return;
}
log.info("Generated outputs config at", outputsPath);
if (CompositorService.isDwl)
reloadConfig();
if (callback)
callback(true);
});
}
function reloadConfig() {
Proc.runCommand("mango-reload", ["mmsg", "dispatch", "reload_config"], (output, exitCode) => {
if (exitCode !== 0)
log.warn("mmsg reload_config failed:", output);
});
}
function generateLayoutConfig() {
if (!CompositorService.isDwl)
return;
const defaultRadius = typeof SettingsData !== "undefined" ? SettingsData.cornerRadius : 12;
const defaultGaps = typeof SettingsData !== "undefined" ? Math.max(4, (SettingsData.barConfigs[0]?.spacing ?? 4)) : 4;
const defaultBorderSize = 2;
const cornerRadius = (typeof SettingsData !== "undefined" && SettingsData.mangoLayoutRadiusOverride >= 0) ? SettingsData.mangoLayoutRadiusOverride : defaultRadius;
const gaps = (typeof SettingsData !== "undefined" && SettingsData.mangoLayoutGapsOverride >= 0) ? SettingsData.mangoLayoutGapsOverride : defaultGaps;
const borderSize = (typeof SettingsData !== "undefined" && SettingsData.mangoLayoutBorderSize >= 0) ? SettingsData.mangoLayoutBorderSize : defaultBorderSize;
let content = `# Auto-generated by DMS - do not edit manually
border_radius=${cornerRadius}
gappih=${gaps}
gappiv=${gaps}
gappoh=${gaps}
gappov=${gaps}
borderpx=${borderSize}
`;
Proc.runCommand("mango-write-layout", ["sh", "-c", `mkdir -p "${mangoDmsDir}" && cat > "${layoutPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
if (exitCode !== 0) {
log.warn("Failed to write layout config:", output);
return;
}
log.info("Generated layout config at", layoutPath);
reloadConfig();
});
}
function transformToMango(transform) {
switch (transform) {
case "Normal":
return 0;
case "90":
return 1;
case "180":
return 2;
case "270":
return 3;
case "Flipped":
return 4;
case "Flipped90":
return 5;
case "Flipped180":
return 6;
case "Flipped270":
return 7;
default:
return 0;
}
}
function generateCursorConfig() {
if (!CompositorService.isDwl)
return;
log.debug("Generating cursor config...");
const settings = typeof SettingsData !== "undefined" ? SettingsData.cursorSettings : null;
if (!settings) {
Proc.runCommand("mango-write-cursor", ["sh", "-c", `mkdir -p "${mangoDmsDir}" && : > "${cursorPath}"`], (output, exitCode) => {
if (exitCode !== 0)
log.warn("Failed to write cursor config:", output);
});
return;
}
const themeName = settings.theme === "System Default" ? (SettingsData.systemDefaultCursorTheme || "") : settings.theme;
const size = settings.size || 24;
const hideTimeout = settings.dwl?.cursorHideTimeout || 0;
const isDefaultConfig = !themeName && size === 24 && hideTimeout === 0;
if (isDefaultConfig) {
Proc.runCommand("mango-write-cursor", ["sh", "-c", `mkdir -p "${mangoDmsDir}" && : > "${cursorPath}"`], (output, exitCode) => {
if (exitCode !== 0)
log.warn("Failed to write cursor config:", output);
});
return;
}
let content = `# Auto-generated by DMS - do not edit manually
cursor_size=${size}`;
if (themeName)
content += `\ncursor_theme=${themeName}`;
if (hideTimeout > 0)
content += `\ncursor_hide_timeout=${hideTimeout}`;
content += `\n`;
Proc.runCommand("mango-write-cursor", ["sh", "-c", `mkdir -p "${mangoDmsDir}" && cat > "${cursorPath}" << 'EOF'\n${content}EOF`], (output, exitCode) => {
if (exitCode !== 0) {
log.warn("Failed to write cursor config:", output);
return;
}
log.info("Generated cursor config at", cursorPath);
reloadConfig();
});
}
}
+9 -6
View File
@@ -14,13 +14,13 @@ Singleton {
id: root id: root
readonly property var log: Log.scoped("KeybindsService") readonly property var log: Log.scoped("KeybindsService")
property bool available: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango property bool available: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango
property string currentProvider: { property string currentProvider: {
if (CompositorService.isNiri) if (CompositorService.isNiri)
return "niri"; return "niri";
if (CompositorService.isHyprland) if (CompositorService.isHyprland)
return "hyprland"; return "hyprland";
if (CompositorService.isDwl || CompositorService.isMango) if (CompositorService.isMango)
return "mangowc"; return "mangowc";
return ""; return "";
} }
@@ -30,7 +30,7 @@ Singleton {
return "niri"; return "niri";
if (CompositorService.isHyprland) if (CompositorService.isHyprland)
return "hyprland"; return "hyprland";
if (CompositorService.isDwl || CompositorService.isMango) if (CompositorService.isMango)
return "mangowc"; return "mangowc";
return ""; return "";
} }
@@ -290,13 +290,16 @@ Singleton {
configFile: mainConfigPath, configFile: mainConfigPath,
backupFile: backupPath, backupFile: backupPath,
fragmentFiles: [compositorConfigDir + "/dms/binds.lua", compositorConfigDir + "/dms/binds-user.lua"], fragmentFiles: [compositorConfigDir + "/dms/binds.lua", compositorConfigDir + "/dms/binds-user.lua"],
includes: [{ includes: [
{
grepPattern: "dms.binds", grepPattern: "dms.binds",
includeLine: "require(\"dms.binds\")" includeLine: "require(\"dms.binds\")"
}, { },
{
grepPattern: "dms.binds-user", grepPattern: "dms.binds-user",
includeLine: "require(\"dms.binds-user\")" includeLine: "require(\"dms.binds-user\")"
}] }
]
}); });
break; break;
case "mangowc": case "mangowc":
+3 -4
View File
@@ -10,9 +10,8 @@ import qs.Services
// Native MangoWM IPC client. mango advertises a JSON-over-Unix-socket protocol // Native MangoWM IPC client. mango advertises a JSON-over-Unix-socket protocol
// via MANGO_INSTANCE_SIGNATURE; each connection issues one `watch <target>` verb // via MANGO_INSTANCE_SIGNATURE; each connection issues one `watch <target>` verb
// and gets a full JSON snapshot followed by newline-delimited updates. Replaces // and gets a full JSON snapshot followed by newline-delimited updates. Exposes
// the legacy dwl-ipc-v2 path (DwlService) for mango, exposing a // a dwl-style tag API plus a per-client window list.
// DwlService-compatible tag API plus a per-client window list.
Singleton { Singleton {
id: root id: root
readonly property var log: Log.scoped("MangoService") readonly property var log: Log.scoped("MangoService")
@@ -219,7 +218,7 @@ Singleton {
root.windows = data.clients; root.windows = data.clients;
} }
// DwlService-compatible tag API // Tag API (dwl-style tag model)
function getOutputState(outputName) { function getOutputState(outputName) {
return (outputs && outputs[outputName]) ? outputs[outputName] : null; return (outputs && outputs[outputName]) ? outputs[outputName] : null;
+63
View File
@@ -966,4 +966,67 @@ Singleton {
} }
return result; return result;
} }
readonly property string _ipcIdPattern: "^[a-zA-Z0-9_\\-:]{1,64}$";
IpcHandler {
target: "plugin-scan"
function scan(): string {
root.scanPlugins();
return `SCAN_TRIGGERED: ${Object.keys(root.availablePlugins).length} known before debounce`;
}
function rescan(pluginId: string): string {
if (!pluginId)
return "ERROR: rescan requires a pluginId";
if (!new RegExp(root._ipcIdPattern).test(pluginId))
return `ERROR: invalid pluginId '${pluginId}' (allowed: [a-zA-Z0-9_\\-:]{1,64})`;
if (!(pluginId in root.availablePlugins))
return `ERROR: unknown pluginId '${pluginId}' (try 'list' first)`;
root.forceRescanPlugin(pluginId);
return `RESCAN_TRIGGERED: ${pluginId}`;
}
function reload(pluginId: string): string {
if (!pluginId)
return "ERROR: reload requires a pluginId";
if (!new RegExp(root._ipcIdPattern).test(pluginId))
return `ERROR: invalid pluginId '${pluginId}' (allowed: [a-zA-Z0-9_\\-:]{1,64})`;
if (!(pluginId in root.availablePlugins))
return `ERROR: unknown pluginId '${pluginId}'`;
root.reloadPlugin(pluginId);
return `RELOAD_TRIGGERED: ${pluginId}`;
}
function list(): string {
const ids = Object.keys(root.availablePlugins);
const cap = 256;
const n = Math.min(ids.length, cap);
const lines = [];
for (let i = 0; i < n; i++) {
const id = ids[i];
if (!new RegExp(root._ipcIdPattern).test(id))
continue;
const p = root.availablePlugins[id];
const safeName = String(p.name || "").replace(/[\t\n\r]/g, " ");
lines.push(`${id}\t${p.loaded ? "loaded" : "unloaded"}\t${p.type || "unknown"}\t${safeName}`);
}
const header = `# count=${ids.length} returned=${n}${ids.length > n ? " (truncated, see cap)" : ""}`;
return header + "\n" + lines.join("\n");
}
function status(pluginId: string): string {
if (!pluginId)
return "ERROR: status requires a pluginId";
if (!new RegExp(root._ipcIdPattern).test(pluginId))
return `ERROR: invalid pluginId '${pluginId}'`;
const plugin = root.availablePlugins[pluginId];
if (!plugin)
return `ERROR: unknown pluginId '${pluginId}'`;
const err = root.pluginLoadErrors[pluginId] || "";
const safeErr = String(err).replace(/[\t\n\r]/g, " ");
return `${plugin.loaded ? "loaded" : "unloaded"}\t${plugin.type || ""}\t${safeErr}`;
}
}
} }
-6
View File
@@ -4,7 +4,6 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Hyprland
import Quickshell.I3 import Quickshell.I3
import qs.Common import qs.Common
import qs.Services import qs.Services
@@ -314,11 +313,6 @@ Singleton {
return; return;
} }
if (CompositorService.isDwl) {
DwlService.quit();
return;
}
if (CompositorService.isMango) { if (CompositorService.isMango) {
MangoService.quit(); MangoService.quit();
return; return;
@@ -35,8 +35,10 @@ Singleton {
readonly property var conditionMap: ({ readonly property var conditionMap: ({
"isNiri": () => CompositorService.isNiri, "isNiri": () => CompositorService.isNiri,
"isHyprland": () => CompositorService.isHyprland, "isHyprland": () => CompositorService.isHyprland,
"isDwl": () => CompositorService.isDwl,
"isMango": () => CompositorService.isMango, "isMango": () => CompositorService.isMango,
"isHyprlandOrNiri": () => CompositorService.isHyprland || CompositorService.isNiri,
"windowRulesCapable": () => CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango,
"layoutCapable": () => CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango,
"keybindsAvailable": () => KeybindsService.available, "keybindsAvailable": () => KeybindsService.available,
"soundsAvailable": () => AudioService.soundsAvailable, "soundsAvailable": () => AudioService.soundsAvailable,
"cupsAvailable": () => CupsService.cupsAvailable, "cupsAvailable": () => CupsService.cupsAvailable,
+1 -1
View File
@@ -279,7 +279,7 @@ Singleton {
} }
// High-level apply matching the generateOutputsConfig() pattern used by // High-level apply matching the generateOutputsConfig() pattern used by
// NiriService, HyprlandService and DwlService. Instead of writing a // NiriService, HyprlandService and MangoService. Instead of writing a
// config file, the changes are applied directly via the // config file, the changes are applied directly via the
// wlr-output-management protocol. // wlr-output-management protocol.
function applyOutputsConfig(outputsData, connectedOutputs) { function applyOutputsConfig(outputsData, connectedOutputs) {
+1 -14
View File
@@ -1,11 +1,6 @@
#version 450 #version 450
// Connected Frame Mode silhouette as a signed-distance field: the frame ring // Connected frame silhouette: frame ring + chrome bodies as one SDF with elevation shadow.
// (an inverted rounded rectangle) smooth-unioned with each active chrome
// (popout/modal, dock, notification). The smooth-min radius IS the connector
// fillet. Antialiasing is analytic via fwidth -> crisp at any scale, no FBO.
// The elevation shadow samples the same field at the light offset, so both
// elevation states render in this one pass.
layout(location = 0) in vec2 qt_TexCoord0; layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor; layout(location = 0) out vec4 fragColor;
@@ -53,7 +48,6 @@ float sdRoundBox(vec2 p, vec2 c, vec2 hs, float r) {
return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - r; return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - r;
} }
// Per-corner rounded box. r = (topLeft, topRight, bottomRight, bottomLeft).
float sdRoundBox4(vec2 p, vec2 c, vec2 hs, vec4 r) { float sdRoundBox4(vec2 p, vec2 c, vec2 hs, vec4 r) {
p -= c; p -= c;
float rr = (p.x >= 0.0) ? (p.y >= 0.0 ? r.z : r.y) : (p.y >= 0.0 ? r.w : r.x); float rr = (p.x >= 0.0) ? (p.y >= 0.0 ? r.z : r.y) : (p.y >= 0.0 ? r.w : r.x);
@@ -62,7 +56,6 @@ float sdRoundBox4(vec2 p, vec2 c, vec2 hs, vec4 r) {
return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - rr; return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - rr;
} }
// Circular smooth-min: blends two SDFs with a fillet of radius k.
float smin(float a, float b, float k) { float smin(float a, float b, float k) {
if (k <= 0.0) if (k <= 0.0)
return min(a, b); return min(a, b);
@@ -74,14 +67,12 @@ float chromeDist(vec2 px, vec4 rect, vec4 corner) {
return sdRoundBox4(px, c, rect.zw * 0.5, corner); return sdRoundBox4(px, c, rect.zw * 0.5, corner);
} }
// Per-corner junction fillet radius, selected by chrome-rect quadrant.
float chromeK(vec2 px, vec4 rect, vec4 ks) { float chromeK(vec2 px, vec4 rect, vec4 ks) {
vec2 p = px - (rect.xy + rect.zw * 0.5); vec2 p = px - (rect.xy + rect.zw * 0.5);
return (p.x >= 0.0) ? (p.y >= 0.0 ? ks.z : ks.y) : (p.y >= 0.0 ? ks.w : ks.x); return (p.x >= 0.0) ? (p.y >= 0.0 ? ks.z : ks.y) : (p.y >= 0.0 ? ks.w : ks.x);
} }
float sceneDist(vec2 px) { float sceneDist(vec2 px) {
// Frame ring: inside the screen rect AND outside the rounded cutout (hole).
vec2 sc = vec2(ubuf.widthPx, ubuf.heightPx) * 0.5; vec2 sc = vec2(ubuf.widthPx, ubuf.heightPx) * 0.5;
float dOuter = sdBox(px, sc, sc); float dOuter = sdBox(px, sc, sc);
vec2 cutC = vec2((ubuf.cutout.x + ubuf.cutout.z) * 0.5, (ubuf.cutout.y + ubuf.cutout.w) * 0.5); vec2 cutC = vec2((ubuf.cutout.x + ubuf.cutout.z) * 0.5, (ubuf.cutout.y + ubuf.cutout.w) * 0.5);
@@ -89,7 +80,6 @@ float sceneDist(vec2 px) {
float dCut = sdRoundBox(px, cutC, cutH, ubuf.cutoutRadius); float dCut = sdRoundBox(px, cutC, cutH, ubuf.cutoutRadius);
float d = max(dOuter, -dCut); float d = max(dOuter, -dCut);
// Smooth-union the active chrome surfaces; smin radius = junction fillet.
if (ubuf.chromeParam0.x > 0.5) if (ubuf.chromeParam0.x > 0.5)
d = smin(d, chromeDist(px, ubuf.chromeRect0, ubuf.chromeCorner0), chromeK(px, ubuf.chromeRect0, ubuf.chromeK0)); d = smin(d, chromeDist(px, ubuf.chromeRect0, ubuf.chromeCorner0), chromeK(px, ubuf.chromeRect0, ubuf.chromeK0));
if (ubuf.chromeParam1.x > 0.5) if (ubuf.chromeParam1.x > 0.5)
@@ -106,14 +96,11 @@ void main() {
float d = sceneDist(px); float d = sceneDist(px);
float fw = max(fwidth(d), 1e-4); float fw = max(fwidth(d), 1e-4);
float cov = 1.0 - smoothstep(-fw, fw, d); float cov = 1.0 - smoothstep(-fw, fw, d);
// Opaque silhouette over shadow, then the surface alpha applied to the
// whole result — matches the old flattened-FBO + group-opacity look.
vec4 col = vec4(ubuf.surfaceColor.rgb, 1.0) * cov; vec4 col = vec4(ubuf.surfaceColor.rgb, 1.0) * cov;
if (ubuf.shadowColor.a > 0.0) { if (ubuf.shadowColor.a > 0.0) {
float dk = sceneDist(px - ubuf.shadowParam.zw) - ubuf.shadowParam.y; float dk = sceneDist(px - ubuf.shadowParam.zw) - ubuf.shadowParam.y;
float bk = max(ubuf.shadowParam.x, fw); float bk = max(ubuf.shadowParam.x, fw);
float covK = 1.0 - smoothstep(-bk, bk, dk); float covK = 1.0 - smoothstep(-bk, bk, dk);
// Ambient wrap reuses the field already computed for the silhouette.
float ba = max(ubuf.ambientParam.x, fw); float ba = max(ubuf.ambientParam.x, fw);
float covA = 1.0 - smoothstep(-ba, ba, d - ubuf.ambientParam.y); float covA = 1.0 - smoothstep(-ba, ba, d - ubuf.ambientParam.y);
float sh = 1.0 - (1.0 - covK * ubuf.shadowColor.a) * (1.0 - covA * ubuf.ambientParam.z); float sh = 1.0 - (1.0 - covK * ubuf.shadowColor.a) * (1.0 - covA * ubuf.ambientParam.z);
@@ -1,10 +1,6 @@
#version 450 #version 450
// Popout-local connected chrome as a signed-distance field: the body rounded // Popout-local connected chrome body + bar-edge connector as one SDF.
// rect smooth-unioned against the bar-edge half-plane, so the connector
// fillets form analytically — the SDF twin of the old ConnectedShape +
// ConnectedCorner + MultiEffect FBO. Key + ambient shadows sample the same
// field; shadow is masked outside the silhouette.
layout(location = 0) in vec2 qt_TexCoord0; layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor; layout(location = 0) out vec4 fragColor;
@@ -23,7 +19,6 @@ layout(std140, binding = 0) uniform buf {
vec4 edgeParam; // x = bar side (0 top, 1 bottom, 2 left, 3 right), y = fillet k vec4 edgeParam; // x = bar side (0 top, 1 bottom, 2 left, 3 right), y = fillet k
} ubuf; } ubuf;
// Per-corner rounded box. r = (topLeft, topRight, bottomRight, bottomLeft).
float sdRoundBox4(vec2 p, vec2 c, vec2 hs, vec4 r) { float sdRoundBox4(vec2 p, vec2 c, vec2 hs, vec4 r) {
p -= c; p -= c;
float rr = (p.x >= 0.0) ? (p.y >= 0.0 ? r.z : r.y) : (p.y >= 0.0 ? r.w : r.x); float rr = (p.x >= 0.0) ? (p.y >= 0.0 ? r.z : r.y) : (p.y >= 0.0 ? r.w : r.x);
@@ -32,7 +27,6 @@ float sdRoundBox4(vec2 p, vec2 c, vec2 hs, vec4 r) {
return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - rr; return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - rr;
} }
// Circular smooth-min: blends two SDFs with a fillet of radius k.
float smin(float a, float b, float k) { float smin(float a, float b, float k) {
if (k <= 0.0) if (k <= 0.0)
return min(a, b); return min(a, b);
@@ -40,8 +34,6 @@ float smin(float a, float b, float k) {
} }
float sceneDist(vec2 px) { float sceneDist(vec2 px) {
// Bar edge as a half-plane whose interior lies off-item, on the bar's
// side; only its fillet contribution is visible.
float side = ubuf.edgeParam.x; float side = ubuf.edgeParam.x;
float dEdge = side < 0.5 ? px.y float dEdge = side < 0.5 ? px.y
: side < 1.5 ? (ubuf.heightPx - px.y) : side < 1.5 ? (ubuf.heightPx - px.y)
+1 -7
View File
@@ -1,10 +1,6 @@
#version 450 #version 450
// Standalone elevation surface as a signed-distance field: one quad draws the // Standalone rounded rect with border and M3 elevation shadow as one SDF.
// rounded-rect fill, its border, and the M3 two-part shadow (directional key +
// non-directional ambient) analytically — no FBO, no blur passes. The shadow
// is masked to outside the silhouette, so translucent fills never get
// interior darkening.
layout(location = 0) in vec2 qt_TexCoord0; layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor; layout(location = 0) out vec4 fragColor;
@@ -24,7 +20,6 @@ layout(std140, binding = 0) uniform buf {
vec4 ambientParam; // ambient: x = blur px, y = spread px, z = alpha vec4 ambientParam; // ambient: x = blur px, y = spread px, z = alpha
} ubuf; } ubuf;
// Per-corner rounded box. r = (topLeft, topRight, bottomRight, bottomLeft).
float sdRoundBox4(vec2 p, vec2 c, vec2 hs, vec4 r) { float sdRoundBox4(vec2 p, vec2 c, vec2 hs, vec4 r) {
p -= c; p -= c;
float rr = (p.x >= 0.0) ? (p.y >= 0.0 ? r.z : r.y) : (p.y >= 0.0 ? r.w : r.x); float rr = (p.x >= 0.0) ? (p.y >= 0.0 ? r.z : r.y) : (p.y >= 0.0 ? r.w : r.x);
@@ -43,7 +38,6 @@ void main() {
float d = rectDist(px); float d = rectDist(px);
float fw = max(fwidth(d), 1e-4); float fw = max(fwidth(d), 1e-4);
float cov = 1.0 - smoothstep(-fw, fw, d); float cov = 1.0 - smoothstep(-fw, fw, d);
// Qt Rectangle semantics: border band on the rim, fill inset inside it.
float covInner = 1.0 - smoothstep(-fw, fw, d + ubuf.borderWidth); float covInner = 1.0 - smoothstep(-fw, fw, d + ubuf.borderWidth);
vec4 col = vec4(ubuf.fillColor.rgb, 1.0) * (ubuf.fillColor.a * covInner) vec4 col = vec4(ubuf.fillColor.rgb, 1.0) * (ubuf.fillColor.a * covInner)
+ vec4(ubuf.borderColor.rgb, 1.0) * (ubuf.borderColor.a * max(0.0, cov - covInner)); + vec4(ubuf.borderColor.rgb, 1.0) * (ubuf.borderColor.a * max(0.0, cov - covInner));
+1 -3
View File
@@ -1,8 +1,6 @@
#version 450 #version 450
// Frame perimeter ring as a signed-distance field: the window rectangle minus // Frame perimeter ring with rounded cutout as one SDF.
// a rounded-rectangle cutout. Antialiasing is analytic via fwidth -> crisp at
// any scale, no FBO and no mask textures.
layout(location = 0) in vec2 qt_TexCoord0; layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor; layout(location = 0) out vec4 fragColor;
-151
View File
@@ -1,151 +0,0 @@
import QtQuick
import QtQuick.Shapes
import "../Common/ConnectorGeometry.js" as ConnectorGeometry
// Concave arc connector filling the gap between a bar corner and an adjacent surface.
//
// NOTE: FrameWindow now uses ConnectedShape.qml for frame-owned connected chrome
// (unified single-path rendering). This component is still used by DankPopout's
// own shadow source for non-frame-owned chrome (popouts on non-frame screens).
Item {
id: root
property string barSide: "top"
property string placement: "left"
property real spacing: 4
property real connectorRadius: 12
property color color: "transparent"
property real edgeStrokeWidth: 0
property color edgeStrokeColor: color
property real dpr: 1
readonly property bool isHorizontalBar: barSide === "top" || barSide === "bottom"
readonly property bool isPlacementLeft: placement === "left"
readonly property real _edgeStrokeWidth: Math.max(0, edgeStrokeWidth)
readonly property string arcCorner: ConnectorGeometry.arcCorner(barSide, placement)
readonly property real pathStartX: {
switch (arcCorner) {
case "topLeft":
return width;
case "topRight":
case "bottomLeft":
return 0;
default:
return 0;
}
}
readonly property real pathStartY: {
switch (arcCorner) {
case "bottomRight":
return height;
default:
return 0;
}
}
readonly property real firstLineX: {
switch (arcCorner) {
case "topLeft":
case "bottomLeft":
return width;
default:
return 0;
}
}
readonly property real firstLineY: {
switch (arcCorner) {
case "topLeft":
case "topRight":
return height;
default:
return 0;
}
}
readonly property real secondLineX: {
switch (arcCorner) {
case "topRight":
case "bottomLeft":
case "bottomRight":
return width;
default:
return 0;
}
}
readonly property real secondLineY: {
switch (arcCorner) {
case "topLeft":
case "topRight":
case "bottomLeft":
return height;
default:
return 0;
}
}
readonly property real arcCenterX: arcCorner === "topRight" || arcCorner === "bottomRight" ? width : 0
readonly property real arcCenterY: arcCorner === "bottomLeft" || arcCorner === "bottomRight" ? height : 0
readonly property real arcStartAngle: {
switch (arcCorner) {
case "topLeft":
case "topRight":
return 90;
case "bottomLeft":
return 0;
default:
return -90;
}
}
readonly property real arcSweepAngle: {
switch (arcCorner) {
case "topRight":
return 90;
default:
return -90;
}
}
width: isHorizontalBar ? connectorRadius : (spacing + connectorRadius)
height: isHorizontalBar ? (spacing + connectorRadius) : connectorRadius
Shape {
x: -root._edgeStrokeWidth
y: -root._edgeStrokeWidth
width: root.width + root._edgeStrokeWidth * 2
height: root.height + root._edgeStrokeWidth * 2
asynchronous: false
antialiasing: true
preferredRendererType: Shape.CurveRenderer
layer.enabled: true
layer.smooth: true
layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0)
ShapePath {
fillColor: root.color
strokeColor: root._edgeStrokeWidth > 0 ? root.edgeStrokeColor : "transparent"
strokeWidth: root._edgeStrokeWidth * 2
joinStyle: ShapePath.RoundJoin
capStyle: ShapePath.RoundCap
fillRule: ShapePath.WindingFill
startX: root.pathStartX + root._edgeStrokeWidth
startY: root.pathStartY + root._edgeStrokeWidth
PathLine {
x: root.firstLineX + root._edgeStrokeWidth
y: root.firstLineY + root._edgeStrokeWidth
}
PathLine {
x: root.secondLineX + root._edgeStrokeWidth
y: root.secondLineY + root._edgeStrokeWidth
}
PathAngleArc {
centerX: root.arcCenterX + root._edgeStrokeWidth
centerY: root.arcCenterY + root._edgeStrokeWidth
radiusX: root.connectorRadius
radiusY: root.connectorRadius
startAngle: root.arcStartAngle
sweepAngle: root.arcSweepAngle
}
}
}
}
-414
View File
@@ -1,414 +0,0 @@
import QtQuick
import QtQuick.Shapes
import qs.Common
// Unified connected silhouette: body + near/far concave arcs as one ShapePath.
// Keeping the connected chrome in one path avoids sibling alignment seams.
Item {
id: root
property string barSide: "top"
property real bodyWidth: 0
property real bodyHeight: 0
property real connectorRadius: 12
property real startConnectorRadius: connectorRadius
property real endConnectorRadius: connectorRadius
property real farStartConnectorRadius: 0
property real farEndConnectorRadius: 0
property real surfaceRadius: 12
property color fillColor: "transparent"
readonly property bool _horiz: barSide === "top" || barSide === "bottom"
readonly property real _sc: Math.max(0, startConnectorRadius)
readonly property real _ec: Math.max(0, endConnectorRadius)
readonly property real _fsc: Math.max(0, farStartConnectorRadius)
readonly property real _fec: Math.max(0, farEndConnectorRadius)
readonly property real _firstCr: barSide === "left" ? _sc : _ec
readonly property real _secondCr: barSide === "left" ? _ec : _sc
readonly property real _firstFarCr: barSide === "left" ? _fsc : _fec
readonly property real _secondFarCr: barSide === "left" ? _fec : _fsc
readonly property real _farExtent: Math.max(_fsc, _fec)
readonly property real _sr: Math.max(0, Math.min(surfaceRadius, (_horiz ? bodyWidth : bodyHeight) / 2, (_horiz ? bodyHeight : bodyWidth) / 2))
readonly property real _firstSr: _firstFarCr > 0 ? 0 : _sr
readonly property real _secondSr: _secondFarCr > 0 ? 0 : _sr
readonly property real _firstFarInset: _firstFarCr > 0 ? _firstFarCr : _firstSr
readonly property real _secondFarInset: _secondFarCr > 0 ? _secondFarCr : _secondSr
// Root-level aliases PathArc/PathLine elements can't use `parent`.
readonly property real _bw: bodyWidth
readonly property real _bh: bodyHeight
readonly property real _bodyLeft: _horiz ? _sc : (barSide === "right" ? _farExtent : 0)
readonly property real _bodyRight: _bodyLeft + _bw
readonly property real _bodyTop: _horiz ? (barSide === "bottom" ? _farExtent : 0) : _sc
readonly property real _bodyBottom: _bodyTop + _bh
readonly property real _totalW: _horiz ? _bw + _sc + _ec : _bw + _farExtent
readonly property real _totalH: _horiz ? _bh + _farExtent : _bh + _sc + _ec
width: _totalW
height: _totalH
readonly property real bodyX: root._bodyLeft
readonly property real bodyY: root._bodyTop
Shape {
anchors.fill: parent
asynchronous: false
preferredRendererType: Shape.CurveRenderer
antialiasing: true
ShapePath {
fillColor: root.fillColor
strokeWidth: -1
fillRule: ShapePath.WindingFill
// CW path: bar edge concave arc body convex arc far edge convex arc body concave arc
startX: root.barSide === "right" ? root._totalW : 0
startY: {
switch (root.barSide) {
case "bottom":
return root._totalH;
case "left":
return root._totalH;
case "right":
return 0;
default:
return 0;
}
}
// Bar edge
PathLine {
x: {
switch (root.barSide) {
case "left":
return 0;
case "right":
return root._totalW;
default:
return root._totalW;
}
}
y: {
switch (root.barSide) {
case "bottom":
return root._totalH;
case "left":
return 0;
case "right":
return root._totalH;
default:
return 0;
}
}
}
// Concave arc 1
PathArc {
relativeX: {
switch (root.barSide) {
case "left":
return root._firstCr;
case "right":
return -root._firstCr;
default:
return -root._firstCr;
}
}
relativeY: {
switch (root.barSide) {
case "bottom":
return -root._firstCr;
case "left":
return root._firstCr;
case "right":
return -root._firstCr;
default:
return root._firstCr;
}
}
radiusX: root._firstCr
radiusY: root._firstCr
direction: root.barSide === "bottom" ? PathArc.Clockwise : PathArc.Counterclockwise
}
// Body edge to first convex corner
PathLine {
x: {
switch (root.barSide) {
case "left":
return root._bodyRight - root._firstSr;
case "right":
return root._bodyLeft + root._firstSr;
default:
return root._bodyRight;
}
}
y: {
switch (root.barSide) {
case "bottom":
return root._bodyTop + root._firstSr;
case "left":
return root._bodyTop;
case "right":
return root._bodyBottom;
default:
return root._bodyBottom - root._firstSr;
}
}
}
// Convex arc 1
PathArc {
relativeX: {
switch (root.barSide) {
case "left":
return root._firstSr;
case "right":
return -root._firstSr;
default:
return -root._firstSr;
}
}
relativeY: {
switch (root.barSide) {
case "bottom":
return -root._firstSr;
case "left":
return root._firstSr;
case "right":
return -root._firstSr;
default:
return root._firstSr;
}
}
radiusX: root._firstSr
radiusY: root._firstSr
direction: root.barSide === "bottom" ? PathArc.Counterclockwise : PathArc.Clockwise
}
// Opposite-side connector 1
PathLine {
x: {
switch (root.barSide) {
case "left":
return root._firstFarCr > 0 ? root._bodyRight + root._firstFarCr : root._bodyRight;
case "right":
return root._firstFarCr > 0 ? root._bodyLeft - root._firstFarCr : root._bodyLeft;
default:
return root._firstFarCr > 0 ? root._bodyRight : root._bodyRight - root._firstSr;
}
}
y: {
switch (root.barSide) {
case "bottom":
return root._firstFarCr > 0 ? root._bodyTop - root._firstFarCr : root._bodyTop;
case "left":
return root._firstFarCr > 0 ? root._bodyTop : root._bodyTop + root._firstSr;
case "right":
return root._firstFarCr > 0 ? root._bodyBottom : root._bodyBottom - root._firstSr;
default:
return root._firstFarCr > 0 ? root._bodyBottom + root._firstFarCr : root._bodyBottom;
}
}
}
PathArc {
relativeX: {
switch (root.barSide) {
case "left":
return -root._firstFarCr;
case "right":
return root._firstFarCr;
default:
return -root._firstFarCr;
}
}
relativeY: {
switch (root.barSide) {
case "bottom":
return root._firstFarCr;
case "left":
return root._firstFarCr;
case "right":
return -root._firstFarCr;
default:
return -root._firstFarCr;
}
}
radiusX: root._firstFarCr
radiusY: root._firstFarCr
direction: root.barSide === "bottom" ? PathArc.Clockwise : PathArc.Counterclockwise
}
// Far edge
PathLine {
x: {
switch (root.barSide) {
case "left":
return root._bodyRight;
case "right":
return root._bodyLeft;
default:
return root._bodyLeft + root._secondFarInset;
}
}
y: {
switch (root.barSide) {
case "bottom":
return root._bodyTop;
case "left":
return root._bodyBottom - root._secondFarInset;
case "right":
return root._bodyTop + root._secondFarInset;
default:
return root._bodyBottom;
}
}
}
// Opposite-side connector 2
PathArc {
relativeX: {
switch (root.barSide) {
case "left":
return root._secondFarCr;
case "right":
return -root._secondFarCr;
default:
return -root._secondFarCr;
}
}
relativeY: {
switch (root.barSide) {
case "bottom":
return -root._secondFarCr;
case "left":
return root._secondFarCr;
case "right":
return -root._secondFarCr;
default:
return root._secondFarCr;
}
}
radiusX: root._secondFarCr
radiusY: root._secondFarCr
direction: root.barSide === "bottom" ? PathArc.Clockwise : PathArc.Counterclockwise
}
PathLine {
x: {
switch (root.barSide) {
case "left":
return root._secondFarCr > 0 ? root._bodyRight : root._bodyRight;
case "right":
return root._secondFarCr > 0 ? root._bodyLeft : root._bodyLeft;
default:
return root._secondFarCr > 0 ? root._bodyLeft : root._bodyLeft + root._secondSr;
}
}
y: {
switch (root.barSide) {
case "bottom":
return root._secondFarCr > 0 ? root._bodyTop : root._bodyTop;
case "left":
return root._secondFarCr > 0 ? root._bodyBottom : root._bodyBottom - root._secondSr;
case "right":
return root._secondFarCr > 0 ? root._bodyTop : root._bodyTop + root._secondSr;
default:
return root._secondFarCr > 0 ? root._bodyBottom : root._bodyBottom;
}
}
}
// Convex arc 2
PathArc {
relativeX: {
switch (root.barSide) {
case "left":
return -root._secondSr;
case "right":
return root._secondSr;
default:
return -root._secondSr;
}
}
relativeY: {
switch (root.barSide) {
case "bottom":
return root._secondSr;
case "left":
return root._secondSr;
case "right":
return -root._secondSr;
default:
return -root._secondSr;
}
}
radiusX: root._secondSr
radiusY: root._secondSr
direction: root.barSide === "bottom" ? PathArc.Counterclockwise : PathArc.Clockwise
}
// Body edge to second concave arc
PathLine {
x: {
switch (root.barSide) {
case "left":
return root._bodyLeft + root._ec;
case "right":
return root._bodyRight - root._sc;
default:
return root._bodyLeft;
}
}
y: {
switch (root.barSide) {
case "bottom":
return root._bodyBottom - root._sc;
case "left":
return root._bodyBottom;
case "right":
return root._bodyTop;
default:
return root._bodyTop + root._sc;
}
}
}
// Concave arc 2
PathArc {
relativeX: {
switch (root.barSide) {
case "left":
return -root._secondCr;
case "right":
return root._secondCr;
default:
return -root._secondCr;
}
}
relativeY: {
switch (root.barSide) {
case "bottom":
return root._secondCr;
case "left":
return root._secondCr;
case "right":
return -root._secondCr;
default:
return -root._secondCr;
}
}
radiusX: root._secondCr
radiusY: root._secondCr
direction: root.barSide === "bottom" ? PathArc.Clockwise : PathArc.Counterclockwise
}
}
}
}
+1 -7
View File
@@ -53,11 +53,7 @@ Item {
readonly property var backgroundWindow: impl.item ? impl.item.backgroundWindow : null readonly property var backgroundWindow: impl.item ? impl.item.backgroundWindow : null
readonly property var contentWindow: impl.item ? impl.item.contentWindow : null readonly property var contentWindow: impl.item ? impl.item.contentWindow : null
// On Hyprland the OnDemand content surface only receives keyboard focus // Hyprland OnDemand grab: whitelist popout surfaces and bars so dismiss clicks still land.
// through a grab; everywhere else Exclusive focus covers this. Both
// popout windows plus every bar are whitelisted so clicks on them are
// delivered normally (dismiss MouseAreas, widget-to-widget transfer)
// instead of being consumed clearing the grab.
HyprlandFocusGrab { HyprlandFocusGrab {
windows: { windows: {
const list = []; const list = [];
@@ -146,8 +142,6 @@ Item {
return _usesConnectedBackendForScreen(targetScreen) ? connectedComp : standaloneComp; return _usesConnectedBackendForScreen(targetScreen) ? connectedComp : standaloneComp;
} }
// Defer Loader source-component swap until impl is fully closed; avoids
// tearing down a popout mid-animation when frame mode is toggled.
function _maybeResolveBackend() { function _maybeResolveBackend() {
_resolveBackendForScreen(screen); _resolveBackendForScreen(screen);
} }
+3 -35
View File
@@ -53,7 +53,6 @@ Item {
"rightBar": 0 "rightBar": 0
}) })
property var screen: null property var screen: null
// Connected resize uses one full-screen surface; body-sized regions are masks.
readonly property bool useBackgroundWindow: false readonly property bool useBackgroundWindow: false
readonly property var effectivePopoutLayer: LayerShell.fromEnv("DMS_POPOUT_LAYER", root.triggerUsesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top, { readonly property var effectivePopoutLayer: LayerShell.fromEnv("DMS_POPOUT_LAYER", root.triggerUsesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top, {
"allow": ["top", "overlay"], "allow": ["top", "overlay"],
@@ -91,7 +90,6 @@ Item {
signal popoutClosed signal popoutClosed
signal backgroundClicked signal backgroundClicked
// Coalesce per-channel dirty bits; one ConnectedModeState write per tick.
Timer { Timer {
id: _syncTimer id: _syncTimer
interval: 0 interval: 0
@@ -278,11 +276,9 @@ Item {
chromeLease.release(); chromeLease.release();
} }
// Exposed animation state for ConnectedModeState
readonly property real contentAnimX: contentContainer.animX readonly property real contentAnimX: contentContainer.animX
readonly property real contentAnimY: contentContainer.animY readonly property real contentAnimY: contentContainer.animY
// ConnectedModeState sync
function _syncPopoutChromeState() { function _syncPopoutChromeState() {
if (!root.frameOwnsConnectedChrome) { if (!root.frameOwnsConnectedChrome) {
_releaseConnectedChromeState(); _releaseConnectedChromeState();
@@ -422,16 +418,12 @@ Item {
const screenChanged = _lastOpenedScreen !== null && _lastOpenedScreen !== screen; const screenChanged = _lastOpenedScreen !== null && _lastOpenedScreen !== screen;
if (screenChanged) { if (screenChanged) {
// Hide on this tick so Qt actually tears down the wl_surface; the show
// gets deferred below so the unmap is processed before the remap.
contentWindow.visible = false; contentWindow.visible = false;
} }
_lastOpenedScreen = screen; _lastOpenedScreen = screen;
PopoutManager.showPopout(popoutHandle); PopoutManager.showPopout(popoutHandle);
if (contentContainer) { if (contentContainer) {
// Snap morph closed only on a fresh open; on screen-change re-open we stay at 1
// because shouldBeVisible doesn't change and won't drive morph back to 1.
if (!shouldBeVisible) if (!shouldBeVisible)
morph.openProgress = 0; morph.openProgress = 0;
_captureChromeAnimTravel(); _captureChromeAnimTravel();
@@ -445,11 +437,7 @@ Item {
} }
if (screenChanged) { if (screenChanged) {
// Defer the show one event-loop tick. Qt coalesces a synchronous // Unmap/remap wl_surface across ticks so blur republishes on the new screen.
// falsetrue visibility flip into a no-op, leaving WindowBlur committed
// to the previous screen's wl_surface. Splitting the flip across ticks
// forces a real surface destroy+create so BackgroundEffect.surfaceCreated
// fires and the blur region republishes on the new surface.
Qt.callLater(() => { Qt.callLater(() => {
if (!root.shouldBeVisible) if (!root.shouldBeVisible)
return; return;
@@ -568,9 +556,7 @@ Item {
return Math.abs(value - bound) <= Math.max(1, Theme.hairline(root.dpr) * 2); return Math.abs(value - bound) <= Math.max(1, Theme.hairline(root.dpr) * 2);
} }
// Snap a frame-perpendicular position flush to the frame bound when it // Snap positions within connector radius flush to the frame edge (avoids pinched arcs).
// lands within the connector radius: a gap smaller than the radius cannot
// form the close-gap arc and renders as a pinched wedge instead.
function _snapNearFrameBound(value, minBound, maxBound, minIsFrame, maxIsFrame) { function _snapNearFrameBound(value, minBound, maxBound, minIsFrame, maxIsFrame) {
if (!root.usesConnectedSurfaceChrome || !root.closeFrameGapsActive) if (!root.usesConnectedSurfaceChrome || !root.closeFrameGapsActive)
return value; return value;
@@ -637,7 +623,6 @@ Item {
property real renderedAlignedY: alignedY property real renderedAlignedY: alignedY
property real renderedAlignedHeight: alignedHeight property real renderedAlignedHeight: alignedHeight
readonly property bool renderedGeometryGrowing: alignedHeight >= renderedAlignedHeight readonly property bool renderedGeometryGrowing: alignedHeight >= renderedAlignedHeight
// Snap rendered geometry while the entrance morph runs so it doesn't ride a second animation (side-bar ramp).
readonly property bool _settlingToOpen: fullHeightSurface && shouldBeVisible && morphAnim.running readonly property bool _settlingToOpen: fullHeightSurface && shouldBeVisible && morphAnim.running
Behavior on renderedAlignedY { Behavior on renderedAlignedY {
@@ -687,8 +672,6 @@ Item {
return 0; return 0;
if (!root.usesConnectedSurfaceChrome) if (!root.usesConnectedSurfaceChrome)
return exclusion; return exclusion;
// In a shared frame corner, the adjacent connected bar already occupies
// one rounded-corner radius before the popout's own connector begins.
return exclusion + Theme.connectedCornerRadius * 2; return exclusion + Theme.connectedCornerRadius * 2;
} }
@@ -721,10 +704,8 @@ Item {
switch (effectiveBarPosition) { switch (effectiveBarPosition) {
case SettingsData.Position.Left: case SettingsData.Position.Left:
// bar on left: left side is bar-adjacent (popupGap), right side is frame-perpendicular (edgeGap)
return Math.max(popupGap, Math.min(screenWidth - popupWidth - edgeGapRight, anchorX)); return Math.max(popupGap, Math.min(screenWidth - popupWidth - edgeGapRight, anchorX));
case SettingsData.Position.Right: case SettingsData.Position.Right:
// bar on right: right side is bar-adjacent (popupGap), left side is frame-perpendicular (edgeGap)
return Math.max(edgeGapLeft, Math.min(screenWidth - popupWidth - popupGap, anchorX - popupWidth)); return Math.max(edgeGapLeft, Math.min(screenWidth - popupWidth - popupGap, anchorX - popupWidth));
default: default:
const rawX = triggerX + (triggerWidth / 2) - (popupWidth / 2); const rawX = triggerX + (triggerWidth / 2) - (popupWidth / 2);
@@ -744,10 +725,8 @@ Item {
switch (effectiveBarPosition) { switch (effectiveBarPosition) {
case SettingsData.Position.Bottom: case SettingsData.Position.Bottom:
// bar on bottom: bottom side is bar-adjacent (popupGap), top side is frame-perpendicular (edgeGap)
return Math.max(edgeGapTop, Math.min(screenHeight - popupHeight - popupGap, anchorY - popupHeight)); return Math.max(edgeGapTop, Math.min(screenHeight - popupHeight - popupGap, anchorY - popupHeight));
case SettingsData.Position.Top: case SettingsData.Position.Top:
// bar on top: top side is bar-adjacent (popupGap), bottom side is frame-perpendicular (edgeGap)
return Math.max(popupGap, Math.min(screenHeight - popupHeight - edgeGapBottom, anchorY)); return Math.max(popupGap, Math.min(screenHeight - popupHeight - edgeGapBottom, anchorY));
default: default:
const rawY = triggerY - (popupHeight / 2); const rawY = triggerY - (popupHeight / 2);
@@ -788,10 +767,8 @@ Item {
blurEnabled: root.effectiveSurfaceBlurEnabled && !root.frameOwnsConnectedChrome blurEnabled: root.effectiveSurfaceBlurEnabled && !root.frameOwnsConnectedChrome
readonly property real s: Math.min(1, contentContainer.scaleValue) readonly property real s: Math.min(1, contentContainer.scaleValue)
readonly property bool trackBlurFromBarEdge: root.usesConnectedSurfaceChrome || Theme.isDirectionalEffect readonly property bool trackBlurFromBarEdge: root.usesConnectedSurfaceChrome
// Directional popouts clip to the bar edge, so the blur needs to grow from
// that same edge instead of translating through the bar before settling.
readonly property real _dyClamp: (contentContainer.barTop || contentContainer.barBottom) ? Math.max(-contentContainer.height, Math.min(contentContainer.animY, contentContainer.height)) : 0 readonly property real _dyClamp: (contentContainer.barTop || contentContainer.barBottom) ? Math.max(-contentContainer.height, Math.min(contentContainer.animY, contentContainer.height)) : 0
readonly property real _dxClamp: (contentContainer.barLeft || contentContainer.barRight) ? Math.max(-contentContainer.width, Math.min(contentContainer.animX, contentContainer.width)) : 0 readonly property real _dxClamp: (contentContainer.barLeft || contentContainer.barRight) ? Math.max(-contentContainer.width, Math.min(contentContainer.animX, contentContainer.width)) : 0
@@ -827,7 +804,6 @@ Item {
Region { Region {
id: contentInputMask id: contentInputMask
// Use bar-aware mask so bar widget clicks pass through when a popout is open.
item: (shouldBeVisible && backgroundInteractive) ? backgroundDismissalMask : contentMaskRect item: (shouldBeVisible && backgroundInteractive) ? backgroundDismissalMask : contentMaskRect
} }
@@ -938,7 +914,6 @@ Item {
readonly property real computedScaleCollapsed: root.animationScaleCollapsed readonly property real computedScaleCollapsed: root.animationScaleCollapsed
// openProgress: 0 = closed (at offset, scaleCollapsed), 1 = open (at 0, scale 1).
QtObject { QtObject {
id: morph id: morph
property real openProgress: 0 property real openProgress: 0
@@ -985,7 +960,6 @@ Item {
clip: shouldClip clip: shouldClip
// Bound the clipping strictly to the bar side, allowing massive overflow on the other 3 sides for shadows
x: shouldClip ? (contentContainer.barLeft ? -connectedClipAllowance : -clipOversize) : 0 x: shouldClip ? (contentContainer.barLeft ? -connectedClipAllowance : -clipOversize) : 0
y: shouldClip ? (contentContainer.barTop ? -connectedClipAllowance : -clipOversize) : 0 y: shouldClip ? (contentContainer.barTop ? -connectedClipAllowance : -clipOversize) : 0
@@ -1043,8 +1017,6 @@ Item {
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !(root.suspendShadowWhileResizing && root._resizeActive) && !root.frameOwnsConnectedChrome shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !(root.suspendShadowWhileResizing && root._resizeActive) && !root.frameOwnsConnectedChrome
} }
// Local connected chrome (body + connector fillets joined to
// the bar edge) and its shadow as one SDF quad no FBO.
Item { Item {
id: localChrome id: localChrome
visible: root.usesLocalConnectedSurfaceChrome visible: root.usesLocalConnectedSurfaceChrome
@@ -1070,8 +1042,6 @@ Item {
ShaderEffect { ShaderEffect {
anchors.fill: parent anchors.fill: parent
// Shadow overflow pads every side except the bar
// edge, where the silhouette must end flush.
anchors.topMargin: contentContainer.barTop ? 0 : -localChrome.pad anchors.topMargin: contentContainer.barTop ? 0 : -localChrome.pad
anchors.bottomMargin: contentContainer.barBottom ? 0 : -localChrome.pad anchors.bottomMargin: contentContainer.barBottom ? 0 : -localChrome.pad
anchors.leftMargin: contentContainer.barLeft ? 0 : -localChrome.pad anchors.leftMargin: contentContainer.barLeft ? 0 : -localChrome.pad
@@ -1144,8 +1114,6 @@ Item {
Connections { Connections {
target: contentWindow target: contentWindow
function onVisibleChanged() { function onVisibleChanged() {
// open() flips contentWindow.visible to rebind the layer surface to
// a new screen; don't deactivate the wrapper while still open.
if (!contentWindow.visible && !root.shouldBeVisible) if (!contentWindow.visible && !root.shouldBeVisible)
contentWrapper._renderActive = false; contentWrapper._renderActive = false;
} }
+12 -6
View File
@@ -589,15 +589,19 @@ Item {
id: popoutBlur id: popoutBlur
targetWindow: contentWindow targetWindow: contentWindow
readonly property real s: Math.min(1, contentContainer.scaleValue) readonly property real s: Math.min(1, contentContainer.scaleValue)
readonly property bool trackBlurFromBarEdge: root.fluidStandaloneActive
readonly property real op: Math.max(0, Math.min(1, (morph.openProgress - 0.08) * 1.6)) readonly property real op: Math.max(0, Math.min(1, (morph.openProgress - 0.08) * 1.6))
readonly property bool blurAlive: trackBlurFromBarEdge ? (contentContainer.revealWidth > 0 && contentContainer.revealHeight > 0) : root.shouldBeVisible readonly property bool revealClipActive: root.fluidStandaloneActive
blurX: trackBlurFromBarEdge ? contentContainer.x + contentContainer.revealX : contentContainer.x + contentContainer.width * (1 - s * op) * 0.5 + Theme.snap(contentContainer.animX, root.dpr) blurX: revealClipActive ? contentContainer.x : contentContainer.x + contentContainer.width * (1 - s * op) * 0.5 + Theme.snap(contentContainer.animX, root.dpr)
blurY: trackBlurFromBarEdge ? contentContainer.y + contentContainer.revealY : contentContainer.y + contentContainer.height * (1 - s * op) * 0.5 + Theme.snap(contentContainer.animY, root.dpr) blurY: revealClipActive ? contentContainer.y : contentContainer.y + contentContainer.height * (1 - s * op) * 0.5 + Theme.snap(contentContainer.animY, root.dpr)
blurWidth: blurAlive ? (trackBlurFromBarEdge ? contentContainer.revealWidth : contentContainer.width * s * op) : 0 blurWidth: root.shouldBeVisible ? (revealClipActive ? contentContainer.width : contentContainer.width * s * op) : 0
blurHeight: blurAlive ? (trackBlurFromBarEdge ? contentContainer.revealHeight : contentContainer.height * s * op) : 0 blurHeight: root.shouldBeVisible ? (revealClipActive ? contentContainer.height : contentContainer.height * s * op) : 0
blurRadius: Theme.cornerRadius blurRadius: Theme.cornerRadius
clipEnabled: revealClipActive
clipX: contentContainer.x + contentContainer.revealX
clipY: contentContainer.y + contentContainer.revealY
clipWidth: root.shouldBeVisible ? contentContainer.revealWidth : 0
clipHeight: root.shouldBeVisible ? contentContainer.revealHeight : 0
} }
WlrLayershell.namespace: root.layerNamespace WlrLayershell.namespace: root.layerNamespace
@@ -702,6 +706,8 @@ Item {
QtObject { QtObject {
id: morph id: morph
property real openProgress: 0 property real openProgress: 0
onOpenProgressChanged: if (root.fluidStandaloneActive)
root._kickBlurCommit()
Behavior on openProgress { Behavior on openProgress {
enabled: root.animationsEnabled enabled: root.animationsEnabled
NumberAnimation { NumberAnimation {
+108
View File
@@ -0,0 +1,108 @@
import QtQuick
import QtQuick.Shapes
import qs.Common
Item {
id: root
property real size: 48
property real strokeWidth: Math.max(2, size / 12)
property color color: Theme.primary
property bool running: visible
implicitWidth: size
implicitHeight: size
onRunningChanged: {
if (running)
return;
arc.rotation = 0;
arc.startAngle = 0;
arc.sweepAngle = 16;
}
Item {
id: rotator
anchors.fill: parent
RotationAnimator on rotation {
from: 0
to: 360
duration: 1568
loops: Animation.Infinite
running: root.running
}
Shape {
id: arc
property real startAngle: 0
property real sweepAngle: 16
anchors.fill: parent
antialiasing: true
preferredRendererType: Shape.CurveRenderer
ShapePath {
strokeColor: root.color
strokeWidth: root.strokeWidth
fillColor: "transparent"
capStyle: ShapePath.RoundCap
PathAngleArc {
centerX: arc.width / 2
centerY: arc.height / 2
radiusX: Math.max(1, (Math.min(arc.width, arc.height) - root.strokeWidth) / 2)
radiusY: radiusX
startAngle: arc.startAngle
sweepAngle: arc.sweepAngle
}
}
}
SequentialAnimation {
running: root.running
loops: Animation.Infinite
NumberAnimation {
target: arc
property: "sweepAngle"
from: 16
to: 270
duration: 666
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
ParallelAnimation {
NumberAnimation {
target: arc
property: "startAngle"
from: 0
to: 254
duration: 666
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
NumberAnimation {
target: arc
property: "sweepAngle"
from: 270
to: 16
duration: 666
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
ScriptAction {
script: {
arc.rotation = (arc.rotation + 254) % 360;
arc.startAngle = 0;
}
}
}
}
}
-1
View File
@@ -1,7 +1,6 @@
import QtQuick import QtQuick
import qs.Common import qs.Common
// Defines the screen area (excluding bars) that dismisses popouts
QtObject { QtObject {
id: root id: root

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