mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-15 23:55:21 -04:00
Compare commits
19 Commits
cc8d68d753
...
hover
| Author | SHA1 | Date | |
|---|---|---|---|
| fc72b6d779 | |||
| 3701b3d7a3 | |||
| bae98daa5c | |||
| b34a04f723 | |||
| 1c0245f2db | |||
| 7777e87dc8 | |||
| 820fa07846 | |||
| 66794582c9 | |||
| 73eb471ae3 | |||
| 0f2f4b96c4 | |||
| d53809cf2b | |||
| 08fd6e26d8 | |||
| 29e8470f2e | |||
| 573785d4ce | |||
| 5483303714 | |||
| 5a5cc4f4e9 | |||
| cd672c341f | |||
| 12438d63c2 | |||
| 35255e4053 |
@@ -235,7 +235,7 @@ Conditionally show/hide the bar pill:
|
||||
```qml
|
||||
PluginComponent {
|
||||
visibilityCommand: "pgrep -x myapp"
|
||||
visibilityInterval: 5000 // check every 5 seconds
|
||||
visibilityInterval: 5 // seconds between checks; polling pauses while the bar is hidden
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -115,3 +115,5 @@ core.*
|
||||
.direnv/
|
||||
quickshell/dms-plugins
|
||||
__pycache__
|
||||
|
||||
.vscode/
|
||||
|
||||
@@ -72,7 +72,7 @@ func runResolveInclude(cmd *cobra.Command, args []string) {
|
||||
result, err = checkHyprlandInclude(filename)
|
||||
case "niri":
|
||||
result, err = checkNiriInclude(filename)
|
||||
case "mangowc", "dwl", "mango":
|
||||
case "mangowc", "mango":
|
||||
result, err = checkMangoWCInclude(filename)
|
||||
default:
|
||||
log.Fatalf("Unknown compositor: %s", compositor)
|
||||
|
||||
@@ -39,7 +39,7 @@ Modes:
|
||||
full - Capture the focused output
|
||||
all - Capture all outputs combined
|
||||
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
|
||||
|
||||
Output format (--format):
|
||||
@@ -97,7 +97,7 @@ If no previous region exists, falls back to interactive selection.`,
|
||||
var ssWindowCmd = &cobra.Command{
|
||||
Use: "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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/dwl_ipc"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
|
||||
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
@@ -19,9 +18,9 @@ const (
|
||||
CompositorHyprland
|
||||
CompositorSway
|
||||
CompositorNiri
|
||||
CompositorDWL
|
||||
CompositorScroll
|
||||
CompositorMiracle
|
||||
CompositorMango
|
||||
)
|
||||
|
||||
var detectedCompositor Compositor = -1
|
||||
@@ -36,8 +35,14 @@ func DetectCompositor() Compositor {
|
||||
swaySocket := os.Getenv("SWAYSOCK")
|
||||
scrollSocket := os.Getenv("SCROLLSOCK")
|
||||
miracleSocket := os.Getenv("MIRACLESOCK")
|
||||
mangoSocket := os.Getenv("MANGO_INSTANCE_SIGNATURE")
|
||||
|
||||
switch {
|
||||
case mangoSocket != "":
|
||||
if _, err := os.Stat(mangoSocket); err == nil {
|
||||
detectedCompositor = CompositorMango
|
||||
return detectedCompositor
|
||||
}
|
||||
case niriSocket != "":
|
||||
if _, err := os.Stat(niriSocket); err == nil {
|
||||
detectedCompositor = CompositorNiri
|
||||
@@ -63,66 +68,29 @@ func DetectCompositor() Compositor {
|
||||
return detectedCompositor
|
||||
}
|
||||
|
||||
if detectDWLProtocol() {
|
||||
detectedCompositor = CompositorDWL
|
||||
return detectedCompositor
|
||||
}
|
||||
|
||||
detectedCompositor = CompositorUnknown
|
||||
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 {
|
||||
X int32
|
||||
Y int32
|
||||
Width int32
|
||||
Height int32
|
||||
Output string
|
||||
Scale float64
|
||||
OutputX int32
|
||||
OutputY int32
|
||||
OutputTransform int32
|
||||
X int32
|
||||
Y int32
|
||||
Width int32
|
||||
Height int32
|
||||
Output string
|
||||
Scale float64
|
||||
OutputX int32
|
||||
OutputY int32
|
||||
}
|
||||
|
||||
func GetActiveWindow() (*WindowGeometry, error) {
|
||||
switch DetectCompositor() {
|
||||
case CompositorHyprland:
|
||||
return getHyprlandActiveWindow()
|
||||
case CompositorDWL:
|
||||
return getDWLActiveWindow()
|
||||
case CompositorMango:
|
||||
return getMangoActiveWindow()
|
||||
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 ""
|
||||
}
|
||||
|
||||
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 {
|
||||
Output string `json:"output"`
|
||||
IsFocused bool `json:"is_focused"`
|
||||
@@ -309,121 +364,6 @@ func getNiriFocusedMonitor() string {
|
||||
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 {
|
||||
switch DetectCompositor() {
|
||||
case CompositorHyprland:
|
||||
@@ -436,8 +376,8 @@ func GetFocusedMonitor() string {
|
||||
return getMiracleFocusedMonitor()
|
||||
case CompositorNiri:
|
||||
return getNiriFocusedMonitor()
|
||||
case CompositorDWL:
|
||||
return getDWLFocusedMonitor()
|
||||
case CompositorMango:
|
||||
return getMangoFocusedMonitor()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -534,161 +474,3 @@ func getAllOutputInfos() map[string]*outputInfo {
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -156,14 +156,14 @@ func (s *Screenshoter) captureWindow() (*CaptureResult, error) {
|
||||
switch DetectCompositor() {
|
||||
case CompositorHyprland:
|
||||
return s.captureAndCrop(output, region)
|
||||
case CompositorDWL:
|
||||
return s.captureDWLWindow(output, region, geom)
|
||||
case CompositorMango:
|
||||
return s.captureMangoWindow(output, region, geom)
|
||||
default:
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -628,7 +628,7 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio
|
||||
w := int32(float64(region.Width) * scale)
|
||||
h := int32(float64(region.Height) * scale)
|
||||
|
||||
if DetectCompositor() == CompositorDWL {
|
||||
if DetectCompositor() == CompositorMango {
|
||||
scaledOutW := int32(float64(output.width) * scale)
|
||||
scaledOutH := int32(float64(output.height) * scale)
|
||||
if localX >= scaledOutW {
|
||||
|
||||
@@ -935,7 +935,7 @@ func (m *Manager) CreateHistoryEntryFromPinned(pinnedEntry *Entry) error {
|
||||
Pinned: false,
|
||||
}
|
||||
|
||||
if err := m.storeEntryWithoutDedup(newEntry); err != nil {
|
||||
if err := m.storeEntry(newEntry); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -945,36 +945,6 @@ func (m *Manager) CreateHistoryEntryFromPinned(pinnedEntry *Entry) error {
|
||||
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() {
|
||||
if m.db == nil {
|
||||
return
|
||||
@@ -1653,6 +1623,37 @@ func (m *Manager) UnpinEntry(id uint64) error {
|
||||
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
|
||||
encoded, err := encodeEntry(entry)
|
||||
if err != nil {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
m := &Manager{
|
||||
subscribers: make(map[string]chan State),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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, ¤tState) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups"
|
||||
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/freedesktop"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/location"
|
||||
@@ -125,15 +124,6 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
||||
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 brightnessManager == nil {
|
||||
models.RespondError(conn, req.ID, "brightness manager not initialized")
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups"
|
||||
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/freedesktop"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/location"
|
||||
@@ -39,7 +38,7 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
)
|
||||
|
||||
const APIVersion = 24
|
||||
const APIVersion = 25
|
||||
|
||||
var CLIVersion = "dev"
|
||||
|
||||
@@ -66,7 +65,6 @@ var bluezManager *bluez.Manager
|
||||
var appPickerManager *apppicker.Manager
|
||||
var cupsManager *cups.Manager
|
||||
var tailscaleManager *tailscale.Manager
|
||||
var dwlManager *dwl.Manager
|
||||
var brightnessManager *brightness.Manager
|
||||
var wlrOutputManager *wlroutput.Manager
|
||||
var evdevManager *evdev.Manager
|
||||
@@ -252,30 +250,6 @@ func InitializeCupsManager() error {
|
||||
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 {
|
||||
manager, err := brightness.NewManager()
|
||||
if err != nil {
|
||||
@@ -468,10 +442,6 @@ func getCapabilities() Capabilities {
|
||||
caps = append(caps, "tailscale")
|
||||
}
|
||||
|
||||
if dwlManager != nil {
|
||||
caps = append(caps, "dwl")
|
||||
}
|
||||
|
||||
if brightnessManager != nil {
|
||||
caps = append(caps, "brightness")
|
||||
}
|
||||
@@ -538,10 +508,6 @@ func getServerInfo() ServerInfo {
|
||||
caps = append(caps, "tailscale")
|
||||
}
|
||||
|
||||
if dwlManager != nil {
|
||||
caps = append(caps, "dwl")
|
||||
}
|
||||
|
||||
if brightnessManager != nil {
|
||||
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 {
|
||||
wg.Add(2)
|
||||
brightnessStateChan := brightnessManager.Subscribe(clientID + "-brightness-state")
|
||||
@@ -1333,9 +1267,6 @@ func cleanupManagers() {
|
||||
if cupsManager != nil {
|
||||
cupsManager.Close()
|
||||
}
|
||||
if dwlManager != nil {
|
||||
dwlManager.Close()
|
||||
}
|
||||
if brightnessManager != nil {
|
||||
brightnessManager.Close()
|
||||
}
|
||||
@@ -1502,19 +1433,6 @@ func Start(printDocs bool) error {
|
||||
log.Info(" cups.resumePrinter - Resume printer (params: printerName)")
|
||||
log.Info(" cups.cancelJob - Cancel job (params: printerName, jobID)")
|
||||
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.getState - Get current brightness state for all devices")
|
||||
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)
|
||||
}
|
||||
|
||||
if err := InitializeDwlManager(); err != nil {
|
||||
log.Debugf("DWL manager unavailable: %v", err)
|
||||
}
|
||||
|
||||
if err := InitializeWlrOutputManager(); err != nil {
|
||||
log.Debugf("WlrOutput manager unavailable: %v", err)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ Item {
|
||||
|
||||
required property var modalHandle
|
||||
required property string claimPrefix
|
||||
property string surfaceKind: "modal"
|
||||
property string screenName: ""
|
||||
property bool enabled: false
|
||||
property bool active: false
|
||||
@@ -14,112 +15,97 @@ Item {
|
||||
property bool dockBlocked: false
|
||||
property string dockSide: ""
|
||||
|
||||
property string claimId: ""
|
||||
property string claimedScreenName: ""
|
||||
property alias claimId: lease.claimId
|
||||
property alias claimedScreenName: lease.claimedScreenName
|
||||
|
||||
signal recoveryRequested
|
||||
|
||||
visible: false
|
||||
|
||||
function _nextClaimId() {
|
||||
return claimPrefix + ":" + (new Date()).getTime() + ":" + Math.floor(Math.random() * 1000);
|
||||
}
|
||||
|
||||
function _isCurrentModal(name) {
|
||||
return !!name && ModalManager.isCurrentModal(modalHandle, name);
|
||||
}
|
||||
|
||||
function _shouldRecover() {
|
||||
return active && enabled && _isCurrentModal(screenName);
|
||||
}
|
||||
|
||||
function _requestRecovery() {
|
||||
if (_shouldRecover())
|
||||
recoveryRequested();
|
||||
ConnectedSurfaceLease {
|
||||
id: lease
|
||||
claimPrefix: root.claimPrefix
|
||||
screenName: root.screenName
|
||||
enabled: root.enabled
|
||||
active: root.active
|
||||
presented: root.presented
|
||||
dockBlocked: root.dockBlocked
|
||||
dockSide: root.dockSide
|
||||
isCurrentOwner: function(name) {
|
||||
return root._isCurrentModal(name);
|
||||
}
|
||||
hasOwner: function(name, ownerId) {
|
||||
return ConnectedModeState.hasModalOwner(name, ownerId);
|
||||
}
|
||||
statePresent: function(name, ownerId) {
|
||||
return ConnectedModeState.hasModalOwner(name, ownerId) && ConnectedModeState.hasSurfaceDescriptor(name, root.surfaceKind, ownerId);
|
||||
}
|
||||
claimState: function(name, state, ownerId) {
|
||||
return ConnectedModeState.claimModalState(name, state, ownerId);
|
||||
}
|
||||
ensureState: function(name, state, ownerId) {
|
||||
return ConnectedModeState.ensureModalState(name, state, ownerId);
|
||||
}
|
||||
releaseState: function(name, ownerId) {
|
||||
return ConnectedModeState.clearModalState(name, ownerId);
|
||||
}
|
||||
updateAnimationState: function(name, ownerId, animX, animY) {
|
||||
return ConnectedModeState.setModalAnim(name, animX, animY, ownerId);
|
||||
}
|
||||
updateBodyState: function(name, ownerId, bodyX, bodyY, bodyW, bodyH) {
|
||||
return ConnectedModeState.setModalBody(name, bodyX, bodyY, bodyW, bodyH, ownerId);
|
||||
}
|
||||
requestDockRetract: function(ownerId, name, side) {
|
||||
return ConnectedModeState.requestDockRetract(ownerId, name, side);
|
||||
}
|
||||
releaseDockRetract: function(ownerId) {
|
||||
return ConnectedModeState.releaseDockRetract(ownerId);
|
||||
}
|
||||
onRecoveryRequested: root.recoveryRequested()
|
||||
}
|
||||
|
||||
function publish(state) {
|
||||
if (!enabled || !screenName || !state) {
|
||||
release();
|
||||
return false;
|
||||
}
|
||||
if (claimedScreenName && claimedScreenName !== screenName)
|
||||
release();
|
||||
|
||||
const isCurrent = _isCurrentModal(screenName);
|
||||
let isClaim = !claimId;
|
||||
if (isClaim && !isCurrent)
|
||||
return false;
|
||||
if (isClaim)
|
||||
claimId = _nextClaimId();
|
||||
|
||||
let published = isClaim ? ConnectedModeState.claimModalState(screenName, state, claimId) : ConnectedModeState.ensureModalState(screenName, state, claimId);
|
||||
if (!published && !isClaim && isCurrent) {
|
||||
ConnectedModeState.releaseDockRetract(claimId);
|
||||
claimId = _nextClaimId();
|
||||
published = ConnectedModeState.claimModalState(screenName, state, claimId);
|
||||
}
|
||||
if (!published)
|
||||
return false;
|
||||
|
||||
claimedScreenName = screenName;
|
||||
if (dockBlocked && presented)
|
||||
ConnectedModeState.requestDockRetract(claimId, screenName, dockSide);
|
||||
else
|
||||
ConnectedModeState.releaseDockRetract(claimId);
|
||||
return true;
|
||||
return lease.publish(Object.assign({}, state, {
|
||||
"kind": root.surfaceKind,
|
||||
"screenName": root.screenName,
|
||||
"presented": root.presented,
|
||||
"dockRetractSide": root.dockBlocked ? root.dockSide : ""
|
||||
}), false);
|
||||
}
|
||||
|
||||
function updateAnim(animX, animY) {
|
||||
if (!enabled || !claimId || !claimedScreenName)
|
||||
return false;
|
||||
if (!ConnectedModeState.hasModalOwner(claimedScreenName, claimId)) {
|
||||
_requestRecovery();
|
||||
return false;
|
||||
}
|
||||
return ConnectedModeState.setModalAnim(claimedScreenName, animX, animY, claimId);
|
||||
return lease.updateAnim(animX, animY);
|
||||
}
|
||||
|
||||
function updateBody(bodyX, bodyY, bodyW, bodyH) {
|
||||
if (!enabled || !claimId || !claimedScreenName)
|
||||
return false;
|
||||
if (!ConnectedModeState.hasModalOwner(claimedScreenName, claimId)) {
|
||||
_requestRecovery();
|
||||
return false;
|
||||
}
|
||||
return ConnectedModeState.setModalBody(claimedScreenName, bodyX, bodyY, bodyW, bodyH, claimId);
|
||||
return lease.updateBody(bodyX, bodyY, bodyW, bodyH);
|
||||
}
|
||||
|
||||
function release() {
|
||||
if (!claimId)
|
||||
return;
|
||||
ConnectedModeState.releaseDockRetract(claimId);
|
||||
const releasedClaimId = claimId;
|
||||
const releasedScreenName = claimedScreenName;
|
||||
claimId = "";
|
||||
claimedScreenName = "";
|
||||
if (releasedScreenName)
|
||||
ConnectedModeState.clearModalState(releasedScreenName, releasedClaimId);
|
||||
return lease.release();
|
||||
}
|
||||
|
||||
Component.onDestruction: release()
|
||||
|
||||
Connections {
|
||||
target: ModalManager
|
||||
function onModalChanged() {
|
||||
root._requestRecovery();
|
||||
lease.requestRecovery();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ConnectedModeState
|
||||
function onModalOwnersChanged() {
|
||||
if (!ConnectedModeState.hasModalOwner(root.screenName, root.claimId))
|
||||
root._requestRecovery();
|
||||
lease.checkOwnershipRecovery();
|
||||
}
|
||||
function onModalStatesChanged() {
|
||||
if (!ConnectedModeState.modalStates[root.screenName])
|
||||
root._requestRecovery();
|
||||
lease.checkStateRecovery();
|
||||
}
|
||||
function onSurfaceDescriptorsChanged() {
|
||||
lease.checkStateRecovery();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,123 @@ pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import "ConnectedSurfaceDescriptor.js" as SurfaceDescriptor
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property var surfaceDescriptors: ({})
|
||||
|
||||
function _surfaceSlot(kind) {
|
||||
return SurfaceDescriptor.slotForKind(kind);
|
||||
}
|
||||
|
||||
function surfaceDescriptor(screenName, kind) {
|
||||
const slot = _surfaceSlot(kind);
|
||||
const screenDescriptors = screenName ? surfaceDescriptors[screenName] : null;
|
||||
const descriptor = screenDescriptors && screenDescriptors[slot] ? screenDescriptors[slot] : SurfaceDescriptor.empty(kind, screenName);
|
||||
let bodyRect = descriptor.bodyRect;
|
||||
let animationOffset = descriptor.animationOffset;
|
||||
if (slot === "popout" && popoutScreen === screenName) {
|
||||
bodyRect = {
|
||||
"x": popoutBodyX,
|
||||
"y": popoutBodyY,
|
||||
"width": popoutBodyW,
|
||||
"height": popoutBodyH
|
||||
};
|
||||
animationOffset = {
|
||||
"x": popoutAnimX,
|
||||
"y": popoutAnimY
|
||||
};
|
||||
} else if (slot === "modal" && modalStates[screenName]) {
|
||||
const modal = modalStates[screenName];
|
||||
bodyRect = {
|
||||
"x": modal.bodyX,
|
||||
"y": modal.bodyY,
|
||||
"width": modal.bodyW,
|
||||
"height": modal.bodyH
|
||||
};
|
||||
animationOffset = {
|
||||
"x": modal.animX,
|
||||
"y": modal.animY
|
||||
};
|
||||
} else if (slot === "dock" && dockStates[screenName]) {
|
||||
const dock = dockStates[screenName];
|
||||
const slide = dockSlides[screenName] || {
|
||||
"x": dock.slideX,
|
||||
"y": dock.slideY
|
||||
};
|
||||
bodyRect = {
|
||||
"x": dock.bodyX,
|
||||
"y": dock.bodyY,
|
||||
"width": dock.bodyW,
|
||||
"height": dock.bodyH
|
||||
};
|
||||
animationOffset = {
|
||||
"x": slide.x,
|
||||
"y": slide.y
|
||||
};
|
||||
} else if (slot === "notification" && notificationStates[screenName]) {
|
||||
const notification = notificationStates[screenName];
|
||||
bodyRect = {
|
||||
"x": notification.bodyX,
|
||||
"y": notification.bodyY,
|
||||
"width": notification.bodyW,
|
||||
"height": notification.bodyH
|
||||
};
|
||||
}
|
||||
return SurfaceDescriptor.normalize({
|
||||
"bodyRect": bodyRect,
|
||||
"animationOffset": animationOffset
|
||||
}, descriptor);
|
||||
}
|
||||
|
||||
function hasSurfaceDescriptor(screenName, kind, ownerId) {
|
||||
const descriptor = surfaceDescriptor(screenName, kind);
|
||||
return descriptor.phase !== "hidden" && (!ownerId || descriptor.ownerId === ownerId);
|
||||
}
|
||||
|
||||
function _setSurfaceDescriptor(screenName, slotKind, state, ownerId) {
|
||||
if (!screenName || !state)
|
||||
return false;
|
||||
const slot = _surfaceSlot(slotKind);
|
||||
const currentScreen = surfaceDescriptors[screenName] || {};
|
||||
const previous = currentScreen[slot] || SurfaceDescriptor.empty(state.kind || slotKind, screenName);
|
||||
let normalized = SurfaceDescriptor.normalize(Object.assign({}, state, {
|
||||
"ownerId": ownerId !== undefined ? ownerId : previous.ownerId,
|
||||
"screenName": screenName,
|
||||
"revision": previous.revision
|
||||
}), previous);
|
||||
if (SurfaceDescriptor.same(previous, normalized))
|
||||
return true;
|
||||
normalized = SurfaceDescriptor.withRevision(normalized, previous.revision + 1);
|
||||
const nextScreen = _cloneDict(currentScreen);
|
||||
nextScreen[slot] = normalized;
|
||||
const next = _cloneDict(surfaceDescriptors);
|
||||
next[screenName] = nextScreen;
|
||||
surfaceDescriptors = next;
|
||||
return true;
|
||||
}
|
||||
|
||||
function _clearSurfaceDescriptor(screenName, kind, ownerId) {
|
||||
if (!screenName)
|
||||
return false;
|
||||
const slot = _surfaceSlot(kind);
|
||||
const currentScreen = surfaceDescriptors[screenName];
|
||||
const current = currentScreen ? currentScreen[slot] : null;
|
||||
if (!current || (ownerId && current.ownerId !== ownerId))
|
||||
return false;
|
||||
const nextScreen = _cloneDict(currentScreen);
|
||||
delete nextScreen[slot];
|
||||
const next = _cloneDict(surfaceDescriptors);
|
||||
if (Object.keys(nextScreen).length > 0)
|
||||
next[screenName] = nextScreen;
|
||||
else
|
||||
delete next[screenName];
|
||||
surfaceDescriptors = next;
|
||||
return true;
|
||||
}
|
||||
|
||||
readonly property var emptyDockState: ({
|
||||
"reveal": false,
|
||||
"barSide": "bottom",
|
||||
@@ -18,7 +131,6 @@ Singleton {
|
||||
"slideY": 0
|
||||
})
|
||||
|
||||
// Popout state (updated by DankPopout when connectedFrameModeActive)
|
||||
property string popoutOwnerId: ""
|
||||
property bool popoutVisible: false
|
||||
property string popoutBarSide: "top"
|
||||
@@ -32,14 +144,10 @@ Singleton {
|
||||
property bool popoutOmitStartConnector: false
|
||||
property bool popoutOmitEndConnector: false
|
||||
|
||||
// Dock state (updated by Dock when connectedFrameModeActive), keyed by screen.name
|
||||
property var dockStates: ({})
|
||||
|
||||
// Dock slide offsets — hot-path updates separated from full geometry state
|
||||
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: ({})
|
||||
|
||||
function _cloneDict(src) {
|
||||
@@ -69,8 +177,10 @@ Singleton {
|
||||
popoutOwnerId = claimId;
|
||||
const ok = updatePopout(claimId, state);
|
||||
if (ok) {
|
||||
if (previousScreen && previousScreen !== popoutScreen)
|
||||
if (previousScreen && previousScreen !== popoutScreen) {
|
||||
_clearSurfaceDescriptor(previousScreen, "popout");
|
||||
_bumpSurfaceRevision(previousScreen);
|
||||
}
|
||||
_bumpSurfaceRevision(popoutScreen);
|
||||
}
|
||||
return ok;
|
||||
@@ -103,6 +213,21 @@ Singleton {
|
||||
if (state.omitEndConnector !== undefined)
|
||||
popoutOmitEndConnector = !!state.omitEndConnector;
|
||||
|
||||
_setSurfaceDescriptor(popoutScreen, "popout", Object.assign({}, state, {
|
||||
"kind": "popout",
|
||||
"screenName": popoutScreen,
|
||||
"visible": popoutVisible,
|
||||
"presented": state.presented !== undefined ? !!state.presented : popoutVisible,
|
||||
"barSide": popoutBarSide,
|
||||
"bodyX": popoutBodyX,
|
||||
"bodyY": popoutBodyY,
|
||||
"bodyW": popoutBodyW,
|
||||
"bodyH": popoutBodyH,
|
||||
"animX": popoutAnimX,
|
||||
"animY": popoutAnimY,
|
||||
"omitStartConnector": popoutOmitStartConnector,
|
||||
"omitEndConnector": popoutOmitEndConnector
|
||||
}), claimId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -123,6 +248,7 @@ Singleton {
|
||||
popoutScreen = "";
|
||||
popoutOmitStartConnector = false;
|
||||
popoutOmitEndConnector = false;
|
||||
_clearSurfaceDescriptor(releasedScreen, "popout", claimId);
|
||||
_bumpSurfaceRevision(releasedScreen);
|
||||
return true;
|
||||
}
|
||||
@@ -193,13 +319,21 @@ Singleton {
|
||||
return false;
|
||||
|
||||
const normalized = _normalizeDockState(state);
|
||||
if (_sameDockState(dockStates[screenName], normalized))
|
||||
return true;
|
||||
const descriptorState = Object.assign({}, state, normalized, {
|
||||
"kind": "dock",
|
||||
"screenName": screenName,
|
||||
"visible": normalized.reveal,
|
||||
"presented": normalized.reveal,
|
||||
"phase": normalized.reveal ? (state.phase || "open") : "hidden"
|
||||
});
|
||||
const previous = dockStates[screenName] || emptyDockState;
|
||||
|
||||
const next = _cloneDict(dockStates);
|
||||
next[screenName] = normalized;
|
||||
dockStates = next;
|
||||
const stateChanged = !_sameDockState(dockStates[screenName], normalized);
|
||||
if (stateChanged) {
|
||||
const next = _cloneDict(dockStates);
|
||||
next[screenName] = normalized;
|
||||
dockStates = next;
|
||||
}
|
||||
_setSurfaceDescriptor(screenName, "dock", descriptorState, "dock:" + screenName);
|
||||
if (!!previous.reveal !== !!normalized.reveal)
|
||||
_bumpSurfaceRevision(screenName);
|
||||
return true;
|
||||
@@ -212,8 +346,8 @@ Singleton {
|
||||
const next = _cloneDict(dockStates);
|
||||
delete next[screenName];
|
||||
dockStates = next;
|
||||
_clearSurfaceDescriptor(screenName, "dock");
|
||||
|
||||
// Also clear corresponding slide
|
||||
if (dockSlides[screenName]) {
|
||||
const nextSlides = _cloneDict(dockSlides);
|
||||
delete nextSlides[screenName];
|
||||
@@ -283,13 +417,20 @@ Singleton {
|
||||
return false;
|
||||
|
||||
const normalized = _normalizeNotificationState(state);
|
||||
if (_sameNotificationState(notificationStates[screenName], normalized))
|
||||
return true;
|
||||
const descriptorState = Object.assign({}, state, normalized, {
|
||||
"kind": "notification",
|
||||
"screenName": screenName,
|
||||
"presented": normalized.visible,
|
||||
"phase": normalized.visible ? (state.phase || "open") : "hidden"
|
||||
});
|
||||
const previous = notificationStates[screenName] || emptyNotificationState;
|
||||
|
||||
const next = _cloneDict(notificationStates);
|
||||
next[screenName] = normalized;
|
||||
notificationStates = next;
|
||||
const stateChanged = !_sameNotificationState(notificationStates[screenName], normalized);
|
||||
if (stateChanged) {
|
||||
const next = _cloneDict(notificationStates);
|
||||
next[screenName] = normalized;
|
||||
notificationStates = next;
|
||||
}
|
||||
_setSurfaceDescriptor(screenName, "notification", descriptorState, "notification:" + screenName);
|
||||
if (!!previous.visible !== !!normalized.visible)
|
||||
_bumpSurfaceRevision(screenName);
|
||||
return true;
|
||||
@@ -302,11 +443,11 @@ Singleton {
|
||||
const next = _cloneDict(notificationStates);
|
||||
delete next[screenName];
|
||||
notificationStates = next;
|
||||
_clearSurfaceDescriptor(screenName, "notification");
|
||||
_bumpSurfaceRevision(screenName);
|
||||
return true;
|
||||
}
|
||||
|
||||
// DankModal / DankLauncherV2Modal State
|
||||
readonly property var emptyModalState: ({
|
||||
"visible": false,
|
||||
"barSide": "bottom",
|
||||
@@ -362,6 +503,10 @@ Singleton {
|
||||
const next = _cloneDict(modalStates);
|
||||
next[screenName] = normalized;
|
||||
modalStates = next;
|
||||
_setSurfaceDescriptor(screenName, "modal", Object.assign({}, state, normalized, {
|
||||
"kind": state.kind || "modal",
|
||||
"screenName": screenName
|
||||
}), ownerId || "");
|
||||
_bumpSurfaceRevision(screenName);
|
||||
return true;
|
||||
}
|
||||
@@ -372,11 +517,16 @@ Singleton {
|
||||
if (ownerId && modalOwners[screenName] !== ownerId)
|
||||
return false;
|
||||
const normalized = _normalizeModalState(state);
|
||||
if (_sameModalState(modalStates[screenName], normalized))
|
||||
return true;
|
||||
const next = _cloneDict(modalStates);
|
||||
next[screenName] = normalized;
|
||||
modalStates = next;
|
||||
const descriptorState = Object.assign({}, state, normalized, {
|
||||
"kind": state.kind || (surfaceDescriptor(screenName, "modal").kind || "modal"),
|
||||
"screenName": screenName
|
||||
});
|
||||
if (!_sameModalState(modalStates[screenName], normalized)) {
|
||||
const next = _cloneDict(modalStates);
|
||||
next[screenName] = normalized;
|
||||
modalStates = next;
|
||||
}
|
||||
_setSurfaceDescriptor(screenName, "modal", descriptorState, ownerId || modalOwners[screenName] || "");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -395,10 +545,6 @@ Singleton {
|
||||
return updateModalState(screenName, state, ownerId);
|
||||
}
|
||||
|
||||
function setModalState(screenName, state) {
|
||||
return updateModalState(screenName, state, null);
|
||||
}
|
||||
|
||||
function clearModalState(screenName, ownerId) {
|
||||
if (!screenName)
|
||||
return false;
|
||||
@@ -418,6 +564,7 @@ Singleton {
|
||||
delete nextOwners[screenName];
|
||||
modalOwners = nextOwners;
|
||||
}
|
||||
_clearSurfaceDescriptor(screenName, "modal", ownerId);
|
||||
_bumpSurfaceRevision(screenName);
|
||||
return true;
|
||||
}
|
||||
@@ -501,9 +648,6 @@ Singleton {
|
||||
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() {
|
||||
const live = {};
|
||||
const screens = Quickshell.screens || [];
|
||||
@@ -543,6 +687,9 @@ Singleton {
|
||||
const nextSurfaceRevisions = pruneKeyed(surfaceRevisions);
|
||||
if (nextSurfaceRevisions !== null)
|
||||
surfaceRevisions = nextSurfaceRevisions;
|
||||
const nextDescriptors = pruneKeyed(surfaceDescriptors);
|
||||
if (nextDescriptors !== null)
|
||||
surfaceDescriptors = nextDescriptors;
|
||||
|
||||
let retractChanged = false;
|
||||
const nextRetract = {};
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
.pragma library
|
||||
|
||||
var VALID_KINDS = {
|
||||
"popout": true,
|
||||
"modal": true,
|
||||
"launcher": true,
|
||||
"dock": true,
|
||||
"notification": true
|
||||
};
|
||||
|
||||
var VALID_PHASES = {
|
||||
"opening": true,
|
||||
"open": true,
|
||||
"closing": true,
|
||||
"hidden": true,
|
||||
"recovering": true
|
||||
};
|
||||
|
||||
function _number(value, fallback) {
|
||||
var n = Number(value);
|
||||
return isNaN(n) ? fallback : n;
|
||||
}
|
||||
|
||||
function _bool(value, fallback) {
|
||||
return value === undefined ? fallback : !!value;
|
||||
}
|
||||
|
||||
function _kind(value, fallback) {
|
||||
if (VALID_KINDS[value])
|
||||
return value;
|
||||
return VALID_KINDS[fallback] ? fallback : "modal";
|
||||
}
|
||||
|
||||
function _defaultBarSide(kind) {
|
||||
return kind === "popout" || kind === "notification" ? "top" : "bottom";
|
||||
}
|
||||
|
||||
function _barSide(value, fallback) {
|
||||
if (value === "top" || value === "bottom" || value === "left" || value === "right")
|
||||
return value;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function slotForKind(kind) {
|
||||
return kind === "launcher" ? "modal" : _kind(kind, "modal");
|
||||
}
|
||||
|
||||
function inferPhase(visible, presented, requestedPhase) {
|
||||
if (VALID_PHASES[requestedPhase])
|
||||
return requestedPhase;
|
||||
if (!visible && !presented)
|
||||
return "hidden";
|
||||
if (!visible && presented)
|
||||
return "closing";
|
||||
return "open";
|
||||
}
|
||||
|
||||
function normalize(input, defaults) {
|
||||
var source = input || {};
|
||||
var base = defaults || {};
|
||||
var kind = _kind(source.kind, base.kind);
|
||||
var defaultSide = _defaultBarSide(kind);
|
||||
var sourceRect = source.bodyRect || {};
|
||||
var baseRect = base.bodyRect || {};
|
||||
var sourceOffset = source.animationOffset || {};
|
||||
var baseOffset = base.animationOffset || {};
|
||||
var visible = _bool(source.visible !== undefined ? source.visible : source.reveal, _bool(base.visible !== undefined ? base.visible : base.reveal, false));
|
||||
var presented = _bool(source.presented, _bool(base.presented, visible));
|
||||
var bodyRect = {
|
||||
"x": _number(sourceRect.x !== undefined ? sourceRect.x : source.bodyX, _number(baseRect.x !== undefined ? baseRect.x : base.bodyX, 0)),
|
||||
"y": _number(sourceRect.y !== undefined ? sourceRect.y : source.bodyY, _number(baseRect.y !== undefined ? baseRect.y : base.bodyY, 0)),
|
||||
"width": Math.max(0, _number(sourceRect.width !== undefined ? sourceRect.width : source.bodyW, _number(baseRect.width !== undefined ? baseRect.width : base.bodyW, 0))),
|
||||
"height": Math.max(0, _number(sourceRect.height !== undefined ? sourceRect.height : source.bodyH, _number(baseRect.height !== undefined ? baseRect.height : base.bodyH, 0)))
|
||||
};
|
||||
var animationOffset = {
|
||||
"x": _number(sourceOffset.x !== undefined ? sourceOffset.x : (source.animX !== undefined ? source.animX : source.slideX), _number(baseOffset.x !== undefined ? baseOffset.x : (base.animX !== undefined ? base.animX : base.slideX), 0)),
|
||||
"y": _number(sourceOffset.y !== undefined ? sourceOffset.y : (source.animY !== undefined ? source.animY : source.slideY), _number(baseOffset.y !== undefined ? baseOffset.y : (base.animY !== undefined ? base.animY : base.slideY), 0))
|
||||
};
|
||||
var screenName = source.screenName !== undefined ? source.screenName : (source.screen !== undefined ? source.screen : (base.screenName !== undefined ? base.screenName : base.screen));
|
||||
var opacity = Math.max(0, Math.min(1, _number(source.opacity, _number(base.opacity, 1))));
|
||||
|
||||
return {
|
||||
"ownerId": String(source.ownerId !== undefined ? source.ownerId : (base.ownerId || "")),
|
||||
"kind": kind,
|
||||
"screenName": String(screenName || ""),
|
||||
"phase": inferPhase(visible, presented, source.phase !== undefined ? source.phase : base.phase),
|
||||
"visible": visible,
|
||||
"presented": presented,
|
||||
"barSide": _barSide(source.barSide, _barSide(base.barSide, defaultSide)),
|
||||
"bodyRect": bodyRect,
|
||||
"animationOffset": animationOffset,
|
||||
"scale": Math.max(0, _number(source.scale, _number(base.scale, 1))),
|
||||
"opacity": opacity,
|
||||
"omitStartConnector": _bool(source.omitStartConnector, _bool(base.omitStartConnector, false)),
|
||||
"omitEndConnector": _bool(source.omitEndConnector, _bool(base.omitEndConnector, false)),
|
||||
"dockRetractSide": String(source.dockRetractSide !== undefined ? source.dockRetractSide : (base.dockRetractSide || "")),
|
||||
"revision": Math.max(0, Math.floor(_number(source.revision, _number(base.revision, 0))))
|
||||
};
|
||||
}
|
||||
|
||||
function empty(kind, screenName) {
|
||||
return normalize({
|
||||
"kind": kind,
|
||||
"screenName": screenName || "",
|
||||
"phase": "hidden",
|
||||
"visible": false,
|
||||
"presented": false
|
||||
});
|
||||
}
|
||||
|
||||
function withRevision(descriptor, revision) {
|
||||
var next = normalize(descriptor);
|
||||
next.revision = Math.max(0, Math.floor(_number(revision, next.revision)));
|
||||
return next;
|
||||
}
|
||||
|
||||
function withAnimationOffset(descriptor, x, y) {
|
||||
var next = normalize(descriptor);
|
||||
next.animationOffset = {
|
||||
"x": x === undefined ? next.animationOffset.x : _number(x, next.animationOffset.x),
|
||||
"y": y === undefined ? next.animationOffset.y : _number(y, next.animationOffset.y)
|
||||
};
|
||||
return next;
|
||||
}
|
||||
|
||||
function withBodyRect(descriptor, x, y, width, height) {
|
||||
var next = normalize(descriptor);
|
||||
next.bodyRect = {
|
||||
"x": x === undefined ? next.bodyRect.x : _number(x, next.bodyRect.x),
|
||||
"y": y === undefined ? next.bodyRect.y : _number(y, next.bodyRect.y),
|
||||
"width": width === undefined ? next.bodyRect.width : Math.max(0, _number(width, next.bodyRect.width)),
|
||||
"height": height === undefined ? next.bodyRect.height : Math.max(0, _number(height, next.bodyRect.height))
|
||||
};
|
||||
return next;
|
||||
}
|
||||
|
||||
function same(a, b, threshold) {
|
||||
if (!a || !b)
|
||||
return false;
|
||||
var epsilon = threshold === undefined ? 0.5 : Math.max(0, Number(threshold));
|
||||
return a.ownerId === b.ownerId
|
||||
&& a.kind === b.kind
|
||||
&& a.screenName === b.screenName
|
||||
&& a.phase === b.phase
|
||||
&& a.visible === b.visible
|
||||
&& a.presented === b.presented
|
||||
&& a.barSide === b.barSide
|
||||
&& Math.abs(a.bodyRect.x - b.bodyRect.x) < epsilon
|
||||
&& Math.abs(a.bodyRect.y - b.bodyRect.y) < epsilon
|
||||
&& Math.abs(a.bodyRect.width - b.bodyRect.width) < epsilon
|
||||
&& Math.abs(a.bodyRect.height - b.bodyRect.height) < epsilon
|
||||
&& Math.abs(a.animationOffset.x - b.animationOffset.x) < epsilon
|
||||
&& Math.abs(a.animationOffset.y - b.animationOffset.y) < epsilon
|
||||
&& Math.abs(a.scale - b.scale) < 0.0001
|
||||
&& Math.abs(a.opacity - b.opacity) < 0.0001
|
||||
&& a.omitStartConnector === b.omitStartConnector
|
||||
&& a.omitEndConnector === b.omitEndConnector
|
||||
&& a.dockRetractSide === b.dockRetractSide;
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
.pragma library
|
||||
|
||||
function _number(value, fallback) {
|
||||
var n = Number(value);
|
||||
return isNaN(n) ? fallback : n;
|
||||
}
|
||||
|
||||
function snap(value, dpr) {
|
||||
var scale = dpr || 1;
|
||||
return Math.round(_number(value, 0) * scale) / scale;
|
||||
}
|
||||
|
||||
function isHorizontal(side) {
|
||||
return side === "top" || side === "bottom";
|
||||
}
|
||||
|
||||
function isVertical(side) {
|
||||
return side === "left" || side === "right";
|
||||
}
|
||||
|
||||
function bodyRect(descriptor, dpr) {
|
||||
var source = descriptor && descriptor.bodyRect ? descriptor.bodyRect : descriptor || {};
|
||||
return {
|
||||
"x": snap(source.x !== undefined ? source.x : source.bodyX, dpr),
|
||||
"y": snap(source.y !== undefined ? source.y : source.bodyY, dpr),
|
||||
"width": Math.max(0, snap(source.width !== undefined ? source.width : source.bodyW, dpr)),
|
||||
"height": Math.max(0, snap(source.height !== undefined ? source.height : source.bodyH, dpr))
|
||||
};
|
||||
}
|
||||
|
||||
function animatedBodyRect(descriptor, dpr) {
|
||||
var rect = bodyRect(descriptor, dpr);
|
||||
var offset = descriptor && descriptor.animationOffset ? descriptor.animationOffset : descriptor || {};
|
||||
var side = descriptor && descriptor.barSide ? descriptor.barSide : "bottom";
|
||||
var dx = isVertical(side) ? Math.max(-rect.width, Math.min(_number(offset.x !== undefined ? offset.x : offset.animX, 0), rect.width)) : 0;
|
||||
var dy = isHorizontal(side) ? Math.max(-rect.height, Math.min(_number(offset.y !== undefined ? offset.y : offset.animY, 0), rect.height)) : 0;
|
||||
|
||||
return {
|
||||
"x": snap(rect.x + (side === "right" ? dx : 0), dpr),
|
||||
"y": snap(rect.y + (side === "bottom" ? dy : 0), dpr),
|
||||
"width": Math.max(0, snap(rect.width - Math.abs(dx), dpr)),
|
||||
"height": Math.max(0, snap(rect.height - Math.abs(dy), dpr)),
|
||||
"dx": snap(dx, dpr),
|
||||
"dy": snap(dy, dpr)
|
||||
};
|
||||
}
|
||||
|
||||
function translatedBodyRect(descriptor, dpr) {
|
||||
var rect = bodyRect(descriptor, dpr);
|
||||
var offset = descriptor && descriptor.animationOffset ? descriptor.animationOffset : {};
|
||||
return {
|
||||
"x": snap(rect.x + _number(offset.x, 0), dpr),
|
||||
"y": snap(rect.y + _number(offset.y, 0), dpr),
|
||||
"width": rect.width,
|
||||
"height": rect.height
|
||||
};
|
||||
}
|
||||
|
||||
function connectorRadii(descriptor, rect, connectedRadius, surfaceRadius, dpr, nearIncludesSurface) {
|
||||
var side = descriptor && descriptor.barSide ? descriptor.barSide : "bottom";
|
||||
var horizontal = isHorizontal(side);
|
||||
var extent = horizontal ? rect.height : rect.width;
|
||||
var crossSize = horizontal ? rect.width : rect.height;
|
||||
var nearLimit = nearIncludesSurface ? Math.min(connectedRadius, surfaceRadius, extent, crossSize / 2) : Math.min(connectedRadius, extent, crossSize / 2);
|
||||
var farLimit = Math.min(connectedRadius, surfaceRadius, crossSize / 2);
|
||||
var near = snap(Math.max(0, nearLimit), dpr);
|
||||
var far = snap(Math.max(0, farLimit), dpr);
|
||||
var omitStart = !!(descriptor && descriptor.omitStartConnector);
|
||||
var omitEnd = !!(descriptor && descriptor.omitEndConnector);
|
||||
return {
|
||||
"near": near,
|
||||
"far": far,
|
||||
"start": omitStart ? 0 : near,
|
||||
"end": omitEnd ? 0 : near,
|
||||
"farStart": omitStart ? far : 0,
|
||||
"farEnd": omitEnd ? far : 0,
|
||||
"farExtent": Math.max(omitStart ? far : 0, omitEnd ? far : 0)
|
||||
};
|
||||
}
|
||||
|
||||
function _connectorWidth(side, spacing, radius) {
|
||||
return isVertical(side) ? spacing + radius : radius;
|
||||
}
|
||||
|
||||
function _connectorHeight(side, spacing, radius) {
|
||||
return isVertical(side) ? radius : spacing + radius;
|
||||
}
|
||||
|
||||
function connectorRect(side, rect, placement, spacing, radius, dpr) {
|
||||
var width = _connectorWidth(side, spacing, radius);
|
||||
var height = _connectorHeight(side, spacing, radius);
|
||||
var seamX = isVertical(side) ? (side === "left" ? rect.x : rect.x + rect.width) : (placement === "left" ? rect.x : rect.x + rect.width);
|
||||
var seamY = side === "top" ? rect.y : (side === "bottom" ? rect.y + rect.height : (placement === "left" ? rect.y : rect.y + rect.height));
|
||||
var x = isVertical(side) ? (side === "left" ? seamX : seamX - width) : (placement === "left" ? seamX - width : seamX);
|
||||
var y = side === "top" ? seamY : (side === "bottom" ? seamY - height : (placement === "left" ? seamY - height : seamY));
|
||||
return {
|
||||
"x": snap(x, dpr),
|
||||
"y": snap(y, dpr),
|
||||
"width": Math.max(0, snap(width, dpr)),
|
||||
"height": Math.max(0, snap(height, dpr))
|
||||
};
|
||||
}
|
||||
|
||||
function farConnectorRect(side, rect, placement, radius, dpr) {
|
||||
var x;
|
||||
var y;
|
||||
if (isHorizontal(side)) {
|
||||
x = placement === "left" ? rect.x : rect.x + rect.width - radius;
|
||||
y = side === "top" ? rect.y + rect.height : rect.y - radius;
|
||||
} else {
|
||||
x = side === "left" ? rect.x + rect.width : rect.x - radius;
|
||||
y = placement === "left" ? rect.y : rect.y + rect.height - radius;
|
||||
}
|
||||
return {
|
||||
"x": snap(x, dpr),
|
||||
"y": snap(y, dpr),
|
||||
"width": Math.max(0, snap(radius, dpr)),
|
||||
"height": Math.max(0, snap(radius, dpr))
|
||||
};
|
||||
}
|
||||
|
||||
function farBodyCapRect(side, rect, placement, radius, dpr) {
|
||||
var x;
|
||||
var y;
|
||||
if (isHorizontal(side)) {
|
||||
x = placement === "left" ? rect.x : rect.x + rect.width - radius;
|
||||
y = side === "top" ? rect.y + rect.height - radius : rect.y;
|
||||
} else {
|
||||
x = side === "left" ? rect.x + rect.width - radius : rect.x;
|
||||
y = placement === "left" ? rect.y : rect.y + rect.height - radius;
|
||||
}
|
||||
return {
|
||||
"x": snap(x, dpr),
|
||||
"y": snap(y, dpr),
|
||||
"width": Math.max(0, snap(radius, dpr)),
|
||||
"height": Math.max(0, snap(radius, dpr))
|
||||
};
|
||||
}
|
||||
|
||||
function chromeBounds(rect, side, startRadius, endRadius, farExtent, dpr) {
|
||||
var horizontal = isHorizontal(side);
|
||||
var bodyOffsetX = horizontal ? startRadius : (side === "right" ? farExtent : 0);
|
||||
var bodyOffsetY = horizontal ? (side === "bottom" ? farExtent : 0) : startRadius;
|
||||
return {
|
||||
"x": snap(rect.x - bodyOffsetX, dpr),
|
||||
"y": snap(rect.y - bodyOffsetY, dpr),
|
||||
"width": Math.max(0, snap(horizontal ? rect.width + startRadius + endRadius : rect.width + farExtent, dpr)),
|
||||
"height": Math.max(0, snap(horizontal ? rect.height + farExtent : rect.height + startRadius + endRadius, dpr)),
|
||||
"bodyOffsetX": snap(bodyOffsetX, dpr),
|
||||
"bodyOffsetY": snap(bodyOffsetY, dpr)
|
||||
};
|
||||
}
|
||||
|
||||
function fillBounds(rect, side, seamOverlap, dpr) {
|
||||
var overlapX = isHorizontal(side) ? seamOverlap : 0;
|
||||
var overlapY = isVertical(side) ? seamOverlap : 0;
|
||||
return {
|
||||
"x": snap(rect.x - overlapX, dpr),
|
||||
"y": snap(rect.y - overlapY, dpr),
|
||||
"width": Math.max(0, snap(rect.width + overlapX * 2, dpr)),
|
||||
"height": Math.max(0, snap(rect.height + overlapY * 2, dpr))
|
||||
};
|
||||
}
|
||||
|
||||
function clipEnvelope(rect, side, radii, seamOverlap, dpr) {
|
||||
var fill = fillBounds(rect, side, seamOverlap, dpr);
|
||||
var chrome = chromeBounds(fill, side, radii.start, radii.end, radii.farExtent, dpr);
|
||||
return {
|
||||
"x": chrome.x,
|
||||
"y": chrome.y,
|
||||
"width": chrome.width,
|
||||
"height": chrome.height,
|
||||
"bodyX": snap(fill.x - chrome.x, dpr),
|
||||
"bodyY": snap(fill.y - chrome.y, dpr),
|
||||
"bodyWidth": fill.width,
|
||||
"bodyHeight": fill.height
|
||||
};
|
||||
}
|
||||
|
||||
function blurRegions(descriptor, rect, radii, dpr) {
|
||||
var side = descriptor.barSide;
|
||||
var regions = [bodyRect(rect, dpr)];
|
||||
if (radii.start > 0)
|
||||
regions.push(connectorRect(side, rect, "left", 0, radii.start, dpr));
|
||||
if (radii.end > 0)
|
||||
regions.push(connectorRect(side, rect, "right", 0, radii.end, dpr));
|
||||
if (radii.farStart > 0) {
|
||||
regions.push(farConnectorRect(side, rect, "left", radii.farStart, dpr));
|
||||
regions.push(farBodyCapRect(side, rect, "left", radii.farStart, dpr));
|
||||
}
|
||||
if (radii.farEnd > 0) {
|
||||
regions.push(farConnectorRect(side, rect, "right", radii.farEnd, dpr));
|
||||
regions.push(farBodyCapRect(side, rect, "right", radii.farEnd, dpr));
|
||||
}
|
||||
return regions;
|
||||
}
|
||||
|
||||
function unionBounds(rects, padding, dpr) {
|
||||
var minX = Infinity;
|
||||
var minY = Infinity;
|
||||
var maxX = -Infinity;
|
||||
var maxY = -Infinity;
|
||||
for (var i = 0; i < rects.length; i++) {
|
||||
var rect = rects[i];
|
||||
if (!rect || rect.width <= 0 || rect.height <= 0)
|
||||
continue;
|
||||
minX = Math.min(minX, rect.x);
|
||||
minY = Math.min(minY, rect.y);
|
||||
maxX = Math.max(maxX, rect.x + rect.width);
|
||||
maxY = Math.max(maxY, rect.y + rect.height);
|
||||
}
|
||||
if (minX === Infinity)
|
||||
return {"x": 0, "y": 0, "width": 0, "height": 0};
|
||||
var pad = Math.max(0, _number(padding, 0));
|
||||
return {
|
||||
"x": snap(minX - pad, dpr),
|
||||
"y": snap(minY - pad, dpr),
|
||||
"width": Math.max(0, snap(maxX - minX + pad * 2, dpr)),
|
||||
"height": Math.max(0, snap(maxY - minY + pad * 2, dpr))
|
||||
};
|
||||
}
|
||||
|
||||
function shadowSourceBounds(descriptor, rect, radii, padding, dpr) {
|
||||
return unionBounds(blurRegions(descriptor, rect, radii, dpr), padding, dpr);
|
||||
}
|
||||
|
||||
function stableEqual(a, b, dpr) {
|
||||
if (!a || !b)
|
||||
return false;
|
||||
var threshold = 0.5 / (dpr || 1);
|
||||
return Math.abs(a.x - b.x) < threshold && Math.abs(a.y - b.y) < threshold && Math.abs(a.width - b.width) < threshold && Math.abs(a.height - b.height) < threshold;
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property string claimPrefix
|
||||
required property var isCurrentOwner
|
||||
required property var hasOwner
|
||||
required property var claimState
|
||||
required property var ensureState
|
||||
required property var releaseState
|
||||
|
||||
property var statePresent: null
|
||||
property var updateAnimationState: null
|
||||
property var updateBodyState: null
|
||||
property var requestDockRetract: null
|
||||
property var releaseDockRetract: null
|
||||
|
||||
property string screenName: ""
|
||||
property bool enabled: false
|
||||
property bool active: false
|
||||
property bool presented: false
|
||||
property bool dockBlocked: false
|
||||
property string dockSide: ""
|
||||
property bool renewTokenOnRecovery: true
|
||||
|
||||
property string claimId: ""
|
||||
property string claimedScreenName: ""
|
||||
property int _claimSerial: 0
|
||||
|
||||
signal recoveryRequested
|
||||
|
||||
visible: false
|
||||
|
||||
function _nextClaimId() {
|
||||
_claimSerial += 1;
|
||||
return claimPrefix + ":" + (new Date()).getTime() + ":" + _claimSerial + ":" + Math.floor(Math.random() * 1000000);
|
||||
}
|
||||
|
||||
function _isCurrent(name) {
|
||||
return !!name && !!isCurrentOwner && !!isCurrentOwner(name);
|
||||
}
|
||||
|
||||
function _hasOwner(name, ownerId) {
|
||||
return !!name && !!ownerId && !!hasOwner && !!hasOwner(name, ownerId);
|
||||
}
|
||||
|
||||
function _hasState(name, ownerId) {
|
||||
return !statePresent || !!statePresent(name, ownerId);
|
||||
}
|
||||
|
||||
function _shouldRecover() {
|
||||
return active && enabled && _isCurrent(screenName);
|
||||
}
|
||||
|
||||
function requestRecovery() {
|
||||
if (!_shouldRecover())
|
||||
return false;
|
||||
recoveryRequested();
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkOwnershipRecovery() {
|
||||
if (!_shouldRecover())
|
||||
return false;
|
||||
if (claimedScreenName === screenName && _hasOwner(screenName, claimId))
|
||||
return false;
|
||||
recoveryRequested();
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkStateRecovery() {
|
||||
if (!_shouldRecover())
|
||||
return false;
|
||||
if (claimedScreenName === screenName && _hasOwner(screenName, claimId) && _hasState(screenName, claimId))
|
||||
return false;
|
||||
recoveryRequested();
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkRecovery() {
|
||||
return checkStateRecovery();
|
||||
}
|
||||
|
||||
function beginClaim() {
|
||||
if (claimId && releaseDockRetract)
|
||||
releaseDockRetract(claimId);
|
||||
claimId = _nextClaimId();
|
||||
claimedScreenName = "";
|
||||
return claimId;
|
||||
}
|
||||
|
||||
function _syncDockRetract() {
|
||||
if (!claimId)
|
||||
return;
|
||||
if (dockBlocked && presented && dockSide && requestDockRetract)
|
||||
requestDockRetract(claimId, screenName, dockSide);
|
||||
else if (releaseDockRetract)
|
||||
releaseDockRetract(claimId);
|
||||
}
|
||||
|
||||
function publish(state, forceClaim) {
|
||||
if (!enabled || !screenName || !state) {
|
||||
release();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (claimedScreenName && claimedScreenName !== screenName)
|
||||
release();
|
||||
|
||||
const current = _isCurrent(screenName);
|
||||
let claiming = !!forceClaim || !claimId;
|
||||
if (claiming && !current)
|
||||
return false;
|
||||
if (!claimId)
|
||||
beginClaim();
|
||||
|
||||
let published = claiming ? claimState(screenName, state, claimId) : ensureState(screenName, state, claimId);
|
||||
if (!published && !claiming && current) {
|
||||
if (renewTokenOnRecovery) {
|
||||
beginClaim();
|
||||
} else if (releaseDockRetract) {
|
||||
releaseDockRetract(claimId);
|
||||
}
|
||||
published = claimState(screenName, state, claimId);
|
||||
}
|
||||
if (!published)
|
||||
return false;
|
||||
|
||||
claimedScreenName = screenName;
|
||||
_syncDockRetract();
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateAnim(animX, animY) {
|
||||
if (!enabled || !claimId || !claimedScreenName || !updateAnimationState)
|
||||
return false;
|
||||
if (!_hasOwner(claimedScreenName, claimId)) {
|
||||
requestRecovery();
|
||||
return false;
|
||||
}
|
||||
return updateAnimationState(claimedScreenName, claimId, animX, animY);
|
||||
}
|
||||
|
||||
function updateBody(bodyX, bodyY, bodyW, bodyH) {
|
||||
if (!enabled || !claimId || !claimedScreenName || !updateBodyState)
|
||||
return false;
|
||||
if (!_hasOwner(claimedScreenName, claimId)) {
|
||||
requestRecovery();
|
||||
return false;
|
||||
}
|
||||
return updateBodyState(claimedScreenName, claimId, bodyX, bodyY, bodyW, bodyH);
|
||||
}
|
||||
|
||||
function release() {
|
||||
if (!claimId) {
|
||||
claimedScreenName = "";
|
||||
return false;
|
||||
}
|
||||
|
||||
const releasedClaimId = claimId;
|
||||
const releasedScreenName = claimedScreenName;
|
||||
claimId = "";
|
||||
claimedScreenName = "";
|
||||
|
||||
if (releaseDockRetract)
|
||||
releaseDockRetract(releasedClaimId);
|
||||
if (releasedScreenName)
|
||||
return !!releaseState(releasedScreenName, releasedClaimId);
|
||||
return false;
|
||||
}
|
||||
|
||||
Component.onDestruction: release()
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import qs.Common
|
||||
|
||||
Item {
|
||||
@@ -19,7 +18,11 @@ Item {
|
||||
property real bottomRightRadius: targetRadius
|
||||
property color borderColor: "transparent"
|
||||
property real borderWidth: 0
|
||||
property bool useCustomSource: false
|
||||
|
||||
property real sourceX: 0
|
||||
property real sourceY: 0
|
||||
property real sourceWidth: width
|
||||
property real sourceHeight: height
|
||||
|
||||
property bool shadowEnabled: Theme.elevationEnabled
|
||||
property real shadowBlurPx: level && level.blurPx !== undefined ? level.blurPx : 0
|
||||
@@ -28,36 +31,24 @@ Item {
|
||||
property real shadowOffsetY: Theme.elevationOffsetYFor(level, direction, fallbackOffset)
|
||||
property color shadowColor: Theme.elevationShadowColor(level)
|
||||
property real shadowOpacity: 1
|
||||
property real blurMax: Theme.elevationBlurMax
|
||||
|
||||
property alias sourceRect: sourceRect
|
||||
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
|
||||
|
||||
layer.enabled: shadowEnabled
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
autoPaddingEnabled: true
|
||||
shadowEnabled: true
|
||||
blurEnabled: false
|
||||
maskEnabled: false
|
||||
shadowBlur: Math.max(0, Math.min(1, root.shadowBlurPx / Math.max(1, root.blurMax)))
|
||||
shadowScale: 1 + (2 * root.shadowSpreadPx) / Math.max(1, Math.min(root.width, root.height))
|
||||
shadowHorizontalOffset: root.shadowOffsetX
|
||||
shadowVerticalOffset: root.shadowOffsetY
|
||||
blurMax: root.blurMax
|
||||
shadowColor: root.shadowColor
|
||||
shadowOpacity: root.shadowOpacity
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: sourceRect
|
||||
ShaderEffect {
|
||||
anchors.fill: parent
|
||||
visible: !root.useCustomSource
|
||||
topLeftRadius: root.topLeftRadius
|
||||
topRightRadius: root.topRightRadius
|
||||
bottomLeftRadius: root.bottomLeftRadius
|
||||
bottomRightRadius: root.bottomRightRadius
|
||||
color: root.targetColor
|
||||
border.color: root.borderColor
|
||||
border.width: root.borderWidth
|
||||
anchors.margins: -root._pad
|
||||
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/elevation_rect.frag.qsb")
|
||||
|
||||
property real widthPx: width
|
||||
property real heightPx: height
|
||||
property real borderWidth: root.borderWidth
|
||||
property vector4d rectPx: Qt.vector4d(root._pad + root.sourceX, root._pad + root.sourceY, root.sourceWidth, root.sourceHeight)
|
||||
property vector4d cornerRadius: Qt.vector4d(root.topLeftRadius, root.topRightRadius, root.bottomRightRadius, root.bottomLeftRadius)
|
||||
property vector4d fillColor: Qt.vector4d(root.targetColor.r, root.targetColor.g, root.targetColor.b, root.targetColor.a)
|
||||
property vector4d borderColor: Qt.vector4d(root.borderColor.r, root.borderColor.g, root.borderColor.b, root.borderColor.a)
|
||||
property vector4d shadowColor: Qt.vector4d(root.shadowColor.r, root.shadowColor.g, root.shadowColor.b, root.shadowEnabled ? root.shadowColor.a * root.shadowOpacity : 0)
|
||||
property vector4d shadowParam: Qt.vector4d(Math.max(0, root.shadowBlurPx), root.shadowSpreadPx, root.shadowOffsetX, root.shadowOffsetY)
|
||||
property vector4d ambientParam: Qt.vector4d(root._ambient.blurPx, root._ambient.spreadPx, root.shadowEnabled ? root._ambient.alpha * root.shadowOpacity : 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Services
|
||||
|
||||
// Manages keyboard focus policy for popouts, modals, and Hyprland focus grabs
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function keyboardFocus(active, customFocus) {
|
||||
if (PopoutManager.screenshotActive)
|
||||
return WlrKeyboardFocus.None;
|
||||
if (customFocus !== null && customFocus !== undefined)
|
||||
return customFocus;
|
||||
if (!active)
|
||||
return WlrKeyboardFocus.None;
|
||||
if (CompositorService.useHyprlandFocusGrab)
|
||||
return WlrKeyboardFocus.OnDemand;
|
||||
return WlrKeyboardFocus.Exclusive;
|
||||
}
|
||||
|
||||
function wantsGrab(active, customFocus) {
|
||||
return CompositorService.useHyprlandFocusGrab && keyboardFocus(active, customFocus) === WlrKeyboardFocus.OnDemand;
|
||||
}
|
||||
|
||||
property list<var> barWindows: []
|
||||
|
||||
function registerBarWindow(window) {
|
||||
if (!window || barWindows.indexOf(window) !== -1)
|
||||
return;
|
||||
barWindows = barWindows.concat([window]);
|
||||
}
|
||||
|
||||
function unregisterBarWindow(window) {
|
||||
const idx = barWindows.indexOf(window);
|
||||
if (idx === -1)
|
||||
return;
|
||||
const next = barWindows.slice();
|
||||
next.splice(idx, 1);
|
||||
barWindows = next;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -16,8 +17,76 @@ Singleton {
|
||||
signal popoutOpening
|
||||
signal popoutChanged
|
||||
|
||||
property real hoverCursorGlobalX: 0
|
||||
property real hoverCursorGlobalY: 0
|
||||
|
||||
function updateHoverCursor(gx, gy) {
|
||||
hoverCursorGlobalX = gx;
|
||||
hoverCursorGlobalY = gy;
|
||||
}
|
||||
|
||||
function cursorOverBar(gx, gy, padding) {
|
||||
const pad = padding !== undefined ? padding : 16;
|
||||
const bars = KeyboardFocus.barWindows || [];
|
||||
for (let i = 0; i < bars.length; i++) {
|
||||
const w = bars[i];
|
||||
if (!w?.visible)
|
||||
continue;
|
||||
if (typeof w.containsGlobalPoint === "function") {
|
||||
if (w.containsGlobalPoint(gx, gy, pad))
|
||||
return true;
|
||||
continue;
|
||||
}
|
||||
const item = w.contentItem;
|
||||
if (!item || typeof item.mapToItem !== "function")
|
||||
continue;
|
||||
const topLeft = item.mapToItem(null, 0, 0);
|
||||
if (!topLeft)
|
||||
continue;
|
||||
if (gx >= topLeft.x - pad && gx < topLeft.x + item.width + pad && gy >= topLeft.y - pad && gy < topLeft.y + item.height + pad)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function _isPopoutPresented(popout) {
|
||||
if (!popout)
|
||||
return false;
|
||||
try {
|
||||
if (popout.dashVisible !== undefined)
|
||||
return !!popout.dashVisible;
|
||||
if (popout.notificationHistoryVisible !== undefined)
|
||||
return !!popout.notificationHistoryVisible;
|
||||
return !!(popout.shouldBeVisible || popout.isClosing);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function _openPopout(popout) {
|
||||
if (popout.dashVisible !== undefined) {
|
||||
if (popout.dashVisible && !popout.shouldBeVisible && !popout.isClosing)
|
||||
popout.dashVisible = false;
|
||||
popout.dashVisible = true;
|
||||
return;
|
||||
}
|
||||
if (popout.notificationHistoryVisible !== undefined) {
|
||||
popout.notificationHistoryVisible = true;
|
||||
return;
|
||||
}
|
||||
popout.open();
|
||||
}
|
||||
|
||||
function _closePopout(popout) {
|
||||
try {
|
||||
if (popout?.hoverDismissEnabled) {
|
||||
if (typeof popout.closeFromHoverDismiss === "function") {
|
||||
popout.closeFromHoverDismiss();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (popout.hoverDismissEnabled !== undefined)
|
||||
popout.hoverDismissEnabled = false;
|
||||
switch (true) {
|
||||
case popout.dashVisible !== undefined:
|
||||
popout.dashVisible = false;
|
||||
@@ -89,7 +158,26 @@ Singleton {
|
||||
continue;
|
||||
_closePopout(popout);
|
||||
}
|
||||
currentPopoutsByScreen = {};
|
||||
// Keep map entries until each popout's close animation finishes (hidePopout).
|
||||
}
|
||||
|
||||
function closePopoutForScreen(screen) {
|
||||
if (!screen)
|
||||
return;
|
||||
const screenName = screen.name;
|
||||
const popout = currentPopoutsByScreen[screenName];
|
||||
if (!popout || _isStale(popout)) {
|
||||
currentPopoutsByScreen[screenName] = null;
|
||||
currentPopoutTriggers[screenName] = null;
|
||||
return;
|
||||
}
|
||||
_closePopout(popout);
|
||||
}
|
||||
|
||||
function cancelHoverDismiss(screen) {
|
||||
const popout = getActivePopout(screen);
|
||||
if (popout?.cancelHoverDismiss)
|
||||
popout.cancelHoverDismiss();
|
||||
}
|
||||
|
||||
function getActivePopout(screen) {
|
||||
@@ -106,6 +194,8 @@ Singleton {
|
||||
function requestPopout(popout, tabIndex, triggerSource) {
|
||||
if (!popout || !popout.screen)
|
||||
return;
|
||||
if (popout.hoverDismissEnabled !== undefined)
|
||||
popout.hoverDismissEnabled = false;
|
||||
screenshotActive = false;
|
||||
const screenName = popout.screen.name;
|
||||
const currentPopout = currentPopoutsByScreen[screenName];
|
||||
@@ -181,16 +271,81 @@ Singleton {
|
||||
ModalManager.closeAllModalsExcept(null);
|
||||
}
|
||||
|
||||
if (movedFromOtherScreen) {
|
||||
popout.open();
|
||||
} else {
|
||||
if (popout.dashVisible !== undefined) {
|
||||
popout.dashVisible = true;
|
||||
} else if (popout.notificationHistoryVisible !== undefined) {
|
||||
popout.notificationHistoryVisible = true;
|
||||
_openPopout(popout);
|
||||
}
|
||||
|
||||
function requestHoverPopout(popout, tabIndex, triggerSource) {
|
||||
if (!popout || !popout.screen)
|
||||
return;
|
||||
screenshotActive = false;
|
||||
const screenName = popout.screen.name;
|
||||
const currentPopout = currentPopoutsByScreen[screenName];
|
||||
const triggerId = triggerSource !== undefined ? triggerSource : tabIndex;
|
||||
|
||||
const willOpen = !(currentPopout === popout && _isPopoutPresented(popout) && triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId);
|
||||
if (willOpen)
|
||||
popoutOpening();
|
||||
|
||||
let movedFromOtherScreen = false;
|
||||
for (const otherScreenName in currentPopoutsByScreen) {
|
||||
if (otherScreenName === screenName)
|
||||
continue;
|
||||
const otherPopout = currentPopoutsByScreen[otherScreenName];
|
||||
if (!otherPopout)
|
||||
continue;
|
||||
|
||||
if (_isStale(otherPopout)) {
|
||||
currentPopoutsByScreen[otherScreenName] = null;
|
||||
currentPopoutTriggers[otherScreenName] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (otherPopout === popout) {
|
||||
movedFromOtherScreen = true;
|
||||
currentPopoutsByScreen[otherScreenName] = null;
|
||||
currentPopoutTriggers[otherScreenName] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
_closePopout(otherPopout);
|
||||
}
|
||||
|
||||
if (currentPopout && currentPopout !== popout) {
|
||||
if (_isStale(currentPopout)) {
|
||||
currentPopoutsByScreen[screenName] = null;
|
||||
currentPopoutTriggers[screenName] = null;
|
||||
} else {
|
||||
popout.open();
|
||||
_closePopout(currentPopout);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentPopout === popout && _isPopoutPresented(popout) && !movedFromOtherScreen) {
|
||||
if (triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId)
|
||||
return;
|
||||
|
||||
if (tabIndex !== undefined && popout.currentTabIndex !== undefined)
|
||||
popout.currentTabIndex = tabIndex;
|
||||
if (popout.updateSurfacePosition)
|
||||
popout.updateSurfacePosition();
|
||||
currentPopoutTriggers[screenName] = triggerId;
|
||||
if (popout.hoverDismissEnabled !== undefined)
|
||||
popout.hoverDismissEnabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
currentPopoutTriggers[screenName] = triggerId;
|
||||
currentPopoutsByScreen[screenName] = popout;
|
||||
popoutChanged();
|
||||
|
||||
if (tabIndex !== undefined && popout.currentTabIndex !== undefined)
|
||||
popout.currentTabIndex = tabIndex;
|
||||
|
||||
if (currentPopout !== popout)
|
||||
ModalManager.closeAllModalsExcept(null);
|
||||
|
||||
if (popout.hoverDismissEnabled !== undefined)
|
||||
popout.hoverDismissEnabled = true;
|
||||
|
||||
_openPopout(popout);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +108,7 @@ Singleton {
|
||||
}
|
||||
|
||||
property bool clipboardEnterToPaste: false
|
||||
property var clipboardVisibleEntryActions: ["pin", "edit", "delete"]
|
||||
|
||||
property var launcherPluginVisibility: ({})
|
||||
|
||||
@@ -489,9 +490,6 @@ Singleton {
|
||||
"hideOnTouch": false,
|
||||
"inactiveTimeout": 0
|
||||
},
|
||||
"dwl": {
|
||||
"cursorHideTimeout": 0
|
||||
},
|
||||
"mango": {
|
||||
"cursorHideTimeout": 0
|
||||
}
|
||||
@@ -518,6 +516,8 @@ Singleton {
|
||||
property bool notepadUseMonospace: true
|
||||
property string notepadFontFamily: ""
|
||||
property real notepadFontSize: 14
|
||||
property real notificationSummaryFontSize: Spec.SPEC.notificationSummaryFontSize.def
|
||||
property real notificationBodyFontSize: Spec.SPEC.notificationBodyFontSize.def
|
||||
property bool notepadShowLineNumbers: false
|
||||
property real notepadTransparencyOverride: -1
|
||||
property real notepadLastCustomTransparency: 0.7
|
||||
@@ -698,6 +698,7 @@ Singleton {
|
||||
property int notificationTimeoutNormal: 5000
|
||||
property int notificationTimeoutCritical: 0
|
||||
property bool notificationCompactMode: false
|
||||
property bool notificationShowTimeoutBar: false
|
||||
property bool notificationDedupeEnabled: true
|
||||
property int notificationPopupPosition: SettingsData.Position.Top
|
||||
property int notificationAnimationSpeed: SettingsData.AnimationSpeed.Short
|
||||
@@ -809,7 +810,8 @@ Singleton {
|
||||
"shadowOpacity": 60,
|
||||
"shadowColorMode": "default",
|
||||
"shadowCustomColor": "#000000",
|
||||
"clickThrough": false
|
||||
"clickThrough": false,
|
||||
"hoverPopouts": false
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1224,8 +1226,6 @@ Singleton {
|
||||
NiriService.generateNiriLayoutConfig();
|
||||
if (CompositorService.isHyprland && typeof HyprlandService !== "undefined")
|
||||
HyprlandService.generateLayoutConfig();
|
||||
if (CompositorService.isDwl && typeof DwlService !== "undefined")
|
||||
DwlService.generateLayoutConfig();
|
||||
if (CompositorService.isMango && typeof MangoService !== "undefined")
|
||||
MangoService.generateLayoutConfig();
|
||||
}
|
||||
@@ -1652,6 +1652,15 @@ Singleton {
|
||||
};
|
||||
}
|
||||
|
||||
function effectiveBarConfigForRender(config, usesFrameBarChrome) {
|
||||
if (!config || !connectedFrameModeActive || usesFrameBarChrome)
|
||||
return config;
|
||||
const backup = connectedFrameBarStyleBackups[config.id];
|
||||
if (!backup)
|
||||
return config;
|
||||
return Object.assign({}, config, backup);
|
||||
}
|
||||
|
||||
// Single entry point for connected-mode settings state.
|
||||
// !active → restore backups
|
||||
function _reconcileConnectedFrameBarStyles() {
|
||||
@@ -2451,10 +2460,6 @@ Singleton {
|
||||
HyprlandService.generateCursorConfig();
|
||||
return;
|
||||
}
|
||||
if (CompositorService.isDwl && typeof DwlService !== "undefined") {
|
||||
DwlService.generateCursorConfig();
|
||||
return;
|
||||
}
|
||||
if (CompositorService.isMango && typeof MangoService !== "undefined") {
|
||||
MangoService.generateCursorConfig();
|
||||
return;
|
||||
|
||||
@@ -911,6 +911,16 @@ Singleton {
|
||||
}
|
||||
return Qt.rgba(r, g, b, alpha);
|
||||
}
|
||||
function elevationAmbient(level) {
|
||||
const blur = (level && level.blurPx !== undefined) ? Math.max(0, level.blurPx) : 0;
|
||||
const alpha = ((level && level.alpha !== undefined) ? level.alpha : 0.3) * 0.5;
|
||||
return {
|
||||
blurPx: blur * 1.75,
|
||||
spreadPx: 1,
|
||||
alpha: alpha
|
||||
};
|
||||
}
|
||||
|
||||
function elevationTintOpacity(level) {
|
||||
if (!level)
|
||||
return 0;
|
||||
|
||||
@@ -570,7 +570,7 @@ Singleton {
|
||||
onExited: exitCode => {
|
||||
const enabling = root.settingsRoot && root.settingsRoot.greeterAutoLogin;
|
||||
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;
|
||||
return;
|
||||
}
|
||||
@@ -645,7 +645,7 @@ Singleton {
|
||||
onExited: exitCode => {
|
||||
const err = (root.authApplySudoProbeStderr || "").trim();
|
||||
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;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -260,6 +260,8 @@ var SPEC = {
|
||||
notepadUseMonospace: { def: true },
|
||||
notepadFontFamily: { def: "" },
|
||||
notepadFontSize: { def: 14 },
|
||||
notificationSummaryFontSize: { def: 0 },
|
||||
notificationBodyFontSize: { def: 0 },
|
||||
notepadShowLineNumbers: { def: false },
|
||||
notepadTransparencyOverride: { def: -1 },
|
||||
notepadLastCustomTransparency: { def: 0.7 },
|
||||
@@ -406,6 +408,7 @@ var SPEC = {
|
||||
notificationTimeoutNormal: { def: 5000 },
|
||||
notificationTimeoutCritical: { def: 0 },
|
||||
notificationCompactMode: { def: false },
|
||||
notificationShowTimeoutBar: { def: false },
|
||||
notificationDedupeEnabled: { def: true },
|
||||
notificationPopupPosition: { def: 0 },
|
||||
notificationAnimationSpeed: { def: 1 },
|
||||
@@ -569,6 +572,7 @@ var SPEC = {
|
||||
|
||||
builtInPluginSettings: { def: {} },
|
||||
clipboardEnterToPaste: { def: false },
|
||||
clipboardVisibleEntryActions: { def: ["pin", "edit", "delete"] },
|
||||
|
||||
launcherPluginVisibility: { def: {} },
|
||||
launcherPluginOrder: { def: [] },
|
||||
|
||||
+2
-19
@@ -64,27 +64,15 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
property bool wallpaperSurfacesLoaded: true
|
||||
|
||||
Loader {
|
||||
id: blurredWallpaperBackgroundLoader
|
||||
active: root.wallpaperSurfacesLoaded && SettingsData.blurredWallpaperLayer && CompositorService.isNiri
|
||||
active: SettingsData.blurredWallpaperLayer && CompositorService.isNiri
|
||||
asynchronous: false
|
||||
|
||||
sourceComponent: BlurredWallpaperBackground {}
|
||||
}
|
||||
|
||||
DeferredAction {
|
||||
id: wallpaperSurfaceReloadAction
|
||||
onTriggered: root.wallpaperSurfacesLoaded = true
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: wallpaperBackgroundLoader
|
||||
active: root.wallpaperSurfacesLoaded
|
||||
asynchronous: false
|
||||
sourceComponent: WallpaperBackground {}
|
||||
}
|
||||
WallpaperBackground {}
|
||||
|
||||
DesktopWidgetLayer {}
|
||||
|
||||
@@ -398,11 +386,6 @@ Item {
|
||||
frameSurfaceReloadAction.schedule();
|
||||
}
|
||||
|
||||
if (root.wallpaperSurfacesLoaded) {
|
||||
root.wallpaperSurfacesLoaded = false;
|
||||
wallpaperSurfaceReloadAction.schedule();
|
||||
}
|
||||
|
||||
root.dockEnabled = false;
|
||||
Qt.callLater(() => {
|
||||
root.dockEnabled = true;
|
||||
|
||||
@@ -337,9 +337,6 @@ Item {
|
||||
const focusedWs = I3.workspaces.values.find(ws => ws.focused === true);
|
||||
return focusedWs?.monitor?.name || "";
|
||||
}
|
||||
if (CompositorService.isDwl && DwlService.activeOutput) {
|
||||
return DwlService.activeOutput;
|
||||
}
|
||||
if (CompositorService.isMango && MangoService.activeOutput) {
|
||||
return MangoService.activeOutput;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import QtQuick
|
||||
import Quickshell.Hyprland
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
@@ -11,11 +10,6 @@ DankModal {
|
||||
|
||||
layerNamespace: "dms:bluetooth-pairing"
|
||||
|
||||
HyprlandFocusGrab {
|
||||
windows: [root.contentWindow]
|
||||
active: root.useHyprlandFocusGrab && root.shouldHaveFocus
|
||||
}
|
||||
|
||||
property string deviceName: ""
|
||||
property string deviceAddress: ""
|
||||
property string requestType: ""
|
||||
|
||||
@@ -7,7 +7,6 @@ Item {
|
||||
id: clipboardContent
|
||||
|
||||
required property var modal
|
||||
required property var clearConfirmDialog
|
||||
|
||||
property alias searchField: searchField
|
||||
property alias clipboardListView: clipboardListView
|
||||
@@ -33,14 +32,7 @@ Item {
|
||||
pinnedCount: modal.pinnedCount
|
||||
onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints
|
||||
onTabChanged: tabName => modal.activeTab = tabName
|
||||
onClearAllClicked: {
|
||||
const hasPinned = modal.pinnedCount > 0;
|
||||
const message = hasPinned ? I18n.tr("This will delete all unpinned entries. %1 pinned entries will be kept.").arg(modal.pinnedCount) : I18n.tr("This will permanently delete all clipboard history.");
|
||||
clearConfirmDialog.show(I18n.tr("Clear History?"), message, function () {
|
||||
modal.clearAll();
|
||||
modal.hide();
|
||||
}, function () {});
|
||||
}
|
||||
onClearAllClicked: modal.confirmClearAll()
|
||||
onCloseClicked: modal.hide()
|
||||
}
|
||||
|
||||
@@ -128,7 +120,7 @@ Item {
|
||||
}
|
||||
|
||||
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
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
@@ -149,8 +141,8 @@ Item {
|
||||
listView: clipboardListView
|
||||
onCopyRequested: clipboardContent.modal.copyEntry(modelData)
|
||||
onDeleteRequested: clipboardContent.modal.deleteEntry(modelData)
|
||||
onPinRequested: clipboardContent.modal.pinEntry(modelData)
|
||||
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
|
||||
onPinRequested: targetEntry => clipboardContent.modal.pinEntry(targetEntry)
|
||||
onUnpinRequested: targetEntry => clipboardContent.modal.unpinEntry(targetEntry)
|
||||
onEditRequested: clipboardContent.modal.editEntry(modelData)
|
||||
}
|
||||
}
|
||||
@@ -202,7 +194,7 @@ Item {
|
||||
}
|
||||
|
||||
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
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
@@ -223,8 +215,8 @@ Item {
|
||||
listView: savedListView
|
||||
onCopyRequested: clipboardContent.modal.copyEntry(modelData)
|
||||
onDeleteRequested: clipboardContent.modal.deletePinnedEntry(modelData)
|
||||
onPinRequested: clipboardContent.modal.pinEntry(modelData)
|
||||
onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
|
||||
onPinRequested: targetEntry => clipboardContent.modal.pinEntry(targetEntry)
|
||||
onUnpinRequested: targetEntry => clipboardContent.modal.unpinEntry(targetEntry)
|
||||
onEditRequested: clipboardContent.modal.editEntry(modelData)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,13 +15,21 @@ Rectangle {
|
||||
|
||||
signal copyRequested
|
||||
signal deleteRequested
|
||||
signal pinRequested
|
||||
signal unpinRequested
|
||||
signal pinRequested(var targetEntry)
|
||||
signal unpinRequested(var targetEntry)
|
||||
signal editRequested
|
||||
|
||||
readonly property string entryType: modal ? modal.getEntryType(entry) : "text"
|
||||
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 hasPinnedDuplicate: pinnedDuplicateEntry !== null
|
||||
readonly property bool effectivePinned: entry.pinned || hasPinnedDuplicate
|
||||
readonly property var visibleEntryActions: SettingsData.clipboardVisibleEntryActions || ["pin", "edit", "delete"]
|
||||
readonly property bool showPinAction: visibleEntryActions.includes("pin")
|
||||
readonly property bool showEditAction: visibleEntryActions.includes("edit")
|
||||
readonly property bool showDeleteAction: visibleEntryActions.includes("delete")
|
||||
readonly property bool showPinnedIndicator: hasPinnedDuplicate && !showPinAction
|
||||
readonly property bool showAnyAction: showPinAction || showEditAction || showDeleteAction || showPinnedIndicator
|
||||
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
@@ -62,19 +70,46 @@ Rectangle {
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
visible: root.showAnyAction
|
||||
|
||||
Item {
|
||||
width: 40
|
||||
height: 40
|
||||
visible: root.showPinnedIndicator
|
||||
|
||||
// Status indicator only; the Pin action remains hidden.
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "push_pin"
|
||||
size: Theme.iconSize - 6
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "push_pin"
|
||||
iconSize: Theme.iconSize - 6
|
||||
iconColor: (entry.pinned || hasPinnedDuplicate) ? Theme.primary : Theme.surfaceText
|
||||
backgroundColor: (entry.pinned || hasPinnedDuplicate) ? Theme.primarySelected : "transparent"
|
||||
onClicked: entry.pinned ? unpinRequested() : pinRequested()
|
||||
visible: root.showPinAction
|
||||
onClicked: {
|
||||
if (entry.pinned) {
|
||||
unpinRequested(entry);
|
||||
return;
|
||||
}
|
||||
if (pinnedDuplicateEntry) {
|
||||
unpinRequested(pinnedDuplicateEntry);
|
||||
return;
|
||||
}
|
||||
pinRequested(entry);
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
iconName: "edit"
|
||||
iconSize: Theme.iconSize - 6
|
||||
iconColor: Theme.surfaceText
|
||||
visible: root.showEditAction
|
||||
|
||||
onClicked: {
|
||||
if (entryType === "image") {
|
||||
@@ -88,6 +123,7 @@ Rectangle {
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 6
|
||||
iconColor: Theme.surfaceText
|
||||
visible: root.showDeleteAction
|
||||
onClicked: deleteRequested()
|
||||
}
|
||||
}
|
||||
@@ -95,8 +131,8 @@ Rectangle {
|
||||
Item {
|
||||
anchors.left: indexBadge.right
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.right: actionButtons.left
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.right: root.showAnyAction ? actionButtons.left : parent.right
|
||||
anchors.rightMargin: root.showAnyAction ? Theme.spacingM : Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
// height: contentColumn.implicitHeight
|
||||
height: ClipboardConstants.itemHeight
|
||||
@@ -157,8 +193,8 @@ Rectangle {
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.left: parent.left
|
||||
anchors.right: actionButtons.left
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.right: root.showAnyAction ? actionButtons.left : parent.right
|
||||
anchors.rightMargin: root.showAnyAction ? Theme.spacingS : 0
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
hoverEnabled: true
|
||||
|
||||
@@ -82,6 +82,15 @@ FocusScope {
|
||||
ClipboardService.clearAll();
|
||||
}
|
||||
|
||||
function confirmClearAll() {
|
||||
const hasPinned = pinnedCount > 0;
|
||||
const message = hasPinned ? I18n.tr("This will delete all unpinned entries. %1 pinned entries will be kept.").arg(pinnedCount) : I18n.tr("This will permanently delete all clipboard history.");
|
||||
clearConfirmDialog.show(I18n.tr("Clear History?"), message, function () {
|
||||
clearAll();
|
||||
hide();
|
||||
}, function () {});
|
||||
}
|
||||
|
||||
function getEntryPreview(entry) {
|
||||
return ClipboardService.getEntryPreview(entry);
|
||||
}
|
||||
@@ -135,7 +144,6 @@ FocusScope {
|
||||
id: historyContent
|
||||
anchors.fill: parent
|
||||
modal: root
|
||||
clearConfirmDialog: root.clearConfirmDialog
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell.Hyprland
|
||||
import qs.Common
|
||||
import qs.Modals.Clipboard
|
||||
import qs.Modals.Common
|
||||
@@ -12,11 +11,6 @@ DankModal {
|
||||
|
||||
layerNamespace: "dms:clipboard"
|
||||
|
||||
HyprlandFocusGrab {
|
||||
windows: [clipboardHistoryModal.contentWindow]
|
||||
active: clipboardHistoryModal.useHyprlandFocusGrab && clipboardHistoryModal.shouldHaveFocus
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (shouldBeVisible) {
|
||||
hide();
|
||||
@@ -64,6 +58,7 @@ DankModal {
|
||||
readonly property bool clipboardAvailable: ClipboardService.clipboardAvailable
|
||||
|
||||
visible: false
|
||||
keepContentLoaded: true
|
||||
modalWidth: ClipboardConstants.modalWidth
|
||||
modalHeight: ClipboardConstants.modalHeight
|
||||
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
@@ -82,22 +77,35 @@ DankModal {
|
||||
id: clearConfirmDialog
|
||||
confirmButtonText: I18n.tr("Clear All")
|
||||
confirmButtonColor: Theme.primary
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
onShouldBeVisibleChanged: {
|
||||
if (shouldBeVisible) {
|
||||
clipboardHistoryModal.shouldHaveFocus = false;
|
||||
selectedButton = 0;
|
||||
keyboardNavigation = true;
|
||||
return;
|
||||
}
|
||||
Qt.callLater(function () {
|
||||
if (!clipboardHistoryModal.shouldBeVisible) {
|
||||
return;
|
||||
}
|
||||
clipboardHistoryModal.shouldHaveFocus = true;
|
||||
clipboardHistoryModal.shouldHaveFocus = Qt.binding(() => clipboardHistoryModal.shouldBeVisible);
|
||||
clipboardHistoryModal.modalFocusScope.forceActiveFocus();
|
||||
if (clipboardHistoryModal.contentLoader.item?.searchField) {
|
||||
clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
Connections {
|
||||
target: clearConfirmDialog.modalFocusScope.Keys
|
||||
function onPressed(event) {
|
||||
if (!clearConfirmDialog.shouldBeVisible || event.key !== Qt.Key_Backtab) {
|
||||
return;
|
||||
}
|
||||
clearConfirmDialog.selectedButton = clearConfirmDialog.selectedButton === -1 ? 1 : (clearConfirmDialog.selectedButton - 1 + 2) % 2;
|
||||
clearConfirmDialog.keyboardNavigation = true;
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content: Component {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Modals.Clipboard
|
||||
import qs.Modals.Common
|
||||
@@ -95,6 +96,35 @@ DankPopout {
|
||||
id: clearConfirmDialog
|
||||
confirmButtonText: I18n.tr("Clear All")
|
||||
confirmButtonColor: Theme.primary
|
||||
onShouldBeVisibleChanged: {
|
||||
if (shouldBeVisible) {
|
||||
root.customKeyboardFocus = WlrKeyboardFocus.None;
|
||||
selectedButton = 0;
|
||||
keyboardNavigation = true;
|
||||
return;
|
||||
}
|
||||
root.customKeyboardFocus = null;
|
||||
Qt.callLater(function () {
|
||||
if (!root.shouldBeVisible || !root.contentLoader.item) {
|
||||
return;
|
||||
}
|
||||
root.contentLoader.item.forceActiveFocus();
|
||||
if (root.contentLoader.item.searchField) {
|
||||
root.contentLoader.item.searchField.forceActiveFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
Connections {
|
||||
target: clearConfirmDialog.modalFocusScope.Keys
|
||||
function onPressed(event) {
|
||||
if (!clearConfirmDialog.shouldBeVisible || event.key !== Qt.Key_Backtab) {
|
||||
return;
|
||||
}
|
||||
clearConfirmDialog.selectedButton = clearConfirmDialog.selectedButton === -1 ? 1 : (clearConfirmDialog.selectedButton - 1 + 2) % 2;
|
||||
clearConfirmDialog.keyboardNavigation = true;
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content: Component {
|
||||
|
||||
@@ -59,8 +59,13 @@ QtObject {
|
||||
return;
|
||||
}
|
||||
const selectedEntry = entries[ClipboardService.selectedIndex];
|
||||
if (modal.activeTab === "saved") {
|
||||
if (selectedEntry.pinned) {
|
||||
modal.unpinEntry(selectedEntry);
|
||||
return;
|
||||
}
|
||||
const pinnedDuplicate = ClipboardService.getPinnedEntryByHash(selectedEntry.hash);
|
||||
if (pinnedDuplicate) {
|
||||
modal.unpinEntry(pinnedDuplicate);
|
||||
} else {
|
||||
modal.pinEntry(selectedEntry);
|
||||
}
|
||||
@@ -120,8 +125,6 @@ QtObject {
|
||||
if (!ClipboardService.keyboardNavigationActive) {
|
||||
ClipboardService.keyboardNavigationActive = true;
|
||||
ClipboardService.selectedIndex = 0;
|
||||
} else if (ClipboardService.selectedIndex === 0) {
|
||||
ClipboardService.keyboardNavigationActive = false;
|
||||
} else {
|
||||
selectPrevious();
|
||||
}
|
||||
@@ -150,8 +153,6 @@ QtObject {
|
||||
if (!ClipboardService.keyboardNavigationActive) {
|
||||
ClipboardService.keyboardNavigationActive = true;
|
||||
ClipboardService.selectedIndex = 0;
|
||||
} else if (ClipboardService.selectedIndex === 0) {
|
||||
ClipboardService.keyboardNavigationActive = false;
|
||||
} else {
|
||||
selectPrevious();
|
||||
}
|
||||
@@ -179,8 +180,7 @@ QtObject {
|
||||
if (event.modifiers & Qt.ShiftModifier) {
|
||||
switch (event.key) {
|
||||
case Qt.Key_Delete:
|
||||
modal.clearAll();
|
||||
modal.hide();
|
||||
modal.confirmClearAll();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_Return:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import QtQuick
|
||||
import Quickshell.Hyprland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
@@ -52,8 +53,13 @@ Item {
|
||||
focus: true
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
// Hyprland OnDemand grab delivers keyboard focus to the modal content surface.
|
||||
HyprlandFocusGrab {
|
||||
windows: root.contentWindow ? [root.contentWindow] : []
|
||||
active: KeyboardFocus.wantsGrab(root.shouldHaveFocus, root.customKeyboardFocus)
|
||||
}
|
||||
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 real screenWidth: impl.item ? impl.item.screenWidth : 1920
|
||||
readonly property real screenHeight: impl.item ? impl.item.screenHeight : 1080
|
||||
@@ -96,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() {
|
||||
if (_resolvedBackend === _desiredBackend)
|
||||
return;
|
||||
|
||||
@@ -31,7 +31,6 @@ Item {
|
||||
property bool closeOnBackgroundClick: true
|
||||
property string animationType: "scale"
|
||||
|
||||
// Opposite side from the launcher by default; subclasses may override
|
||||
property string preferredConnectedBarSide: SettingsData.frameModalEmergeSide
|
||||
|
||||
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 frozenMotionOffsetY: 0
|
||||
readonly property alias contentWindow: contentWindow
|
||||
readonly property alias clickCatcher: clickCatcher
|
||||
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
||||
readonly property bool useBackground: false
|
||||
readonly property bool useSingleWindow: CompositorService.isHyprland
|
||||
|
||||
signal opened
|
||||
signal dialogClosed
|
||||
signal backgroundClicked
|
||||
|
||||
// Coalesce per-channel dirty bits; one ConnectedModeState write per tick.
|
||||
Timer {
|
||||
id: _syncTimer
|
||||
interval: 0
|
||||
@@ -115,6 +111,7 @@ Item {
|
||||
id: modalChrome
|
||||
modalHandle: root.modalHandle
|
||||
claimPrefix: root.layerNamespace + ":modal"
|
||||
surfaceKind: "modal"
|
||||
screenName: root._currentScreenName()
|
||||
enabled: root.frameOwnsConnectedChrome
|
||||
active: root.shouldBeVisible
|
||||
@@ -125,17 +122,38 @@ Item {
|
||||
}
|
||||
|
||||
function _publishModalChromeState() {
|
||||
const presented = shouldBeVisible || contentWindow.visible;
|
||||
const phase = !presented ? "hidden" : (!shouldBeVisible && contentWindow.visible ? "closing" : (!contentWindow.visible ? "opening" : "open"));
|
||||
const bodyRect = {
|
||||
"x": alignedX,
|
||||
"y": alignedY,
|
||||
"width": alignedWidth,
|
||||
"height": alignedHeight
|
||||
};
|
||||
const animationOffset = {
|
||||
"x": modalContainer ? modalContainer.animX : 0,
|
||||
"y": modalContainer ? modalContainer.animY : 0
|
||||
};
|
||||
const state = {
|
||||
"visible": shouldBeVisible || contentWindow.visible,
|
||||
"kind": "modal",
|
||||
"screenName": root._currentScreenName(),
|
||||
"phase": phase,
|
||||
"visible": presented,
|
||||
"presented": presented,
|
||||
"barSide": resolvedConnectedBarSide,
|
||||
"bodyRect": bodyRect,
|
||||
"animationOffset": animationOffset,
|
||||
"scale": 1,
|
||||
"opacity": Theme.connectedSurfaceColor.a,
|
||||
"bodyX": alignedX,
|
||||
"bodyY": alignedY,
|
||||
"bodyW": alignedWidth,
|
||||
"bodyH": alignedHeight,
|
||||
"animX": modalContainer ? modalContainer.animX : 0,
|
||||
"animY": modalContainer ? modalContainer.animY : 0,
|
||||
"animX": animationOffset.x,
|
||||
"animY": animationOffset.y,
|
||||
"omitStartConnector": false,
|
||||
"omitEndConnector": false
|
||||
"omitEndConnector": false,
|
||||
"dockRetractSide": root._dockBlocksEmergence ? resolvedConnectedBarSide : ""
|
||||
};
|
||||
return modalChrome.publish(state);
|
||||
}
|
||||
@@ -222,22 +240,16 @@ Item {
|
||||
const focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen) {
|
||||
contentWindow.screen = focusedScreen;
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.screen = focusedScreen;
|
||||
}
|
||||
|
||||
ModalManager.openModal(modalHandle);
|
||||
if (Theme.isDirectionalEffect || root.useBackground) {
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.visible = true;
|
||||
contentWindow.visible = true;
|
||||
}
|
||||
|
||||
Qt.callLater(() => {
|
||||
animationsEnabled = true;
|
||||
shouldBeVisible = true;
|
||||
if (!useSingleWindow && !clickCatcher.visible)
|
||||
clickCatcher.visible = true;
|
||||
if (!contentWindow.visible)
|
||||
contentWindow.visible = true;
|
||||
opened();
|
||||
@@ -264,8 +276,6 @@ Item {
|
||||
ModalManager.closeModal(modalHandle);
|
||||
closeTimer.stop();
|
||||
contentWindow.visible = false;
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.visible = false;
|
||||
dialogClosed();
|
||||
Qt.callLater(() => animationsEnabled = true);
|
||||
}
|
||||
@@ -304,8 +314,6 @@ Item {
|
||||
const newScreen = CompositorService.getFocusedScreen();
|
||||
if (newScreen) {
|
||||
contentWindow.screen = newScreen;
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.screen = newScreen;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -317,29 +325,12 @@ Item {
|
||||
if (shouldBeVisible)
|
||||
return;
|
||||
contentWindow.visible = false;
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.visible = false;
|
||||
dialogClosed();
|
||||
}
|
||||
}
|
||||
|
||||
// shadowRenderPadding is zeroed when frame owns the chrome
|
||||
// Wayland then clips any content translating past
|
||||
readonly property var shadowLevel: Theme.elevationLevel3
|
||||
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 alignedHeight: Theme.px(modalHeight, dpr)
|
||||
|
||||
@@ -349,7 +340,6 @@ Item {
|
||||
return SettingsData.frameEdgeInsetForSide(effectiveScreen, side);
|
||||
}
|
||||
|
||||
// frameEdgeInsetForSide is the full inset; do not add frameBarSize
|
||||
readonly property real _connectedAlignedX: {
|
||||
switch (resolvedConnectedBarSide) {
|
||||
case "top":
|
||||
@@ -412,57 +402,6 @@ Item {
|
||||
}
|
||||
})(), 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 {
|
||||
id: contentWindow
|
||||
visible: false
|
||||
@@ -472,8 +411,8 @@ Item {
|
||||
targetWindow: contentWindow
|
||||
blurEnabled: root.effectiveBlurEnabled && !root.frameOwnsConnectedChrome
|
||||
readonly property real s: Math.min(1, modalContainer.scaleValue)
|
||||
blurX: 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)
|
||||
blurX: connectedReveal.x + modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, 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
|
||||
blurHeight: (root.shouldBeVisible && !root.frameOwnsConnectedChrome) ? modalContainer.height * s : 0
|
||||
blurRadius: root.effectiveCornerRadius
|
||||
@@ -487,36 +426,15 @@ Item {
|
||||
"error": true
|
||||
})
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: {
|
||||
if (customKeyboardFocus !== null)
|
||||
return customKeyboardFocus;
|
||||
if (!shouldHaveFocus)
|
||||
return WlrKeyboardFocus.None;
|
||||
if (root.useHyprlandFocusGrab)
|
||||
return WlrKeyboardFocus.OnDemand;
|
||||
return WlrKeyboardFocus.Exclusive;
|
||||
}
|
||||
WlrLayershell.keyboardFocus: KeyboardFocus.keyboardFocus(shouldHaveFocus, customKeyboardFocus)
|
||||
|
||||
anchors {
|
||||
left: true
|
||||
top: true
|
||||
right: root.useSingleWindow
|
||||
bottom: root.useSingleWindow
|
||||
right: true
|
||||
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: {
|
||||
if (visible)
|
||||
return;
|
||||
@@ -528,7 +446,7 @@ Item {
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible
|
||||
enabled: root.closeOnBackgroundClick && root.shouldBeVisible
|
||||
z: -2
|
||||
onClicked: root.backgroundClicked()
|
||||
}
|
||||
@@ -537,7 +455,7 @@ Item {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
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
|
||||
|
||||
Behavior on opacity {
|
||||
@@ -551,249 +469,256 @@ Item {
|
||||
}
|
||||
|
||||
Item {
|
||||
id: modalContainer
|
||||
x: (root.useSingleWindow ? root.alignedX : (root.alignedX - contentWindow.actualMarginLeft)) + Theme.snap(animX, root.dpr)
|
||||
y: (root.useSingleWindow ? root.alignedY : (root.alignedY - contentWindow.actualMarginTop)) + Theme.snap(animY, root.dpr)
|
||||
|
||||
id: connectedReveal
|
||||
// Clip to final footprint while frame-owned chrome grows from the bar edge.
|
||||
x: root.alignedX
|
||||
y: root.alignedY
|
||||
width: root.alignedWidth
|
||||
height: root.alignedHeight
|
||||
|
||||
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()
|
||||
clip: root.frameOwnsConnectedChrome
|
||||
|
||||
Item {
|
||||
id: contentContainer
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
clip: false
|
||||
id: modalContainer
|
||||
x: Theme.snap(animX, root.dpr)
|
||||
y: Theme.snap(animY, root.dpr)
|
||||
|
||||
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 {
|
||||
id: animatedContent
|
||||
anchors.fill: parent
|
||||
id: contentContainer
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
clip: false
|
||||
|
||||
property real publishedOpacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (root.shouldBeVisible ? 1 : 0)
|
||||
|
||||
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
|
||||
Item {
|
||||
id: animatedContent
|
||||
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
|
||||
|
||||
Item {
|
||||
id: directContentWrapper
|
||||
property real publishedOpacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (root.shouldBeVisible ? 1 : 0)
|
||||
|
||||
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
|
||||
visible: root.directContent !== null
|
||||
focus: true
|
||||
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
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.directContent) {
|
||||
root.directContent.parent = directContentWrapper;
|
||||
root.directContent.anchors.fill = directContentWrapper;
|
||||
Qt.callLater(() => root.directContent.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
Item {
|
||||
id: directContentWrapper
|
||||
anchors.fill: parent
|
||||
visible: root.directContent !== null
|
||||
focus: true
|
||||
clip: false
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onDirectContentChanged() {
|
||||
Component.onCompleted: {
|
||||
if (root.directContent) {
|
||||
root.directContent.parent = directContentWrapper;
|
||||
root.directContent.anchors.fill = directContentWrapper;
|
||||
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 {
|
||||
id: contentLoader
|
||||
anchors.fill: parent
|
||||
active: root.directContent === null && (root.keepContentLoaded || root.shouldBeVisible || contentWindow.visible)
|
||||
asynchronous: false
|
||||
focus: true
|
||||
clip: false
|
||||
visible: root.directContent === null
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.fill: parent
|
||||
active: root.directContent === null && (root.keepContentLoaded || root.shouldBeVisible || contentWindow.visible)
|
||||
asynchronous: false
|
||||
focus: true
|
||||
clip: false
|
||||
visible: root.directContent === null
|
||||
|
||||
onLoaded: {
|
||||
if (item) {
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
onLoaded: {
|
||||
if (item) {
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,6 +205,7 @@ Item {
|
||||
id: clickCatcher
|
||||
visible: false
|
||||
color: "transparent"
|
||||
updatesEnabled: false
|
||||
|
||||
WlrLayershell.namespace: root.layerNamespace + ":clickcatcher"
|
||||
WlrLayershell.layer: WlrLayershell.Top
|
||||
@@ -259,15 +260,7 @@ Item {
|
||||
"error": true
|
||||
})
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: {
|
||||
if (customKeyboardFocus !== null)
|
||||
return customKeyboardFocus;
|
||||
if (!shouldHaveFocus)
|
||||
return WlrKeyboardFocus.None;
|
||||
if (root.useHyprlandFocusGrab)
|
||||
return WlrKeyboardFocus.OnDemand;
|
||||
return WlrKeyboardFocus.Exclusive;
|
||||
}
|
||||
WlrLayershell.keyboardFocus: KeyboardFocus.keyboardFocus(shouldHaveFocus, customKeyboardFocus)
|
||||
|
||||
anchors {
|
||||
left: true
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
@@ -13,11 +12,6 @@ DankModal {
|
||||
|
||||
layerNamespace: "dms:color-picker"
|
||||
|
||||
HyprlandFocusGrab {
|
||||
windows: [root.contentWindow]
|
||||
active: root.useHyprlandFocusGrab && root.shouldHaveFocus
|
||||
}
|
||||
|
||||
property string pickerTitle: I18n.tr("Choose Color")
|
||||
property color selectedColor: SessionData.recentColors.length > 0 ? SessionData.recentColors[0] : Theme.primary
|
||||
property var onColorSelectedCallback: null
|
||||
|
||||
@@ -30,7 +30,6 @@ Item {
|
||||
property string _pendingMode: ""
|
||||
readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose
|
||||
|
||||
// Animation state — matches DankPopout/DankModal pattern
|
||||
property bool animationsEnabled: true
|
||||
property bool _motionActive: false
|
||||
property real _frozenMotionX: 0
|
||||
@@ -108,8 +107,6 @@ Item {
|
||||
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: {
|
||||
const fallback = {
|
||||
"x": (screenWidth - modalWidth) / 2,
|
||||
@@ -175,8 +172,6 @@ Item {
|
||||
readonly property int effectiveBorderWidth: connectedSurfaceOverride ? 0 : borderWidth
|
||||
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 real shadowFallbackOffset: 6
|
||||
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
|
||||
|
||||
// For directional/depth: window extends from screen top (content slides within)
|
||||
// For standard: small window tightly around the modal + shadow padding
|
||||
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)
|
||||
readonly property real _ccX: _connectedChromeX
|
||||
readonly property real _ccY: _connectedChromeY
|
||||
|
||||
signal dialogClosed
|
||||
|
||||
// Coalesce per-channel dirty bits; one ConnectedModeState write per tick.
|
||||
Timer {
|
||||
id: _syncTimer
|
||||
interval: 0
|
||||
@@ -242,6 +219,7 @@ Item {
|
||||
id: modalChrome
|
||||
modalHandle: root.modalHandle
|
||||
claimPrefix: "dms:launcher-v2"
|
||||
surfaceKind: "launcher"
|
||||
screenName: root._currentScreenName()
|
||||
enabled: root.frameOwnsConnectedChrome
|
||||
active: root.spotlightOpen
|
||||
@@ -252,17 +230,38 @@ Item {
|
||||
}
|
||||
|
||||
function _publishModalChromeState() {
|
||||
const presented = spotlightOpen || contentWindow.visible;
|
||||
const phase = !presented ? "hidden" : (isClosing ? "closing" : (!contentWindow.visible ? "opening" : "open"));
|
||||
const bodyRect = {
|
||||
"x": _connectedChromeX,
|
||||
"y": _connectedChromeY,
|
||||
"width": _connectedChromeWidth,
|
||||
"height": _connectedChromeHeight
|
||||
};
|
||||
const animationOffset = {
|
||||
"x": contentContainer ? contentContainer.animX : 0,
|
||||
"y": contentContainer ? contentContainer.animY : 0
|
||||
};
|
||||
const state = {
|
||||
"visible": spotlightOpen || contentWindow.visible,
|
||||
"kind": "launcher",
|
||||
"screenName": root._currentScreenName(),
|
||||
"phase": phase,
|
||||
"visible": presented,
|
||||
"presented": presented,
|
||||
"barSide": resolvedConnectedBarSide,
|
||||
"bodyRect": bodyRect,
|
||||
"animationOffset": animationOffset,
|
||||
"scale": 1,
|
||||
"opacity": Theme.connectedSurfaceColor.a,
|
||||
"bodyX": _connectedChromeX,
|
||||
"bodyY": _connectedChromeY,
|
||||
"bodyW": _connectedChromeWidth,
|
||||
"bodyH": _connectedChromeHeight,
|
||||
"animX": contentContainer ? contentContainer.animX : 0,
|
||||
"animY": contentContainer ? contentContainer.animY : 0,
|
||||
"animX": animationOffset.x,
|
||||
"animY": animationOffset.y,
|
||||
"omitStartConnector": false,
|
||||
"omitEndConnector": false
|
||||
"omitEndConnector": false,
|
||||
"dockRetractSide": root._dockBlocksEmergence ? resolvedConnectedBarSide : ""
|
||||
};
|
||||
return modalChrome.publish(state);
|
||||
}
|
||||
@@ -359,8 +358,6 @@ Item {
|
||||
return;
|
||||
contentVisible = true;
|
||||
spotlightContent.closeTransientUi?.();
|
||||
// NOTE: forceActiveFocus() is deliberately NOT called here.
|
||||
// It is deferred to after animation starts to avoid compositor IPC stalls.
|
||||
|
||||
if (spotlightContent.searchField) {
|
||||
spotlightContent.searchField.text = query;
|
||||
@@ -398,40 +395,29 @@ Item {
|
||||
isClosing = false;
|
||||
openedFromOverview = false;
|
||||
|
||||
// Disable animations so the snap is instant
|
||||
animationsEnabled = false;
|
||||
|
||||
// Freeze the collapsed offsets (they depend on height which could change)
|
||||
_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);
|
||||
|
||||
var focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen) {
|
||||
backgroundWindow.screen = focusedScreen;
|
||||
contentWindow.screen = focusedScreen;
|
||||
}
|
||||
|
||||
// _motionActive = false ensures motionX/Y snap to frozen collapsed position
|
||||
_motionActive = false;
|
||||
|
||||
// Make windows visible but do NOT request keyboard focus yet
|
||||
ModalManager.openModal(modalHandle);
|
||||
spotlightOpen = true;
|
||||
backgroundWindow.visible = true;
|
||||
contentWindow.visible = true;
|
||||
if (useHyprlandFocusGrab)
|
||||
focusGrab.active = true;
|
||||
|
||||
// Load content and initialize (but no forceActiveFocus — that's deferred)
|
||||
_ensureContentLoadedAndInitialize(query || "", mode || "");
|
||||
|
||||
// Frame 1: enable animations and trigger enter motion
|
||||
// Defer focus until after enter motion starts (avoids compositor IPC stalls).
|
||||
Qt.callLater(() => {
|
||||
root.animationsEnabled = true;
|
||||
root._motionActive = true;
|
||||
|
||||
// Frame 2: request keyboard focus + activate search field
|
||||
// Double-deferred to avoid compositor IPC competing with animation frames
|
||||
Qt.callLater(() => {
|
||||
root.keyboardActive = true;
|
||||
if (root.spotlightContent && root.spotlightContent.searchField)
|
||||
@@ -454,16 +440,13 @@ Item {
|
||||
spotlightContent?.closeTransientUi?.();
|
||||
openedFromOverview = false;
|
||||
isClosing = true;
|
||||
// For directional effects, defer contentVisible=false so content stays rendered during exit slide
|
||||
if (!Theme.isDirectionalEffect)
|
||||
contentVisible = false;
|
||||
|
||||
// Trigger exit animation — Behaviors will animate motionX/Y to frozen collapsed position
|
||||
_motionActive = false;
|
||||
|
||||
keyboardActive = false;
|
||||
spotlightOpen = false;
|
||||
focusGrab.active = false;
|
||||
ModalManager.closeModal(modalHandle);
|
||||
closeCleanupTimer.start();
|
||||
}
|
||||
@@ -500,7 +483,6 @@ Item {
|
||||
isClosing = false;
|
||||
contentVisible = false;
|
||||
contentWindow.visible = false;
|
||||
backgroundWindow.visible = false;
|
||||
if (root.unloadContentOnClose)
|
||||
launcherContentLoader.active = false;
|
||||
dialogClosed();
|
||||
@@ -519,7 +501,7 @@ Item {
|
||||
HyprlandFocusGrab {
|
||||
id: focusGrab
|
||||
windows: [contentWindow]
|
||||
active: false
|
||||
active: root.useHyprlandFocusGrab && root.spotlightOpen
|
||||
|
||||
onCleared: {
|
||||
if (spotlightOpen) {
|
||||
@@ -569,7 +551,6 @@ Item {
|
||||
|
||||
root._releaseModalChrome();
|
||||
root._windowEnabled = false;
|
||||
backgroundWindow.screen = newScreen;
|
||||
contentWindow.screen = newScreen;
|
||||
Qt.callLater(() => {
|
||||
root._windowEnabled = true;
|
||||
@@ -577,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 {
|
||||
id: contentWindow
|
||||
visible: false
|
||||
@@ -663,23 +577,31 @@ Item {
|
||||
WlrLayershell.namespace: "dms:spotlight"
|
||||
WlrLayershell.layer: root.effectiveLauncherLayer
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: PopoutManager.screenshotActive ? WlrKeyboardFocus.None : (keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None)
|
||||
WlrLayershell.keyboardFocus: KeyboardFocus.keyboardFocus(keyboardActive, null)
|
||||
|
||||
anchors {
|
||||
left: true
|
||||
top: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
WlrLayershell.margins {
|
||||
left: root._cwMarginLeft
|
||||
top: root._cwMarginTop
|
||||
}
|
||||
|
||||
implicitWidth: root._cwWidth
|
||||
implicitHeight: root._cwHeight
|
||||
|
||||
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 {
|
||||
@@ -691,16 +613,31 @@ Item {
|
||||
height: root.contentSurfaceHeight
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: dismissArea
|
||||
enabled: root.spotlightOpen
|
||||
z: -2
|
||||
onClicked: root.hide()
|
||||
}
|
||||
|
||||
Item {
|
||||
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
|
||||
y: root._ccY
|
||||
width: root.alignedWidth
|
||||
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 bool dockTop: dockEdge === 0
|
||||
readonly property bool dockBottom: dockEdge === 1
|
||||
@@ -755,7 +692,6 @@ Item {
|
||||
return -Math.max((root.shadowPad || 0) + Theme.effectAnimOffset, 40);
|
||||
}
|
||||
|
||||
// openProgress: 0 = closed (at frozenMotion, scaleCollapsed), 1 = open (at 0, scale 1).
|
||||
QtObject {
|
||||
id: morph
|
||||
property real openProgress: root._motionActive ? 1 : 0
|
||||
@@ -814,7 +750,6 @@ Item {
|
||||
width: contentContainer.width
|
||||
height: contentContainer.height
|
||||
|
||||
// Shadow mirrors contentWrapper position/scale/opacity
|
||||
ElevationShadow {
|
||||
id: launcherShadowLayer
|
||||
width: parent.width
|
||||
@@ -832,7 +767,6 @@ Item {
|
||||
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 {
|
||||
id: contentWrapper
|
||||
width: parent.width
|
||||
|
||||
@@ -84,14 +84,14 @@ Item {
|
||||
readonly property real alignedX: Theme.snap(modalX, 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 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 contentX: Theme.snap(alignedX - windowX, 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 windowWidth: alignedWidth + contentX + shadowPad
|
||||
readonly property real windowHeight: _animatedContentH + contentY + shadowPad + _animHeadroom
|
||||
|
||||
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 bool useSingleWindow: CompositorService.isHyprland || useBackgroundDarken
|
||||
|
||||
signal dialogClosed
|
||||
|
||||
@@ -164,8 +165,6 @@ Item {
|
||||
openedFromOverview = false;
|
||||
keyboardActive = true;
|
||||
ModalManager.openModal(modalHandle);
|
||||
if (useHyprlandFocusGrab)
|
||||
focusGrab.active = true;
|
||||
_ensureContentLoadedAndInitialize(query || "", mode || "");
|
||||
}
|
||||
|
||||
@@ -201,7 +200,6 @@ Item {
|
||||
contentVisible = false;
|
||||
keyboardActive = false;
|
||||
spotlightOpen = false;
|
||||
focusGrab.active = false;
|
||||
ModalManager.closeModal(modalHandle);
|
||||
closeCleanupTimer.start();
|
||||
}
|
||||
@@ -231,7 +229,7 @@ Item {
|
||||
HyprlandFocusGrab {
|
||||
id: focusGrab
|
||||
windows: [launcherWindow]
|
||||
active: false
|
||||
active: root.useHyprlandFocusGrab && root.keyboardActive
|
||||
onCleared: {
|
||||
if (spotlightOpen)
|
||||
hide();
|
||||
@@ -270,8 +268,9 @@ Item {
|
||||
PanelWindow {
|
||||
id: clickCatcher
|
||||
screen: launcherWindow.screen
|
||||
visible: (spotlightOpen || isClosing) && !root.useBackgroundDarken
|
||||
visible: (spotlightOpen || isClosing) && !root.useSingleWindow
|
||||
color: "transparent"
|
||||
updatesEnabled: false
|
||||
|
||||
WlrLayershell.namespace: "dms:spotlight:clickcatcher"
|
||||
WlrLayershell.layer: root.effectiveLauncherLayer
|
||||
@@ -337,24 +336,24 @@ Item {
|
||||
WlrLayershell.namespace: "dms:spotlight"
|
||||
WlrLayershell.layer: root.effectiveLauncherLayer
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: PopoutManager.screenshotActive ? WlrKeyboardFocus.None : (keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None)
|
||||
WlrLayershell.keyboardFocus: KeyboardFocus.keyboardFocus(keyboardActive, null)
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: root.useBackgroundDarken
|
||||
bottom: root.useBackgroundDarken
|
||||
right: root.useSingleWindow
|
||||
bottom: root.useSingleWindow
|
||||
}
|
||||
|
||||
WlrLayershell.margins {
|
||||
left: root.useBackgroundDarken ? 0 : root.windowX
|
||||
top: root.useBackgroundDarken ? 0 : root.windowY
|
||||
left: root.useSingleWindow ? 0 : root.windowX
|
||||
top: root.useSingleWindow ? 0 : root.windowY
|
||||
right: 0
|
||||
bottom: 0
|
||||
}
|
||||
|
||||
implicitWidth: root.useBackgroundDarken ? 0 : root.windowWidth
|
||||
implicitHeight: root.useBackgroundDarken ? 0 : root.windowHeight
|
||||
implicitWidth: root.useSingleWindow ? 0 : root.windowWidth
|
||||
implicitHeight: root.useSingleWindow ? 0 : root.windowHeight
|
||||
|
||||
mask: Region {
|
||||
item: inputMask
|
||||
@@ -364,15 +363,15 @@ Item {
|
||||
id: inputMask
|
||||
visible: false
|
||||
color: "transparent"
|
||||
x: root.useBackgroundDarken ? 0 : modalContainer.x
|
||||
y: root.useBackgroundDarken ? 0 : modalContainer.y + modalContainer.slideOffset
|
||||
width: root.useBackgroundDarken ? launcherWindow.width : root.alignedWidth
|
||||
height: root.useBackgroundDarken ? launcherWindow.height : root._contentImplicitH
|
||||
x: root.useSingleWindow ? 0 : modalContainer.x
|
||||
y: root.useSingleWindow ? 0 : modalContainer.y + modalContainer.slideOffset
|
||||
width: root.useSingleWindow ? launcherWindow.width : root.alignedWidth
|
||||
height: root.useSingleWindow ? launcherWindow.height : root._contentImplicitH
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.useBackgroundDarken && spotlightOpen
|
||||
enabled: root.useSingleWindow && spotlightOpen
|
||||
z: -2
|
||||
onClicked: root.hide()
|
||||
}
|
||||
@@ -396,13 +395,23 @@ Item {
|
||||
|
||||
Item {
|
||||
id: modalContainer
|
||||
x: root.useBackgroundDarken ? root.alignedX : root.contentX
|
||||
y: root.useBackgroundDarken ? root.alignedY : root.contentY
|
||||
x: root.useSingleWindow ? root.alignedX : root.contentX
|
||||
y: root.useSingleWindow ? root.alignedY : root.contentY
|
||||
width: root.alignedWidth
|
||||
height: root._animatedContentH
|
||||
visible: _renderActive
|
||||
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 real slideOffset: contentVisible ? 0 : -root._animHeadroom
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ Item {
|
||||
|
||||
readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
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 var effectiveLauncherLayer: LayerShell.fromEnv("DMS_MODAL_LAYER", root.usesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top, {
|
||||
"allow": ["top", "overlay"],
|
||||
@@ -172,8 +173,6 @@ Item {
|
||||
|
||||
keyboardActive = true;
|
||||
ModalManager.openModal(modalHandle);
|
||||
if (useHyprlandFocusGrab)
|
||||
focusGrab.active = true;
|
||||
|
||||
_ensureContentLoadedAndInitialize(query || "", mode || "");
|
||||
}
|
||||
@@ -211,7 +210,6 @@ Item {
|
||||
|
||||
keyboardActive = false;
|
||||
spotlightOpen = false;
|
||||
focusGrab.active = false;
|
||||
ModalManager.closeModal(modalHandle);
|
||||
|
||||
closeCleanupTimer.start();
|
||||
@@ -262,7 +260,7 @@ Item {
|
||||
HyprlandFocusGrab {
|
||||
id: focusGrab
|
||||
windows: [launcherWindow]
|
||||
active: false
|
||||
active: root.useHyprlandFocusGrab && root.keyboardActive
|
||||
|
||||
onCleared: {
|
||||
if (spotlightOpen) {
|
||||
@@ -306,8 +304,9 @@ Item {
|
||||
PanelWindow {
|
||||
id: clickCatcher
|
||||
screen: launcherWindow.screen
|
||||
visible: (spotlightOpen || isClosing) && !root.useBackgroundDarken
|
||||
visible: (spotlightOpen || isClosing) && !root.useSingleWindow
|
||||
color: "transparent"
|
||||
updatesEnabled: false
|
||||
|
||||
WlrLayershell.namespace: "dms:spotlight:clickcatcher"
|
||||
WlrLayershell.layer: root.effectiveLauncherLayer
|
||||
@@ -373,24 +372,24 @@ Item {
|
||||
WlrLayershell.namespace: "dms:spotlight"
|
||||
WlrLayershell.layer: root.effectiveLauncherLayer
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: PopoutManager.screenshotActive ? WlrKeyboardFocus.None : (keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None)
|
||||
WlrLayershell.keyboardFocus: KeyboardFocus.keyboardFocus(keyboardActive, null)
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: root.useBackgroundDarken
|
||||
bottom: root.useBackgroundDarken
|
||||
right: root.useSingleWindow
|
||||
bottom: root.useSingleWindow
|
||||
}
|
||||
|
||||
WlrLayershell.margins {
|
||||
left: root.useBackgroundDarken ? 0 : root.windowX
|
||||
top: root.useBackgroundDarken ? 0 : root.windowY
|
||||
left: root.useSingleWindow ? 0 : root.windowX
|
||||
top: root.useSingleWindow ? 0 : root.windowY
|
||||
right: 0
|
||||
bottom: 0
|
||||
}
|
||||
|
||||
implicitWidth: root.useBackgroundDarken ? 0 : root.windowWidth
|
||||
implicitHeight: root.useBackgroundDarken ? 0 : root.windowHeight
|
||||
implicitWidth: root.useSingleWindow ? 0 : root.windowWidth
|
||||
implicitHeight: root.useSingleWindow ? 0 : root.windowHeight
|
||||
|
||||
mask: Region {
|
||||
item: launcherInputMask
|
||||
@@ -400,15 +399,15 @@ Item {
|
||||
id: launcherInputMask
|
||||
visible: false
|
||||
color: "transparent"
|
||||
x: root.useBackgroundDarken ? 0 : modalContainer.x
|
||||
y: root.useBackgroundDarken ? 0 : modalContainer.y
|
||||
width: root.useBackgroundDarken ? launcherWindow.width : modalContainer.width
|
||||
height: root.useBackgroundDarken ? launcherWindow.height : modalContainer.height
|
||||
x: root.useSingleWindow ? 0 : modalContainer.x
|
||||
y: root.useSingleWindow ? 0 : modalContainer.y
|
||||
width: root.useSingleWindow ? launcherWindow.width : modalContainer.width
|
||||
height: root.useSingleWindow ? launcherWindow.height : modalContainer.height
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.useBackgroundDarken && spotlightOpen
|
||||
enabled: root.useSingleWindow && spotlightOpen
|
||||
z: -2
|
||||
onClicked: root.hide()
|
||||
}
|
||||
@@ -432,13 +431,23 @@ Item {
|
||||
|
||||
Item {
|
||||
id: modalContainer
|
||||
x: root.useBackgroundDarken ? root.alignedX : root.contentX
|
||||
y: root.useBackgroundDarken ? root.alignedY : root.contentY
|
||||
x: root.useSingleWindow ? root.alignedX : root.contentX
|
||||
y: root.useSingleWindow ? root.alignedY : root.contentY
|
||||
width: root.alignedWidth
|
||||
height: root.alignedHeight
|
||||
visible: _renderActive
|
||||
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 real publishedScale: contentVisible ? 1 : 0.96
|
||||
property real publishedOpacity: contentVisible ? 1 : 0
|
||||
|
||||
@@ -320,8 +320,6 @@ Item {
|
||||
url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings";
|
||||
else if (CompositorService.isHyprland)
|
||||
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)
|
||||
url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings-2";
|
||||
Qt.openUrlExternally(url);
|
||||
|
||||
@@ -130,7 +130,7 @@ Item {
|
||||
title: I18n.tr("Multi-Monitor", "greeter feature card title")
|
||||
description: I18n.tr("Per-screen config", "greeter feature card description")
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Hyprland
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
@@ -29,11 +28,6 @@ DankModal {
|
||||
KeybindsService.loadCheatsheet();
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
windows: [root.contentWindow]
|
||||
active: root.useHyprlandFocusGrab && root.shouldHaveFocus
|
||||
}
|
||||
|
||||
function scrollDown() {
|
||||
if (!root.activeFlickable)
|
||||
return;
|
||||
|
||||
@@ -3,7 +3,6 @@ pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Io
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
@@ -45,12 +44,6 @@ DankModal {
|
||||
}
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: grab
|
||||
windows: [muxModal.contentWindow]
|
||||
active: CompositorService.isHyprland && muxModal.shouldHaveFocus
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (shouldBeVisible) {
|
||||
hide();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import QtQuick
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
@@ -11,11 +10,6 @@ DankModal {
|
||||
|
||||
layerNamespace: "dms:notification-center-modal"
|
||||
|
||||
HyprlandFocusGrab {
|
||||
windows: [notificationModal.contentWindow]
|
||||
active: notificationModal.useHyprlandFocusGrab && notificationModal.shouldHaveFocus
|
||||
}
|
||||
|
||||
property bool notificationModalOpen: false
|
||||
property var notificationListRef: null
|
||||
property var historyListRef: null
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
@@ -13,11 +12,6 @@ DankModal {
|
||||
layerNamespace: "dms:power-menu"
|
||||
keepPopoutsOpen: true
|
||||
|
||||
HyprlandFocusGrab {
|
||||
windows: [root.contentWindow]
|
||||
active: root.useHyprlandFocusGrab && root.shouldHaveFocus
|
||||
}
|
||||
|
||||
property int selectedIndex: 0
|
||||
property int selectedRow: 0
|
||||
property int selectedCol: 0
|
||||
|
||||
@@ -98,7 +98,7 @@ FocusScope {
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: CompositorTab {}
|
||||
sourceComponent: WorkspacesTab {}
|
||||
|
||||
onActiveChanged: {
|
||||
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 {
|
||||
id: dankBarAppearanceLoader
|
||||
anchors.fill: parent
|
||||
@@ -388,7 +426,7 @@ FocusScope {
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
Loader {
|
||||
id: defaultAppsLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 34
|
||||
@@ -474,12 +512,9 @@ FocusScope {
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
DankSpinner {
|
||||
anchors.centerIn: parent
|
||||
visible: root.currentIndex === 22 && widgetsLoader.status === Loader.Loading
|
||||
text: I18n.tr("Loading...", "loading indicator")
|
||||
color: Theme.surfaceVariantText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
}
|
||||
|
||||
Loader {
|
||||
|
||||
@@ -102,6 +102,13 @@ Rectangle {
|
||||
"icon": "volume_up",
|
||||
"tabIndex": 15,
|
||||
"soundsOnly": true
|
||||
},
|
||||
{
|
||||
"id": "compositor_layout",
|
||||
"text": CompositorService.isNiri ? "Niri" : (CompositorService.isHyprland ? "Hyprland" : "MangoWC"),
|
||||
"icon": "layers",
|
||||
"tabIndex": 37,
|
||||
"layoutCapable": true
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -110,24 +117,30 @@ Rectangle {
|
||||
"text": I18n.tr("Dank Bar"),
|
||||
"icon": "toolbar",
|
||||
"children": [
|
||||
{
|
||||
"id": "dankbar_settings",
|
||||
"text": I18n.tr("Settings"),
|
||||
"icon": "tune",
|
||||
"tabIndex": 3
|
||||
},
|
||||
{
|
||||
"id": "dankbar_appearance",
|
||||
"text": I18n.tr("Appearance"),
|
||||
"icon": "palette",
|
||||
"tabIndex": 6
|
||||
},
|
||||
{
|
||||
"id": "dankbar_settings",
|
||||
"text": I18n.tr("Settings"),
|
||||
"icon": "tune",
|
||||
"tabIndex": 3
|
||||
},
|
||||
{
|
||||
"id": "dankbar_widgets",
|
||||
"text": I18n.tr("Widgets"),
|
||||
"icon": "widgets",
|
||||
"tabIndex": 22
|
||||
},
|
||||
{
|
||||
"id": "workspaces",
|
||||
"text": I18n.tr("Workspaces"),
|
||||
"icon": "view_module",
|
||||
"tabIndex": 4
|
||||
},
|
||||
{
|
||||
"id": "frame",
|
||||
"text": I18n.tr("Frame"),
|
||||
@@ -188,12 +201,6 @@ Rectangle {
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "compositor",
|
||||
"text": I18n.tr("Compositor"),
|
||||
"icon": "layers",
|
||||
"tabIndex": 4
|
||||
},
|
||||
{
|
||||
"id": "keybinds",
|
||||
"text": I18n.tr("Keyboard Shortcuts"),
|
||||
@@ -259,6 +266,13 @@ Rectangle {
|
||||
"icon": "line_start",
|
||||
"tabIndex": 36,
|
||||
"autostartOnly": true
|
||||
},
|
||||
{
|
||||
"id": "window_rules",
|
||||
"text": I18n.tr("Window Rules"),
|
||||
"icon": "select_window",
|
||||
"tabIndex": 38,
|
||||
"windowRulesCapable": true
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -372,6 +386,8 @@ Rectangle {
|
||||
return false;
|
||||
if (item.windowRulesCapable && !CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango)
|
||||
return false;
|
||||
if (item.layoutCapable && !CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango)
|
||||
return false;
|
||||
if (item.niriOnly && !CompositorService.isNiri)
|
||||
return false;
|
||||
if (item.clipboardOnly && (!DMSService.isConnected || DMSService.apiVersion < 23))
|
||||
@@ -544,8 +560,8 @@ Rectangle {
|
||||
return -1;
|
||||
|
||||
var normalized = name.toLowerCase().replace(/[_\-\s]/g, "");
|
||||
if (normalized === "workspaces")
|
||||
normalized = "compositor";
|
||||
if (normalized === "compositor")
|
||||
normalized = "workspaces";
|
||||
|
||||
for (var i = 0; i < categoryStructure.length; i++) {
|
||||
var cat = categoryStructure[i];
|
||||
|
||||
@@ -7,6 +7,7 @@ import qs.Widgets
|
||||
import qs.Services
|
||||
|
||||
Variants {
|
||||
readonly property var log: Log.scoped("BlurredWallpaperBackground")
|
||||
model: {
|
||||
if (SessionData.isGreeterMode) {
|
||||
return Quickshell.screens;
|
||||
@@ -32,6 +33,8 @@ Variants {
|
||||
|
||||
color: "transparent"
|
||||
|
||||
updatesEnabled: root.renderActive || root._settleFrames > 0
|
||||
|
||||
mask: Region {
|
||||
item: Item {}
|
||||
}
|
||||
@@ -85,7 +88,6 @@ Variants {
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
blurWallpaperWindow.updatesEnabled = Qt.binding(() => !root.source || root.effectActive || root._renderSettling || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading);
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
@@ -93,51 +95,67 @@ Variants {
|
||||
property real transitionProgress: 0
|
||||
readonly property bool transitioning: transitionAnimation.running
|
||||
property bool effectActive: false
|
||||
property bool _renderSettling: true
|
||||
property bool useNextForEffect: false
|
||||
readonly property var backingWindow: Window.window
|
||||
readonly property bool renderActive: !source || effectActive || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading
|
||||
property int _settleFrames: 3
|
||||
|
||||
Connections {
|
||||
target: currentWallpaper
|
||||
function onStatusChanged() {
|
||||
if (currentWallpaper.status !== Image.Ready && currentWallpaper.status !== Image.Error)
|
||||
return;
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
}
|
||||
function invalidate() {
|
||||
_settleFrames = 3;
|
||||
backingWindow?.update();
|
||||
}
|
||||
|
||||
onRenderActiveChanged: invalidate()
|
||||
onBackingWindowChanged: invalidate()
|
||||
|
||||
Connections {
|
||||
target: blurWallpaperWindow
|
||||
target: root.backingWindow
|
||||
function onFrameSwapped() {
|
||||
if (root._settleFrames > 0)
|
||||
root._settleFrames--;
|
||||
}
|
||||
function onVisibleChanged() {
|
||||
root.invalidate();
|
||||
}
|
||||
function onWidthChanged() {
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
root.invalidate();
|
||||
}
|
||||
function onHeightChanged() {
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
root.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Quickshell
|
||||
function onScreensChanged() {
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
root.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onWallpaperFillModeChanged() {
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
root.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: renderSettleTimer
|
||||
interval: 1000
|
||||
onTriggered: root._renderSettling = false
|
||||
Connections {
|
||||
target: IdleService
|
||||
function onIsShellLockedChanged() {
|
||||
if (IdleService.isShellLocked)
|
||||
return;
|
||||
root.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
function handleTransitionLoadError(failedSource) {
|
||||
log.warn("failed to load candidate wallpaper for", modelData.name + ":", failedSource);
|
||||
transitionDelayTimer.stop();
|
||||
transitionAnimation.stop();
|
||||
root.useNextForEffect = false;
|
||||
root.effectActive = false;
|
||||
root.transitionProgress = 0.0;
|
||||
nextWallpaper.source = "";
|
||||
}
|
||||
|
||||
onSourceChanged: {
|
||||
@@ -164,8 +182,6 @@ Variants {
|
||||
transitionAnimation.stop();
|
||||
root.transitionProgress = 0.0;
|
||||
root.effectActive = false;
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
currentWallpaper.source = newSource;
|
||||
nextWallpaper.source = "";
|
||||
}
|
||||
@@ -194,8 +210,6 @@ Variants {
|
||||
transitionAnimation.stop();
|
||||
root.transitionProgress = 0;
|
||||
root.effectActive = false;
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
currentWallpaper.source = nextWallpaper.source;
|
||||
nextWallpaper.source = "";
|
||||
}
|
||||
@@ -204,9 +218,6 @@ Variants {
|
||||
return;
|
||||
}
|
||||
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
|
||||
nextWallpaper.source = newPath;
|
||||
|
||||
if (nextWallpaper.status === Image.Ready)
|
||||
@@ -215,7 +226,7 @@ Variants {
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
active: !root.source || root.isColorSource
|
||||
active: !root.source || root.isColorSource || currentWallpaper.status === Image.Error
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: DankBackdrop {
|
||||
@@ -238,6 +249,12 @@ Variants {
|
||||
cache: true
|
||||
sourceSize: Qt.size(root.textureWidth, root.textureHeight)
|
||||
fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SessionData.getMonitorWallpaperFillMode(modelData.name))
|
||||
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error) {
|
||||
log.warn("failed to load active wallpaper for", modelData.name + ":", source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
@@ -253,6 +270,10 @@ Variants {
|
||||
fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SessionData.getMonitorWallpaperFillMode(modelData.name))
|
||||
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error) {
|
||||
root.handleTransitionLoadError(source);
|
||||
return;
|
||||
}
|
||||
if (status !== Image.Ready)
|
||||
return;
|
||||
if (!root.transitioning) {
|
||||
@@ -329,8 +350,6 @@ Variants {
|
||||
root.useNextForEffect = false;
|
||||
nextWallpaper.source = "";
|
||||
root.transitionProgress = 0.0;
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
root.effectActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
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
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
@@ -109,15 +109,7 @@ DankPopout {
|
||||
close();
|
||||
}
|
||||
|
||||
customKeyboardFocus: {
|
||||
if (!shouldBeVisible)
|
||||
return WlrKeyboardFocus.None;
|
||||
if (anyModalOpen)
|
||||
return WlrKeyboardFocus.None;
|
||||
if (CompositorService.useHyprlandFocusGrab)
|
||||
return WlrKeyboardFocus.OnDemand;
|
||||
return WlrKeyboardFocus.Exclusive;
|
||||
}
|
||||
customKeyboardFocus: anyModalOpen ? WlrKeyboardFocus.None : null
|
||||
|
||||
onBackgroundClicked: close()
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ Item {
|
||||
// M3 elevation shadow — Level 2 baseline (navigation bar), with per-bar override support
|
||||
readonly property bool hasPerBarOverride: (barConfig?.shadowIntensity ?? 0) > 0
|
||||
readonly property var elevLevel: Theme.elevationLevel2
|
||||
readonly property bool shadowEnabled: !BlurService.enabled && ((Theme.elevationEnabled && (typeof SettingsData !== "undefined" ? (SettingsData.barElevationEnabled ?? true) : false)) || hasPerBarOverride)
|
||||
readonly property bool shadowEnabled: (Theme.elevationEnabled && (typeof SettingsData !== "undefined" ? (SettingsData.barElevationEnabled ?? true) : false)) || hasPerBarOverride
|
||||
readonly property string autoBarShadowDirection: isTop ? "top" : (isBottom ? "bottom" : (isLeft ? "left" : (isRight ? "right" : "top")))
|
||||
readonly property string globalShadowDirection: Theme.elevationLightDirection === "autoBar" ? autoBarShadowDirection : Theme.elevationLightDirection
|
||||
readonly property string perBarShadowDirectionMode: barConfig?.shadowDirectionMode ?? "inherit"
|
||||
@@ -207,7 +207,6 @@ Item {
|
||||
shadowOffsetX: root.shadowOffsetX
|
||||
shadowOffsetY: root.shadowOffsetY
|
||||
shadowColor: root.shadowColor
|
||||
blurMax: Theme.elevationBlurMax
|
||||
}
|
||||
|
||||
Loader {
|
||||
|
||||
@@ -108,8 +108,6 @@ Item {
|
||||
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
|
||||
focusedScreenName = focusedWs?.monitor?.name || "";
|
||||
} else if (CompositorService.isDwl && DwlService.activeOutput) {
|
||||
focusedScreenName = DwlService.activeOutput;
|
||||
} else if (CompositorService.isMango && MangoService.activeOutput) {
|
||||
focusedScreenName = MangoService.activeOutput;
|
||||
}
|
||||
@@ -139,8 +137,6 @@ Item {
|
||||
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
|
||||
focusedScreenName = focusedWs?.monitor?.name || "";
|
||||
} else if (CompositorService.isDwl && DwlService.activeOutput) {
|
||||
focusedScreenName = DwlService.activeOutput;
|
||||
} else if (CompositorService.isMango && MangoService.activeOutput) {
|
||||
focusedScreenName = MangoService.activeOutput;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,8 @@ PanelWindow {
|
||||
id: barWindow
|
||||
readonly property var log: Log.scoped("DankBarWindow")
|
||||
|
||||
Component.onDestruction: KeyboardFocus.unregisterBarWindow(barWindow)
|
||||
|
||||
required property var rootWindow
|
||||
required property var barConfig
|
||||
property var modelData: item
|
||||
@@ -18,6 +20,8 @@ PanelWindow {
|
||||
property var centerWidgetsModel
|
||||
property var rightWidgetsModel
|
||||
|
||||
readonly property bool barRevealed: inputMask.showing
|
||||
|
||||
property var controlCenterButtonRef: null
|
||||
property var clockButtonRef: null
|
||||
property var systemUpdateButtonRef: null
|
||||
@@ -282,9 +286,6 @@ PanelWindow {
|
||||
|
||||
readonly property bool isVertical: axis.isVertical
|
||||
|
||||
property bool gothCornersEnabled: barConfig?.gothCornersEnabled ?? false
|
||||
property real wingtipsRadius: barConfig?.gothCornerRadiusOverride ? (barConfig?.gothCornerRadiusValue ?? 12) : Theme.cornerRadius
|
||||
readonly property real _wingR: Math.max(0, wingtipsRadius)
|
||||
readonly property color _surfaceContainer: Theme.surfaceContainer
|
||||
readonly property string _barId: barConfig?.id ?? "default"
|
||||
property real _backgroundAlpha: barConfig?.transparency ?? 1.0
|
||||
@@ -296,25 +297,30 @@ PanelWindow {
|
||||
}
|
||||
readonly property real _dpr: CompositorService.getScreenScale(barWindow.screen)
|
||||
|
||||
property string screenName: modelData.name
|
||||
|
||||
readonly property bool usesConnectedFrameChrome: CompositorService.usesConnectedFrameChromeForScreen(screenName)
|
||||
readonly property bool usesFrameBarChrome: CompositorService.frameWindowVisibleForScreen(screenName)
|
||||
readonly property var renderBarConfig: SettingsData.effectiveBarConfigForRender(barConfig, usesFrameBarChrome)
|
||||
|
||||
property bool gothCornersEnabled: renderBarConfig?.gothCornersEnabled ?? false
|
||||
property real wingtipsRadius: renderBarConfig?.gothCornerRadiusOverride ? (renderBarConfig?.gothCornerRadiusValue ?? 12) : Theme.cornerRadius
|
||||
readonly property real _wingR: Math.max(0, wingtipsRadius)
|
||||
|
||||
// Shadow buffer: extra window space for shadow to render beyond bar bounds
|
||||
readonly property bool _shadowActive: (Theme.elevationEnabled && (typeof SettingsData !== "undefined" ? (SettingsData.barElevationEnabled ?? true) : false)) || (barConfig?.shadowIntensity ?? 0) > 0
|
||||
readonly property bool _shadowActive: (Theme.elevationEnabled && (typeof SettingsData !== "undefined" ? (SettingsData.barElevationEnabled ?? true) : false)) || (renderBarConfig?.shadowIntensity ?? 0) > 0
|
||||
readonly property real _shadowBuffer: {
|
||||
if (!_shadowActive)
|
||||
return 0;
|
||||
const hasOverride = (barConfig?.shadowIntensity ?? 0) > 0;
|
||||
const hasOverride = (renderBarConfig?.shadowIntensity ?? 0) > 0;
|
||||
if (hasOverride) {
|
||||
const blur = (barConfig.shadowIntensity ?? 0) * 0.2;
|
||||
const blur = (renderBarConfig.shadowIntensity ?? 0) * 0.2;
|
||||
const offset = blur * 0.5;
|
||||
return Theme.snap(Math.max(16, blur + offset + 8), _dpr);
|
||||
}
|
||||
return Theme.snap(Theme.elevationRenderPadding(Theme.elevationLevel2, "top", 4, 8, 16), _dpr);
|
||||
}
|
||||
|
||||
property string screenName: modelData.name
|
||||
|
||||
readonly property bool usesConnectedFrameChrome: CompositorService.usesConnectedFrameChromeForScreen(screenName)
|
||||
readonly property bool usesFrameBarChrome: CompositorService.frameWindowVisibleForScreen(screenName)
|
||||
|
||||
// Flatten/spacing collapse for maximized windows is only for frame-integrated layout.
|
||||
// When the bar draws its own pill, keep rounded corners and spacing like the dock.
|
||||
readonly property bool flattenForMaximizedWindow: !SettingsData.frameEnabled || usesFrameBarChrome
|
||||
@@ -550,11 +556,12 @@ PanelWindow {
|
||||
}
|
||||
|
||||
screen: modelData
|
||||
implicitHeight: !isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((barConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0
|
||||
implicitWidth: isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((barConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0
|
||||
implicitHeight: !isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((renderBarConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0
|
||||
implicitWidth: isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((renderBarConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0
|
||||
color: "transparent"
|
||||
|
||||
Component.onCompleted: {
|
||||
KeyboardFocus.registerBarWindow(barWindow);
|
||||
updateGpuTempConfig();
|
||||
_updateBackgroundAlpha();
|
||||
_updateHasMaximizedToplevel();
|
||||
@@ -702,6 +709,14 @@ PanelWindow {
|
||||
readonly property var _rightSection: topBarContent ? (barWindow.isVertical ? topBarContent.vRightSection : topBarContent.hRightSection) : null
|
||||
readonly property real _revealProgress: topBarSlide.x + topBarSlide.y
|
||||
|
||||
function containsGlobalPoint(gx, gy, padding) {
|
||||
const pad = padding !== undefined ? padding : 16;
|
||||
if (!inputMask.showing)
|
||||
return false;
|
||||
const topLeft = inputMask.mapToItem(null, 0, 0);
|
||||
return gx >= topLeft.x - pad && gx < topLeft.x + inputMask.width + pad && gy >= topLeft.y - pad && gy < topLeft.y + inputMask.height + pad;
|
||||
}
|
||||
|
||||
function sectionRect(section, isCenter, _dep) {
|
||||
if (!section)
|
||||
return {
|
||||
@@ -947,7 +962,7 @@ PanelWindow {
|
||||
id: barBackground
|
||||
barWindow: barWindow
|
||||
axis: axis
|
||||
barConfig: barWindow.barConfig
|
||||
barConfig: barWindow.renderBarConfig
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
@@ -956,8 +971,13 @@ PanelWindow {
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onClicked: {
|
||||
const screenName = barWindow.screen?.name;
|
||||
if (screenName && PopoutManager.currentPopoutsByScreen[screenName])
|
||||
if (!screenName)
|
||||
return;
|
||||
if (PopoutManager.currentPopoutsByScreen[screenName])
|
||||
PopoutManager.closeAllPopouts();
|
||||
if (ModalManager.currentModalsByScreen[screenName])
|
||||
ModalManager.closeAllModalsExcept(null);
|
||||
TrayMenuManager.closeAllMenus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -998,7 +1018,7 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
|
||||
onWheel: wheel => {
|
||||
function processWheel(wheel) {
|
||||
if (!(barConfig?.scrollEnabled ?? true) || actionInProgress) {
|
||||
wheel.accepted = false;
|
||||
return;
|
||||
@@ -1067,6 +1087,8 @@ PanelWindow {
|
||||
|
||||
wheel.accepted = false;
|
||||
}
|
||||
|
||||
onWheel: wheel => processWheel(wheel)
|
||||
}
|
||||
|
||||
DankBarContent {
|
||||
@@ -1078,6 +1100,36 @@ PanelWindow {
|
||||
centerWidgetsModel: barWindow.centerWidgetsModel
|
||||
rightWidgetsModel: barWindow.rightWidgetsModel
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: hoverPopoutArea
|
||||
anchors.fill: parent
|
||||
z: 1
|
||||
hoverEnabled: barConfig?.hoverPopouts ?? false
|
||||
enabled: hoverPopoutArea.hoverEnabled && !barWindow.clickThroughEnabled
|
||||
acceptedButtons: Qt.NoButton
|
||||
propagateComposedEvents: true
|
||||
|
||||
property real lastGlobalX: 0
|
||||
property real lastGlobalY: 0
|
||||
|
||||
onPositionChanged: mouse => {
|
||||
const gp = mapToItem(null, mouse.x, mouse.y);
|
||||
lastGlobalX = gp.x;
|
||||
lastGlobalY = gp.y;
|
||||
topBarContent.checkHoverPopout(gp.x, gp.y);
|
||||
}
|
||||
|
||||
onWheel: wheel => scrollArea.processWheel(wheel)
|
||||
|
||||
onContainsMouseChanged: {
|
||||
if (containsMouse)
|
||||
return;
|
||||
if (topBarContent.cursorOverHoverChain(lastGlobalX, lastGlobalY))
|
||||
return;
|
||||
topBarContent.closeHoverSurfaces();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,7 @@ DankPopout {
|
||||
|
||||
property var triggerScreen: null
|
||||
|
||||
// mango shares dwl's layout model; route to the right service.
|
||||
readonly property bool isDwlLike: CompositorService.isDwl || CompositorService.isMango
|
||||
readonly property var dwlSvc: CompositorService.isMango ? MangoService : DwlService
|
||||
readonly property bool isMango: CompositorService.isMango
|
||||
|
||||
function setTriggerPosition(x, y, width, section, screen, barPosition, barThickness, barSpacing, barConfig) {
|
||||
triggerX = x;
|
||||
@@ -37,8 +35,8 @@ DankPopout {
|
||||
onScreenChanged: updateOutputState()
|
||||
|
||||
function updateOutputState() {
|
||||
if (screen && root.dwlSvc.available) {
|
||||
outputState = root.dwlSvc.getOutputState(screen.name);
|
||||
if (screen && MangoService.available) {
|
||||
outputState = MangoService.getOutputState(screen.name);
|
||||
} else {
|
||||
outputState = null;
|
||||
}
|
||||
@@ -84,7 +82,7 @@ DankPopout {
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DwlService
|
||||
target: MangoService
|
||||
function onStateChanged() {
|
||||
updateOutputState();
|
||||
}
|
||||
@@ -219,7 +217,7 @@ DankPopout {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: root.dwlSvc.layouts
|
||||
model: MangoService.layouts
|
||||
|
||||
delegate: Rectangle {
|
||||
required property string modelData
|
||||
@@ -273,11 +271,11 @@ DankPopout {
|
||||
if (!root.triggerScreen) {
|
||||
return;
|
||||
}
|
||||
if (!root.dwlSvc.available) {
|
||||
if (!MangoService.available) {
|
||||
return;
|
||||
}
|
||||
|
||||
root.dwlSvc.setLayout(root.triggerScreen.name, index);
|
||||
MangoService.setLayout(root.triggerScreen.name, index);
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,15 +38,7 @@ DankPopout {
|
||||
|
||||
backgroundInteractive: !anyModalOpen
|
||||
|
||||
customKeyboardFocus: {
|
||||
if (!shouldBeVisible)
|
||||
return WlrKeyboardFocus.None;
|
||||
if (anyModalOpen)
|
||||
return WlrKeyboardFocus.None;
|
||||
if (CompositorService.useHyprlandFocusGrab)
|
||||
return WlrKeyboardFocus.OnDemand;
|
||||
return WlrKeyboardFocus.Exclusive;
|
||||
}
|
||||
customKeyboardFocus: anyModalOpen ? WlrKeyboardFocus.None : null
|
||||
|
||||
Connections {
|
||||
target: SystemUpdateService
|
||||
|
||||
@@ -282,7 +282,7 @@ Loader {
|
||||
"cpuTemp": dgopAvailable,
|
||||
"gpuTemp": dgopAvailable,
|
||||
"network_speed_monitor": dgopAvailable,
|
||||
"layout": (CompositorService.isDwl && DwlService.dwlAvailable) || (CompositorService.isMango && MangoService.available)
|
||||
"layout": CompositorService.isMango && MangoService.available
|
||||
};
|
||||
|
||||
return widgetVisibility[widgetId] ?? true;
|
||||
|
||||
@@ -13,12 +13,11 @@ BasePill {
|
||||
signal toggleLayoutPopup
|
||||
|
||||
// mango shares dwl's tag/layout model; route to the right service.
|
||||
readonly property bool isDwlLike: CompositorService.isDwl || CompositorService.isMango
|
||||
readonly property var dwlSvc: CompositorService.isMango ? MangoService : DwlService
|
||||
readonly property bool isMango: CompositorService.isMango
|
||||
|
||||
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 int currentLayoutIndex: outputState?.layout || 0
|
||||
|
||||
@@ -41,9 +40,9 @@ BasePill {
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: layout.dwlSvc
|
||||
target: MangoService
|
||||
function onStateChanged() {
|
||||
outputState = parentScreen ? layout.dwlSvc.getOutputState(parentScreen.name) : null;
|
||||
outputState = parentScreen ? MangoService.getOutputState(parentScreen.name) : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,13 +100,13 @@ BasePill {
|
||||
}
|
||||
|
||||
onRightClicked: {
|
||||
if (!parentScreen || !layout.dwlSvc.available || layout.dwlSvc.layouts.length === 0) {
|
||||
if (!parentScreen || !MangoService.available || MangoService.layouts.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
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: {
|
||||
if (CompositorService.isNiri) {
|
||||
return NiriService.getCurrentKeyboardLayoutName();
|
||||
} else if (CompositorService.isDwl) {
|
||||
return DwlService.currentKeyboardLayout;
|
||||
} else if (CompositorService.isMango) {
|
||||
return MangoService.currentKeyboardLayout;
|
||||
}
|
||||
@@ -209,8 +207,6 @@ BasePill {
|
||||
NiriService.cycleKeyboardLayout();
|
||||
} else if (CompositorService.isHyprland) {
|
||||
Quickshell.execDetached(["hyprctl", "switchxkblayout", root.hyprlandKeyboard, "next"]);
|
||||
} else if (CompositorService.isDwl) {
|
||||
Quickshell.execDetached(["mmsg", "dispatch", "switch_keyboard_layout"]);
|
||||
} else if (CompositorService.isMango) {
|
||||
MangoService.cycleKeyboardLayout();
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ BasePill {
|
||||
}
|
||||
|
||||
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
|
||||
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)
|
||||
@@ -66,8 +66,6 @@ BasePill {
|
||||
return "file://" + Theme.shellDir + "/assets/niri.svg";
|
||||
} else if (CompositorService.isHyprland) {
|
||||
return "file://" + Theme.shellDir + "/assets/hyprland.svg";
|
||||
} else if (CompositorService.isDwl) {
|
||||
return "file://" + Theme.shellDir + "/assets/mango.png";
|
||||
} else if (CompositorService.isMango) {
|
||||
return "file://" + Theme.shellDir + "/assets/mango.png";
|
||||
} else if (CompositorService.isSway) {
|
||||
|
||||
@@ -980,21 +980,13 @@ BasePill {
|
||||
screen: root.parentScreen
|
||||
WlrLayershell.layer: root.barUsesOverlayLayer ? WlrLayershell.Overlay : WlrLayershell.Top
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: {
|
||||
if (PopoutManager.screenshotActive)
|
||||
return WlrKeyboardFocus.None;
|
||||
if (!root.menuOpen)
|
||||
return WlrKeyboardFocus.None;
|
||||
if (CompositorService.useHyprlandFocusGrab)
|
||||
return WlrKeyboardFocus.OnDemand;
|
||||
return WlrKeyboardFocus.Exclusive;
|
||||
}
|
||||
WlrLayershell.keyboardFocus: KeyboardFocus.keyboardFocus(root.menuOpen, null)
|
||||
WlrLayershell.namespace: "dms:tray-overflow-menu"
|
||||
color: "transparent"
|
||||
|
||||
HyprlandFocusGrab {
|
||||
windows: [overflowMenu]
|
||||
active: CompositorService.useHyprlandFocusGrab && root.useOverflowPopup && root.menuOpen
|
||||
windows: [overflowMenu].concat(KeyboardFocus.barWindows)
|
||||
active: root.useOverflowPopup && KeyboardFocus.wantsGrab(root.menuOpen, null)
|
||||
}
|
||||
|
||||
Connections {
|
||||
@@ -1051,32 +1043,21 @@ BasePill {
|
||||
"leftBar": 0,
|
||||
"rightBar": 0
|
||||
})
|
||||
readonly property real effectiveBarSize: root.barThickness + root.barSpacing
|
||||
readonly property real maskX: _overflowDismissZone.x
|
||||
readonly property real maskY: _overflowDismissZone.y
|
||||
readonly property real maskWidth: _overflowDismissZone.width
|
||||
readonly property real maskHeight: _overflowDismissZone.height
|
||||
|
||||
readonly property real maskX: {
|
||||
const triggeringBarX = (barPosition === 2) ? effectiveBarSize : 0;
|
||||
const adjacentLeftBar = adjacentBarInfo?.leftBar ?? 0;
|
||||
return Math.max(triggeringBarX, adjacentLeftBar);
|
||||
}
|
||||
|
||||
readonly property real maskY: {
|
||||
const triggeringBarY = (barPosition === 0) ? effectiveBarSize : 0;
|
||||
const adjacentTopBar = adjacentBarInfo?.topBar ?? 0;
|
||||
return Math.max(triggeringBarY, adjacentTopBar);
|
||||
}
|
||||
|
||||
readonly property real maskWidth: {
|
||||
const triggeringBarRight = (barPosition === 3) ? effectiveBarSize : 0;
|
||||
const adjacentRightBar = adjacentBarInfo?.rightBar ?? 0;
|
||||
const rightExclusion = Math.max(triggeringBarRight, adjacentRightBar);
|
||||
return Math.max(100, width - maskX - rightExclusion);
|
||||
}
|
||||
|
||||
readonly property real maskHeight: {
|
||||
const triggeringBarBottom = (barPosition === 1) ? effectiveBarSize : 0;
|
||||
const adjacentBottomBar = adjacentBarInfo?.bottomBar ?? 0;
|
||||
const bottomExclusion = Math.max(triggeringBarBottom, adjacentBottomBar);
|
||||
return Math.max(100, height - maskY - bottomExclusion);
|
||||
DismissZone {
|
||||
id: _overflowDismissZone
|
||||
barPosition: overflowMenu.barPosition
|
||||
barX: overflowMenu.barX
|
||||
barY: overflowMenu.barY
|
||||
barWidth: overflowMenu.barWidth
|
||||
barHeight: overflowMenu.barHeight
|
||||
screenWidth: overflowMenu.width
|
||||
screenHeight: overflowMenu.height
|
||||
adjacentBarInfo: overflowMenu.adjacentBarInfo
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
@@ -1237,13 +1218,7 @@ BasePill {
|
||||
fallbackOffset: 6
|
||||
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
targetRadius: Theme.cornerRadius
|
||||
sourceRect.antialiasing: true
|
||||
sourceRect.smooth: true
|
||||
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && !BlurService.enabled
|
||||
layer.smooth: true
|
||||
layer.textureSize: Qt.size(Math.round(width * overflowMenu.dpr * 2), Math.round(height * overflowMenu.dpr * 2))
|
||||
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
||||
layer.samples: 4
|
||||
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@@ -1450,20 +1425,12 @@ BasePill {
|
||||
screen: menuRoot.parentScreen
|
||||
WlrLayershell.layer: root.barUsesOverlayLayer ? WlrLayershell.Overlay : WlrLayershell.Top
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: {
|
||||
if (PopoutManager.screenshotActive)
|
||||
return WlrKeyboardFocus.None;
|
||||
if (!menuRoot.showMenu)
|
||||
return WlrKeyboardFocus.None;
|
||||
if (CompositorService.useHyprlandFocusGrab)
|
||||
return WlrKeyboardFocus.OnDemand;
|
||||
return WlrKeyboardFocus.Exclusive;
|
||||
}
|
||||
WlrLayershell.keyboardFocus: KeyboardFocus.keyboardFocus(menuRoot.showMenu, null)
|
||||
color: "transparent"
|
||||
|
||||
HyprlandFocusGrab {
|
||||
windows: [menuWindow]
|
||||
active: CompositorService.useHyprlandFocusGrab && menuRoot.showMenu
|
||||
windows: [menuWindow].concat(KeyboardFocus.barWindows)
|
||||
active: KeyboardFocus.wantsGrab(menuRoot.showMenu, null)
|
||||
}
|
||||
|
||||
anchors {
|
||||
@@ -1502,32 +1469,21 @@ BasePill {
|
||||
"leftBar": 0,
|
||||
"rightBar": 0
|
||||
})
|
||||
readonly property real effectiveBarSize: root.barThickness + root.barSpacing
|
||||
readonly property real maskX: _menuDismissZone.x
|
||||
readonly property real maskY: _menuDismissZone.y
|
||||
readonly property real maskWidth: _menuDismissZone.width
|
||||
readonly property real maskHeight: _menuDismissZone.height
|
||||
|
||||
readonly property real maskX: {
|
||||
const triggeringBarX = (barPosition === 2) ? effectiveBarSize : 0;
|
||||
const adjacentLeftBar = adjacentBarInfo?.leftBar ?? 0;
|
||||
return Math.max(triggeringBarX, adjacentLeftBar);
|
||||
}
|
||||
|
||||
readonly property real maskY: {
|
||||
const triggeringBarY = (barPosition === 0) ? effectiveBarSize : 0;
|
||||
const adjacentTopBar = adjacentBarInfo?.topBar ?? 0;
|
||||
return Math.max(triggeringBarY, adjacentTopBar);
|
||||
}
|
||||
|
||||
readonly property real maskWidth: {
|
||||
const triggeringBarRight = (barPosition === 3) ? effectiveBarSize : 0;
|
||||
const adjacentRightBar = adjacentBarInfo?.rightBar ?? 0;
|
||||
const rightExclusion = Math.max(triggeringBarRight, adjacentRightBar);
|
||||
return Math.max(100, width - maskX - rightExclusion);
|
||||
}
|
||||
|
||||
readonly property real maskHeight: {
|
||||
const triggeringBarBottom = (barPosition === 1) ? effectiveBarSize : 0;
|
||||
const adjacentBottomBar = adjacentBarInfo?.bottomBar ?? 0;
|
||||
const bottomExclusion = Math.max(triggeringBarBottom, adjacentBottomBar);
|
||||
return Math.max(100, height - maskY - bottomExclusion);
|
||||
DismissZone {
|
||||
id: _menuDismissZone
|
||||
barPosition: menuWindow.barPosition
|
||||
barX: menuWindow.barX
|
||||
barY: menuWindow.barY
|
||||
barWidth: menuWindow.barWidth
|
||||
barHeight: menuWindow.barHeight
|
||||
screenWidth: menuWindow.width
|
||||
screenHeight: menuWindow.height
|
||||
adjacentBarInfo: menuWindow.adjacentBarInfo
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
@@ -1689,11 +1645,7 @@ BasePill {
|
||||
fallbackOffset: 6
|
||||
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
targetRadius: Theme.cornerRadius
|
||||
sourceRect.antialiasing: true
|
||||
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && !BlurService.enabled
|
||||
layer.smooth: true
|
||||
layer.textureSize: Qt.size(Math.round(width * menuWindow.dpr), Math.round(height * menuWindow.dpr))
|
||||
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
||||
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@@ -1970,4 +1922,53 @@ BasePill {
|
||||
return;
|
||||
currentTrayMenu.showForTrayItem(item, anchor, screen, atBottom, vertical ?? false, axisObj);
|
||||
}
|
||||
|
||||
function _trayLayoutRoot() {
|
||||
const contentChildren = root.visualContent?.children;
|
||||
if (!contentChildren || contentChildren.length === 0)
|
||||
return null;
|
||||
const contentRoot = contentChildren[0];
|
||||
return contentRoot?.layoutLoader?.item || null;
|
||||
}
|
||||
|
||||
function _trayHitAtGlobalPoint(gx, gy) {
|
||||
if (!root.visible || root.width <= 0 || root.height <= 0)
|
||||
return null;
|
||||
const local = root.mapFromItem(null, gx, gy);
|
||||
if (local.x < 0 || local.y < 0 || local.x > root.width || local.y > root.height)
|
||||
return null;
|
||||
const layout = _trayLayoutRoot();
|
||||
if (!layout)
|
||||
return null;
|
||||
const layoutLocal = layout.mapFromItem(null, gx, gy);
|
||||
const children = layout.children || [];
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i];
|
||||
if (!child.visible || child.width <= 0 || child.height <= 0)
|
||||
continue;
|
||||
if (layoutLocal.x < child.x || layoutLocal.x >= child.x + child.width)
|
||||
continue;
|
||||
if (layoutLocal.y < child.y || layoutLocal.y >= child.y + child.height)
|
||||
continue;
|
||||
if (child.trayItem)
|
||||
return child;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function hoverTriggerAtGlobalPoint(gx, gy) {
|
||||
const hit = _trayHitAtGlobalPoint(gx, gy);
|
||||
if (!hit?.trayItem?.hasMenu)
|
||||
return "";
|
||||
return "tray-" + (hit.trayItem.id || hit.itemKey || "");
|
||||
}
|
||||
|
||||
function openHoverAtGlobalPoint(gx, gy) {
|
||||
const hit = _trayHitAtGlobalPoint(gx, gy);
|
||||
if (!hit?.trayItem?.hasMenu)
|
||||
return false;
|
||||
const anchor = hit.children?.length > 0 ? hit.children[0] : hit;
|
||||
showForTrayItem(hit.trayItem, anchor, parentScreen, isAtBottom, isVerticalOrientation, axis);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,10 +22,7 @@ Item {
|
||||
property var hyprlandOverviewLoader: null
|
||||
property var parentScreen: null
|
||||
|
||||
// mango shares dwl's tag model; route to the right service so one set of
|
||||
// branches serves both.
|
||||
readonly property bool isDwlLike: CompositorService.isDwl || CompositorService.isMango
|
||||
readonly property var dwlSvc: CompositorService.isMango ? MangoService : DwlService
|
||||
readonly property bool isMango: CompositorService.isMango
|
||||
|
||||
readonly property real _leftMargin: {
|
||||
if (isVertical)
|
||||
@@ -80,9 +77,8 @@ Item {
|
||||
return NiriService.currentOutput || root.screenName;
|
||||
case "hyprland":
|
||||
return Hyprland.focusedWorkspace?.monitor?.name || root.screenName;
|
||||
case "dwl":
|
||||
case "mango":
|
||||
return root.dwlSvc.activeOutput || root.screenName;
|
||||
return MangoService.activeOutput || root.screenName;
|
||||
case "sway":
|
||||
case "scroll":
|
||||
case "miracle":
|
||||
@@ -101,7 +97,6 @@ Item {
|
||||
switch (CompositorService.compositor) {
|
||||
case "niri":
|
||||
case "hyprland":
|
||||
case "dwl":
|
||||
case "mango":
|
||||
case "sway":
|
||||
case "scroll":
|
||||
@@ -128,7 +123,6 @@ Item {
|
||||
return getNiriActiveWorkspace();
|
||||
case "hyprland":
|
||||
return getHyprlandActiveWorkspace();
|
||||
case "dwl":
|
||||
case "mango":
|
||||
const activeTags = getDwlActiveTags();
|
||||
return activeTags.length > 0 ? activeTags[0] : -1;
|
||||
@@ -141,7 +135,7 @@ Item {
|
||||
}
|
||||
}
|
||||
property var dwlActiveTags: {
|
||||
if (root.isDwlLike) {
|
||||
if (root.isMango) {
|
||||
return getDwlActiveTags();
|
||||
}
|
||||
return [];
|
||||
@@ -160,9 +154,6 @@ Item {
|
||||
case "hyprland":
|
||||
baseList = getHyprlandWorkspaces();
|
||||
break;
|
||||
case "dwl":
|
||||
baseList = getDwlTags();
|
||||
break;
|
||||
case "mango":
|
||||
if (root.mangoOverviewActive)
|
||||
return [];
|
||||
@@ -302,7 +293,7 @@ Item {
|
||||
}
|
||||
} else if (CompositorService.isHyprland) {
|
||||
targetWorkspaceId = ws.id !== undefined ? ws.id : ws;
|
||||
} else if (root.isDwlLike) {
|
||||
} else if (root.isMango) {
|
||||
if (typeof ws !== "object" || ws.tag === undefined) {
|
||||
return [];
|
||||
}
|
||||
@@ -322,8 +313,8 @@ Item {
|
||||
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
|
||||
isActiveWs = focusedWs ? (focusedWs.num === targetWorkspaceId) : false;
|
||||
} else if (root.isDwlLike) {
|
||||
const output = root.dwlSvc.getOutputState(root.effectiveScreenName);
|
||||
} else if (root.isMango) {
|
||||
const output = MangoService.getOutputState(root.effectiveScreenName);
|
||||
if (output && output.tags) {
|
||||
const tag = output.tags.find(t => t.tag === targetWorkspaceId);
|
||||
isActiveWs = tag ? (tag.state === 1) : false;
|
||||
@@ -411,7 +402,7 @@ Item {
|
||||
"id": -1,
|
||||
"name": ""
|
||||
};
|
||||
} else if (root.isDwlLike) {
|
||||
} else if (root.isMango) {
|
||||
placeholder = {
|
||||
"tag": -1
|
||||
};
|
||||
@@ -493,11 +484,11 @@ Item {
|
||||
}
|
||||
|
||||
function getDwlTags() {
|
||||
if (!root.dwlSvc.available)
|
||||
if (!MangoService.available)
|
||||
return [];
|
||||
|
||||
const targetScreen = root.effectiveScreenName;
|
||||
const output = root.dwlSvc.getOutputState(targetScreen);
|
||||
const output = MangoService.getOutputState(targetScreen);
|
||||
if (!output || !output.tags || output.tags.length === 0)
|
||||
return [];
|
||||
|
||||
@@ -510,7 +501,7 @@ Item {
|
||||
}));
|
||||
}
|
||||
|
||||
const visibleTagIndices = root.dwlSvc.getVisibleTags(targetScreen);
|
||||
const visibleTagIndices = MangoService.getVisibleTags(targetScreen);
|
||||
return visibleTagIndices.map(tagIndex => {
|
||||
const tagData = output.tags.find(t => t.tag === tagIndex);
|
||||
return {
|
||||
@@ -523,10 +514,10 @@ Item {
|
||||
}
|
||||
|
||||
function getDwlActiveTags() {
|
||||
if (!root.dwlSvc.available)
|
||||
if (!MangoService.available)
|
||||
return [];
|
||||
|
||||
return root.dwlSvc.getActiveTags(root.effectiveScreenName);
|
||||
return MangoService.getActiveTags(root.effectiveScreenName);
|
||||
}
|
||||
|
||||
function getExtWorkspaceWorkspaces() {
|
||||
@@ -577,7 +568,7 @@ Item {
|
||||
return ws && ws.idx !== -1;
|
||||
if (CompositorService.isHyprland)
|
||||
return ws && ws.id !== -1;
|
||||
if (root.isDwlLike)
|
||||
if (root.isMango)
|
||||
return ws && ws.tag !== -1;
|
||||
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
|
||||
return ws && ws.num !== -1;
|
||||
@@ -605,10 +596,9 @@ Item {
|
||||
HyprlandService.focusWorkspace(data.id);
|
||||
}
|
||||
break;
|
||||
case "dwl":
|
||||
case "mango":
|
||||
if (data.tag !== undefined)
|
||||
root.dwlSvc.switchToTag(root.screenName, data.tag);
|
||||
MangoService.switchToTag(root.screenName, data.tag);
|
||||
break;
|
||||
case "sway":
|
||||
case "scroll":
|
||||
@@ -694,7 +684,7 @@ Item {
|
||||
}
|
||||
|
||||
HyprlandService.focusWorkspace(realWorkspaces[nextIndex].id);
|
||||
} else if (root.isDwlLike) {
|
||||
} else if (root.isMango) {
|
||||
const realWorkspaces = getRealWorkspaces();
|
||||
if (realWorkspaces.length < 2) {
|
||||
return;
|
||||
@@ -708,7 +698,7 @@ Item {
|
||||
return;
|
||||
}
|
||||
|
||||
root.dwlSvc.switchToTag(root.screenName, realWorkspaces[nextIndex].tag);
|
||||
MangoService.switchToTag(root.screenName, realWorkspaces[nextIndex].tag);
|
||||
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||
const realWorkspaces = getRealWorkspaces();
|
||||
if (realWorkspaces.length < 2) {
|
||||
@@ -736,7 +726,7 @@ Item {
|
||||
return (modelData?.idx !== undefined && modelData?.idx !== -1) ? modelData.idx : "";
|
||||
if (CompositorService.isHyprland)
|
||||
return modelData?.id || "";
|
||||
if (root.isDwlLike)
|
||||
if (root.isMango)
|
||||
return (modelData?.tag !== undefined) ? (modelData.tag + 1) : "";
|
||||
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
|
||||
return modelData?.num || "";
|
||||
@@ -751,7 +741,7 @@ Item {
|
||||
isPlaceholder = modelData?.idx === -1;
|
||||
} else if (CompositorService.isHyprland) {
|
||||
isPlaceholder = modelData?.id === -1;
|
||||
} else if (root.isDwlLike) {
|
||||
} else if (root.isMango) {
|
||||
isPlaceholder = modelData?.tag === -1;
|
||||
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||
isPlaceholder = modelData?.num === -1;
|
||||
@@ -786,7 +776,7 @@ Item {
|
||||
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 shouldShow: hasNativeWorkspaceSupport || (useExtWorkspace && hasWorkspaces)
|
||||
|
||||
@@ -1051,7 +1041,7 @@ Item {
|
||||
return !!(modelData && modelData.idx === root.currentWorkspace);
|
||||
if (CompositorService.isHyprland)
|
||||
return !!(modelData && modelData.id === root.currentWorkspace);
|
||||
if (root.isDwlLike)
|
||||
if (root.isMango)
|
||||
return !!(modelData && root.dwlActiveTags.includes(modelData.tag));
|
||||
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
|
||||
return !!(modelData && modelData.num === root.currentWorkspace);
|
||||
@@ -1060,7 +1050,7 @@ Item {
|
||||
property bool isOccupied: {
|
||||
if (CompositorService.isHyprland)
|
||||
return Array.from(Hyprland.toplevels?.values || []).some(tl => tl.workspace?.id === modelData?.id);
|
||||
if (root.isDwlLike)
|
||||
if (root.isMango)
|
||||
return modelData.clients > 0;
|
||||
if (CompositorService.isNiri) {
|
||||
const workspace = NiriService.allWorkspaces.find(ws => ws.idx + 1 === modelData && ws.output === root.effectiveScreenName);
|
||||
@@ -1075,7 +1065,7 @@ Item {
|
||||
return !!(modelData && modelData.idx === -1);
|
||||
if (CompositorService.isHyprland)
|
||||
return !!(modelData && modelData.id === -1);
|
||||
if (root.isDwlLike)
|
||||
if (root.isMango)
|
||||
return !!(modelData && modelData.tag === -1);
|
||||
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
|
||||
return !!(modelData && modelData.num === -1);
|
||||
@@ -1092,7 +1082,7 @@ Item {
|
||||
return modelData?.urgent ?? false;
|
||||
if (CompositorService.isNiri)
|
||||
return loadedIsUrgent;
|
||||
if (root.isDwlLike)
|
||||
if (root.isMango)
|
||||
return modelData?.state === 2;
|
||||
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
|
||||
return loadedIsUrgent;
|
||||
@@ -1120,7 +1110,7 @@ Item {
|
||||
targetWorkspaceId = modelData?.id;
|
||||
} else if (CompositorService.isHyprland) {
|
||||
targetWorkspaceId = modelData?.id;
|
||||
} else if (root.isDwlLike) {
|
||||
} else if (root.isMango) {
|
||||
targetWorkspaceId = modelData?.tag;
|
||||
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||
targetWorkspaceId = modelData?.num;
|
||||
@@ -1383,8 +1373,8 @@ Item {
|
||||
}
|
||||
} else if (CompositorService.isHyprland && modelData?.id) {
|
||||
HyprlandService.focusWorkspace(modelData.id);
|
||||
} else if (root.isDwlLike && modelData?.tag !== undefined) {
|
||||
root.dwlSvc.switchToTag(root.screenName, modelData.tag);
|
||||
} else if (root.isMango && modelData?.tag !== undefined) {
|
||||
MangoService.switchToTag(root.screenName, modelData.tag);
|
||||
} else if ((CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) && modelData?.num) {
|
||||
try {
|
||||
I3.dispatch(`workspace number ${modelData.num}`);
|
||||
@@ -1395,8 +1385,8 @@ Item {
|
||||
NiriService.toggleOverview();
|
||||
} else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
|
||||
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen;
|
||||
} else if (root.isDwlLike && modelData?.tag !== undefined) {
|
||||
root.dwlSvc.toggleTag(root.screenName, modelData.tag);
|
||||
} else if (root.isMango && modelData?.tag !== undefined) {
|
||||
MangoService.toggleTag(root.screenName, modelData.tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1420,7 +1410,7 @@ Item {
|
||||
wsData = modelData || null;
|
||||
} else if (CompositorService.isHyprland) {
|
||||
wsData = modelData;
|
||||
} else if (root.isDwlLike) {
|
||||
} else if (root.isMango) {
|
||||
wsData = modelData;
|
||||
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||
wsData = modelData;
|
||||
@@ -1434,7 +1424,7 @@ Item {
|
||||
}
|
||||
|
||||
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);
|
||||
} else if (CompositorService.isNiri) {
|
||||
delegateRoot.loadedIcons = root.getWorkspaceIcons(isPlaceholder ? null : modelData);
|
||||
@@ -1994,8 +1984,8 @@ Item {
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: root.dwlSvc
|
||||
enabled: root.isDwlLike
|
||||
target: MangoService
|
||||
enabled: root.isMango
|
||||
function onStateChanged() {
|
||||
delegateRoot.updateAllData();
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ Item {
|
||||
borderColor: volumePanel.border.color
|
||||
borderWidth: volumePanel.border.width
|
||||
shadowOpacity: Theme.elevationLevel2 && Theme.elevationLevel2.alpha !== undefined ? Theme.elevationLevel2.alpha : 0.25
|
||||
shadowEnabled: Theme.elevationEnabled && !BlurService.enabled
|
||||
shadowEnabled: Theme.elevationEnabled
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
@@ -272,7 +272,7 @@ Item {
|
||||
borderColor: audioDevicesPanel.border.color
|
||||
borderWidth: audioDevicesPanel.border.width
|
||||
shadowOpacity: Theme.elevationLevel2 && Theme.elevationLevel2.alpha !== undefined ? Theme.elevationLevel2.alpha : 0.25
|
||||
shadowEnabled: Theme.elevationEnabled && !BlurService.enabled
|
||||
shadowEnabled: Theme.elevationEnabled
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
@@ -444,7 +444,7 @@ Item {
|
||||
borderColor: playersPanel.border.color
|
||||
borderWidth: playersPanel.border.width
|
||||
shadowOpacity: Theme.elevationLevel2 && Theme.elevationLevel2.alpha !== undefined ? Theme.elevationLevel2.alpha : 0.25
|
||||
shadowEnabled: Theme.elevationEnabled && !BlurService.enabled
|
||||
shadowEnabled: Theme.elevationEnabled
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
|
||||
@@ -183,7 +183,7 @@ Rectangle {
|
||||
text: {
|
||||
const dateStr = Qt.formatDate(selectedDate, "MMM d");
|
||||
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;
|
||||
@@ -775,7 +775,7 @@ Rectangle {
|
||||
width: parent.width
|
||||
text: {
|
||||
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) {
|
||||
const timeFormat = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP";
|
||||
const startTime = Qt.formatTime(modelData.start, timeFormat);
|
||||
@@ -950,9 +950,8 @@ Rectangle {
|
||||
selectByMouse: true
|
||||
clip: true
|
||||
|
||||
// Hint placeholder 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)
|
||||
visible: !taskInput.text && !taskInput.activeFocus
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
|
||||
@@ -67,9 +67,6 @@ Card {
|
||||
return I18n.tr("on Niri");
|
||||
if (CompositorService.isHyprland)
|
||||
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)
|
||||
return I18n.tr("on MangoWC");
|
||||
if (CompositorService.isSway)
|
||||
@@ -101,9 +98,7 @@ Card {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: DgopService.shortUptime
|
||||
? I18n.tr("up") + DgopService.shortUptime.slice(2)
|
||||
: I18n.tr("up")
|
||||
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'")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
@@ -20,17 +20,25 @@ Card {
|
||||
spacing: Theme.spacingS
|
||||
visible: !WeatherService.weather.available
|
||||
|
||||
DankSpinner {
|
||||
size: 24
|
||||
visible: WeatherService.weather.loading
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: "cloud_off"
|
||||
size: 24
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
visible: !WeatherService.weather.loading
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: WeatherService.weather.loading ? I18n.tr("Loading...") : I18n.tr("No Weather")
|
||||
text: I18n.tr("No Weather")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
visible: !WeatherService.weather.loading
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
|
||||
@@ -186,13 +186,36 @@ Variants {
|
||||
return;
|
||||
}
|
||||
|
||||
const presented = dock.visible && (dock.reveal || slideXAnimation.running || slideYAnimation.running) && dock.hasApps;
|
||||
const phase = !presented ? "hidden" : ((!dock.reveal && (slideXAnimation.running || slideYAnimation.running)) ? "closing" : ((slideXAnimation.running || slideYAnimation.running) ? "opening" : "open"));
|
||||
const bodyX = dock._dockWindowOriginX() + dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x;
|
||||
const bodyY = dock._dockWindowOriginY() + dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y;
|
||||
const bodyW = dock.hasApps ? dockBackground.width : 0;
|
||||
const bodyH = dock.hasApps ? dockBackground.height : 0;
|
||||
ConnectedModeState.setDockState(dock._dockScreenName, {
|
||||
"reveal": dock.visible && (dock.reveal || slideXAnimation.running || slideYAnimation.running) && dock.hasApps,
|
||||
"kind": "dock",
|
||||
"screenName": dock._dockScreenName,
|
||||
"phase": phase,
|
||||
"visible": presented,
|
||||
"presented": presented,
|
||||
"reveal": presented,
|
||||
"barSide": dock.connectedBarSide,
|
||||
"bodyX": dock._dockWindowOriginX() + dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x,
|
||||
"bodyY": dock._dockWindowOriginY() + dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y,
|
||||
"bodyW": dock.hasApps ? dockBackground.width : 0,
|
||||
"bodyH": dock.hasApps ? dockBackground.height : 0,
|
||||
"bodyRect": {
|
||||
"x": bodyX,
|
||||
"y": bodyY,
|
||||
"width": bodyW,
|
||||
"height": bodyH
|
||||
},
|
||||
"animationOffset": {
|
||||
"x": dockSlide.x,
|
||||
"y": dockSlide.y
|
||||
},
|
||||
"scale": 1,
|
||||
"opacity": Theme.connectedSurfaceColor.a,
|
||||
"bodyX": bodyX,
|
||||
"bodyY": bodyY,
|
||||
"bodyW": bodyW,
|
||||
"bodyH": bodyH,
|
||||
"slideX": dockSlide.x,
|
||||
"slideY": dockSlide.y
|
||||
});
|
||||
@@ -724,16 +747,36 @@ Variants {
|
||||
onHeightChanged: dock._syncDockChromeState()
|
||||
}
|
||||
|
||||
ConnectedShape {
|
||||
Item {
|
||||
id: dockConnectedChrome
|
||||
visible: Theme.isConnectedEffect && dock.reveal && !SettingsData.connectedFrameModeActive
|
||||
barSide: dock.connectedBarSide
|
||||
bodyWidth: dockBackground.width
|
||||
bodyHeight: dockBackground.height
|
||||
connectorRadius: Theme.connectedCornerRadius
|
||||
surfaceRadius: dock.surfaceRadius
|
||||
fillColor: dock.surfaceColor
|
||||
x: dockBackground.x - bodyX
|
||||
y: dockBackground.y - bodyY
|
||||
readonly property real extraLeft: dock.isVertical ? 0 : Theme.connectedCornerRadius
|
||||
readonly property real extraTop: dock.isVertical ? Theme.connectedCornerRadius : 0
|
||||
readonly property real bodyRadius: dock.surfaceRadius
|
||||
readonly property bool barTop: dock.connectedBarSide === "top"
|
||||
readonly property bool barBottom: dock.connectedBarSide === "bottom"
|
||||
readonly property bool barLeft: dock.connectedBarSide === "left"
|
||||
readonly property bool barRight: dock.connectedBarSide === "right"
|
||||
|
||||
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 {
|
||||
|
||||
@@ -236,7 +236,7 @@ Item {
|
||||
}
|
||||
|
||||
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
|
||||
width: actualIconSize + SettingsData.dockLauncherLogoSizeOffset
|
||||
height: actualIconSize + SettingsData.dockLauncherLogoSizeOffset
|
||||
@@ -247,8 +247,6 @@ Item {
|
||||
return "file://" + Theme.shellDir + "/assets/niri.svg";
|
||||
} else if (CompositorService.isHyprland) {
|
||||
return "file://" + Theme.shellDir + "/assets/hyprland.svg";
|
||||
} else if (CompositorService.isDwl) {
|
||||
return "file://" + Theme.shellDir + "/assets/mango.png";
|
||||
} else if (CompositorService.isMango) {
|
||||
return "file://" + Theme.shellDir + "/assets/mango.png";
|
||||
} else if (CompositorService.isSway) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import qs.Common
|
||||
|
||||
// Frame perimeter ring with rounded cutout (SDF).
|
||||
Item {
|
||||
id: root
|
||||
|
||||
@@ -16,39 +16,14 @@ Item {
|
||||
required property real cutoutRadius
|
||||
property color borderColor: Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity)
|
||||
|
||||
Rectangle {
|
||||
id: borderRect
|
||||
|
||||
ShaderEffect {
|
||||
anchors.fill: parent
|
||||
// Bake frameOpacity into the color alpha rather than using the `opacity` property
|
||||
color: root.borderColor
|
||||
fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/frame_arc.frag.qsb")
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
maskSource: cutoutMask
|
||||
maskEnabled: true
|
||||
maskInverted: true
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: cutoutMask
|
||||
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
visible: false
|
||||
|
||||
Rectangle {
|
||||
anchors {
|
||||
fill: parent
|
||||
topMargin: root.cutoutTopInset
|
||||
bottomMargin: root.cutoutBottomInset
|
||||
leftMargin: root.cutoutLeftInset
|
||||
rightMargin: root.cutoutRightInset
|
||||
}
|
||||
radius: root.cutoutRadius
|
||||
}
|
||||
property real widthPx: width
|
||||
property real heightPx: height
|
||||
property real cutoutRadius: root.cutoutRadius
|
||||
property vector4d cutout: Qt.vector4d(root.cutoutLeftInset, root.cutoutTopInset, root.width - root.cutoutRightInset, root.height - root.cutoutBottomInset)
|
||||
property vector4d surfaceColor: Qt.vector4d(root.borderColor.r, root.borderColor.g, root.borderColor.b, root.borderColor.a)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -753,9 +753,46 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
TextInput {
|
||||
FocusScope {
|
||||
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.leftMargin: lockIconContainer.width + Theme.spacingM * 2
|
||||
anchors.rightMargin: {
|
||||
@@ -781,7 +818,6 @@ Item {
|
||||
focus: true
|
||||
enabled: !demoMode
|
||||
activeFocusOnTab: !demoMode
|
||||
echoMode: parent.showPassword ? TextInput.Normal : TextInput.Password
|
||||
onTextChanged: {
|
||||
if (!demoMode) {
|
||||
root.passwordBuffer = text;
|
||||
@@ -809,6 +845,8 @@ Item {
|
||||
return;
|
||||
}
|
||||
clear();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (pam.passwd.active) {
|
||||
@@ -816,6 +854,23 @@ Item {
|
||||
event.accepted = true;
|
||||
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: {
|
||||
@@ -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 {
|
||||
|
||||
@@ -31,7 +31,7 @@ Rectangle {
|
||||
height: baseCardHeight + contentItem.extraHeight
|
||||
radius: Theme.cornerRadius
|
||||
clip: false
|
||||
readonly property bool shadowsAllowed: Theme.elevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !BlurService.enabled
|
||||
readonly property bool shadowsAllowed: Theme.elevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||
|
||||
ElevationShadow {
|
||||
id: shadowLayer
|
||||
|
||||
@@ -47,7 +47,7 @@ Rectangle {
|
||||
readonly property real targetHeight: expanded ? (expandedContent.height + cardPadding * 2) : (baseCardHeight + collapsedContent.extraHeight)
|
||||
radius: connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
|
||||
scale: (cardHoverHandler.hovered ? 1.004 : 1.0) * listLevelAdjacentScaleInfluence
|
||||
readonly property bool shadowsAllowed: Theme.elevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !BlurService.enabled
|
||||
readonly property bool shadowsAllowed: Theme.elevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||
readonly property var shadowElevation: Theme.elevationLevel1
|
||||
readonly property real baseShadowBlurPx: (shadowElevation && shadowElevation.blurPx !== undefined) ? shadowElevation.blurPx : 4
|
||||
readonly property real hoverShadowBlurBoost: cardHoverHandler.hovered ? Math.min(2, baseShadowBlurPx * 0.25) : 0
|
||||
|
||||
@@ -641,21 +641,15 @@ PanelWindow {
|
||||
shadowOffsetY: content.shadowOffsetY
|
||||
shadowColor: content.shadowsAllowed && content.elevLevel ? Theme.elevationShadowColor(content.elevLevel) : "transparent"
|
||||
shadowEnabled: !win._isDestroying && win.screenValid && content.shadowsAllowed && !win.connectedFrameMode
|
||||
layer.textureSize: Qt.size(Math.round(width * win.dpr), Math.round(height * win.dpr))
|
||||
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
||||
|
||||
sourceRect.anchors.fill: undefined
|
||||
sourceRect.x: content.shadowRenderPadding + content.cardInset
|
||||
sourceRect.y: content.shadowRenderPadding + content.cardInset
|
||||
sourceRect.width: Math.max(0, content.width - (content.cardInset * 2))
|
||||
sourceRect.height: Math.max(0, content.height - (content.cardInset * 2))
|
||||
sourceRect.radius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
|
||||
sourceRect.color: win.connectedFrameMode ? Theme.floatingSurface : Theme.readableSurface
|
||||
sourceRect.antialiasing: true
|
||||
sourceRect.layer.enabled: false
|
||||
sourceRect.layer.textureSize: Qt.size(0, 0)
|
||||
sourceRect.border.color: notificationData && notificationData.urgency === NotificationUrgency.Critical ? Theme.withAlpha(Theme.primary, 0.3) : Theme.withAlpha(Theme.outline, 0.08)
|
||||
sourceRect.border.width: notificationData && notificationData.urgency === NotificationUrgency.Critical ? 2 : 0
|
||||
sourceX: content.shadowRenderPadding + content.cardInset
|
||||
sourceY: content.shadowRenderPadding + content.cardInset
|
||||
sourceWidth: Math.max(0, content.width - (content.cardInset * 2))
|
||||
sourceHeight: Math.max(0, content.height - (content.cardInset * 2))
|
||||
targetRadius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius
|
||||
targetColor: win.connectedFrameMode ? Theme.floatingSurface : Theme.readableSurface
|
||||
borderColor: win.notificationData && win.notificationData.urgency === NotificationUrgency.Critical ? Theme.withAlpha(Theme.primary, 0.3) : Theme.withAlpha(Theme.outline, 0.08)
|
||||
borderWidth: win.notificationData && win.notificationData.urgency === NotificationUrgency.Critical ? 2 : 0
|
||||
}
|
||||
|
||||
// Keep critical accent outside shadow rendering so connected mode still shows it.
|
||||
@@ -727,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.childrenInherit: true
|
||||
|
||||
@@ -877,10 +916,11 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
StyledText {
|
||||
text: notificationData ? (notificationData.summary || "") : ""
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.pixelSize: SettingsData.notificationSummaryFontSize || Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
@@ -896,7 +936,7 @@ PanelWindow {
|
||||
text: notificationData ? (notificationData.htmlBody || "") : ""
|
||||
textFormat: Text.StyledText
|
||||
color: Theme.surfaceVariantText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.pixelSize: SettingsData.notificationBodyFontSize || Theme.fontSizeSmall
|
||||
width: parent.width
|
||||
elide: descriptionExpanded ? Text.ElideNone : Text.ElideRight
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
|
||||
@@ -513,13 +513,30 @@ QtObject {
|
||||
ConnectedModeState.clearNotificationState(screenName);
|
||||
return;
|
||||
}
|
||||
const bodyRect = {
|
||||
x: minX,
|
||||
y: minY,
|
||||
width: maxXEnd - minX,
|
||||
height: maxYEnd - minY
|
||||
};
|
||||
ConnectedModeState.setNotificationState(screenName, {
|
||||
kind: "notification",
|
||||
screenName: screenName,
|
||||
phase: "open",
|
||||
visible: true,
|
||||
presented: true,
|
||||
barSide: notifBarSide,
|
||||
bodyRect: bodyRect,
|
||||
animationOffset: {
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
scale: 1,
|
||||
opacity: Theme.connectedSurfaceColor.a,
|
||||
bodyX: minX,
|
||||
bodyY: minY,
|
||||
bodyW: maxXEnd - minX,
|
||||
bodyH: maxYEnd - minY,
|
||||
bodyW: bodyRect.width,
|
||||
bodyH: bodyRect.height,
|
||||
omitStartConnector: _notificationOmitStartConnector(),
|
||||
omitEndConnector: _notificationOmitEndConnector()
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@ Item {
|
||||
property bool conditionVisible: true
|
||||
property bool _visibilityOverride: false
|
||||
property bool _visibilityOverrideValue: true
|
||||
readonly property bool _barRevealed: blurBarWindow?.barRevealed ?? true
|
||||
|
||||
readonly property bool effectiveVisible: {
|
||||
if (_visibilityOverride)
|
||||
@@ -122,6 +123,11 @@ Item {
|
||||
conditionVisible = true;
|
||||
}
|
||||
|
||||
on_BarRevealedChanged: {
|
||||
if (_barRevealed && visibilityCommand && !_visibilityOverride)
|
||||
checkVisibility();
|
||||
}
|
||||
|
||||
onVisibilityIntervalChanged: {
|
||||
if (visibilityInterval > 0 && visibilityCommand) {
|
||||
visibilityTimer.restart();
|
||||
@@ -134,7 +140,7 @@ Item {
|
||||
id: visibilityTimer
|
||||
interval: root.visibilityInterval * 1000
|
||||
repeat: true
|
||||
running: root.visibilityInterval > 0 && root.visibilityCommand !== ""
|
||||
running: root.visibilityInterval > 0 && root.visibilityCommand !== "" && root._barRevealed && !root._visibilityOverride
|
||||
onTriggered: root.checkVisibility()
|
||||
}
|
||||
|
||||
@@ -324,6 +330,24 @@ Item {
|
||||
pluginPopout.toggle();
|
||||
}
|
||||
|
||||
function triggerHoverPopout(widgetHostId) {
|
||||
if (pillClickAction) {
|
||||
triggerPopout();
|
||||
return;
|
||||
}
|
||||
if (!hasPopout)
|
||||
return;
|
||||
|
||||
const pill = isVertical ? verticalPill : horizontalPill;
|
||||
const globalPos = pill.visualContent.mapToItem(null, 0, 0);
|
||||
const currentScreen = parentScreen || Screen;
|
||||
const barPosition = axis?.edge === "left" ? 2 : (axis?.edge === "right" ? 3 : (axis?.edge === "top" ? 0 : 1));
|
||||
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, pill.visualWidth, barSpacing, barPosition, barConfig);
|
||||
|
||||
pluginPopout.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen, barPosition, barThickness, barSpacing, barConfig);
|
||||
PopoutManager.requestHoverPopout(pluginPopout, undefined, widgetHostId || pluginId);
|
||||
}
|
||||
|
||||
PluginPopout {
|
||||
id: pluginPopout
|
||||
contentWidth: root.popoutWidth
|
||||
|
||||
@@ -15,7 +15,7 @@ Item {
|
||||
property bool isSway: CompositorService.isSway
|
||||
property bool isScroll: CompositorService.isScroll
|
||||
property bool isMiracle: CompositorService.isMiracle
|
||||
property bool isDwl: CompositorService.isDwl || CompositorService.isMango
|
||||
property bool isMango: CompositorService.isMango
|
||||
property bool isLabwc: CompositorService.isLabwc
|
||||
|
||||
property string compositorName: {
|
||||
@@ -27,7 +27,7 @@ Item {
|
||||
return "scroll";
|
||||
if (isMiracle)
|
||||
return "miracle";
|
||||
if (isDwl)
|
||||
if (isMango)
|
||||
return "mangowc";
|
||||
if (isLabwc)
|
||||
return "labwc";
|
||||
@@ -43,7 +43,7 @@ Item {
|
||||
return "/assets/sway.svg";
|
||||
if (isMiracle)
|
||||
return "/assets/miraclewm.svg";
|
||||
if (isDwl)
|
||||
if (isMango)
|
||||
return "/assets/mango.png";
|
||||
if (isLabwc)
|
||||
return "/assets/labwc.png";
|
||||
@@ -59,7 +59,7 @@ Item {
|
||||
return "https://github.com/dawsers/scroll";
|
||||
if (isMiracle)
|
||||
return "https://github.com/miracle-wm-org/miracle-wm";
|
||||
if (isDwl)
|
||||
if (isMango)
|
||||
return "https://github.com/DreamMaoMao/mangowc";
|
||||
if (isLabwc)
|
||||
return "https://labwc.github.io/";
|
||||
@@ -75,7 +75,7 @@ Item {
|
||||
return I18n.tr("Scroll GitHub");
|
||||
if (isMiracle)
|
||||
return I18n.tr("Scroll GitHub");
|
||||
if (isDwl)
|
||||
if (isMango)
|
||||
return I18n.tr("mangowc GitHub");
|
||||
if (isLabwc)
|
||||
return I18n.tr("LabWC Website");
|
||||
@@ -88,7 +88,7 @@ Item {
|
||||
property string compositorDiscordUrl: {
|
||||
if (isHyprland)
|
||||
return "https://discord.com/invite/hQ9XvMUjjr";
|
||||
if (isDwl)
|
||||
if (isMango)
|
||||
return "https://discord.gg/CPjbDxesh5";
|
||||
return "";
|
||||
}
|
||||
@@ -96,7 +96,7 @@ Item {
|
||||
property string compositorDiscordTooltip: {
|
||||
if (isHyprland)
|
||||
return I18n.tr("Hyprland Discord Server");
|
||||
if (isDwl)
|
||||
if (isMango)
|
||||
return I18n.tr("mangowc Discord Server");
|
||||
return "";
|
||||
}
|
||||
@@ -107,9 +107,9 @@ Item {
|
||||
property string ircUrl: "https://web.libera.chat/gamja/?channels=#labwc"
|
||||
property string ircTooltip: I18n.tr("LabWC IRC Channel")
|
||||
|
||||
property bool showMatrix: isNiri && !isHyprland && !isSway && !isScroll && !isMiracle && !isDwl && !isLabwc
|
||||
property bool showCompositorDiscord: isHyprland || isDwl
|
||||
property bool showReddit: isNiri && !isHyprland && !isSway && !isScroll && !isMiracle && !isDwl && !isLabwc
|
||||
property bool showMatrix: isNiri && !isHyprland && !isSway && !isScroll && !isMiracle && !isMango && !isLabwc
|
||||
property bool showCompositorDiscord: isHyprland || isMango
|
||||
property bool showReddit: isNiri && !isHyprland && !isSway && !isScroll && !isMiracle && !isMango && !isLabwc
|
||||
property bool showIrc: isLabwc
|
||||
|
||||
DankFlickable {
|
||||
|
||||
@@ -722,7 +722,7 @@ Item {
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "system_tray"
|
||||
iconName: "handyman"
|
||||
title: I18n.tr("Tray Icon Fix")
|
||||
visible: DesktopService.isSystemd
|
||||
|
||||
|
||||
@@ -152,6 +152,9 @@ Item {
|
||||
}
|
||||
]
|
||||
|
||||
readonly property var entryActionKeys: ["pin", "edit", "delete"]
|
||||
readonly property var entryActionLabels: [I18n.tr("Pin"), I18n.tr("Edit"), I18n.tr("Delete")]
|
||||
|
||||
function getMaxHistoryText(value) {
|
||||
if (value <= 0)
|
||||
return "∞";
|
||||
@@ -187,6 +190,29 @@ Item {
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
function visibleEntryActionKeys() {
|
||||
return SettingsData.clipboardVisibleEntryActions || ["pin", "edit", "delete"];
|
||||
}
|
||||
|
||||
function visibleEntryActionLabels() {
|
||||
const visibleKeys = visibleEntryActionKeys();
|
||||
return entryActionKeys.map((key, index) => visibleKeys.includes(key) ? entryActionLabels[index] : null).filter(label => label !== null);
|
||||
}
|
||||
|
||||
function setVisibleEntryAction(index, selected) {
|
||||
const actionKey = entryActionKeys[index];
|
||||
if (!actionKey)
|
||||
return;
|
||||
|
||||
let actions = visibleEntryActionKeys().slice();
|
||||
if (selected && !actions.includes(actionKey)) {
|
||||
actions.push(actionKey);
|
||||
} else if (!selected && actions.includes(actionKey)) {
|
||||
actions = actions.filter(action => action !== actionKey);
|
||||
}
|
||||
SettingsData.set("clipboardVisibleEntryActions", actions);
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
configLoaded = false;
|
||||
configError = false;
|
||||
@@ -437,6 +463,24 @@ Item {
|
||||
checked: SettingsData.clipboardEnterToPaste
|
||||
onToggled: checked => SettingsData.set("clipboardEnterToPaste", checked)
|
||||
}
|
||||
|
||||
SettingsButtonGroupRow {
|
||||
tab: "clipboard"
|
||||
tags: ["clipboard", "actions", "buttons", "hide", "density", "pin", "edit", "delete"]
|
||||
settingKey: "clipboardVisibleEntryActions"
|
||||
text: I18n.tr("Visible Entry Actions")
|
||||
description: I18n.tr("Choose which action buttons appear on clipboard entries")
|
||||
selectionMode: "multi"
|
||||
model: root.entryActionLabels
|
||||
currentSelection: root.visibleEntryActionLabels()
|
||||
checkEnabled: false
|
||||
buttonHeight: 28
|
||||
minButtonWidth: 56
|
||||
buttonPadding: Theme.spacingS
|
||||
textSize: Theme.fontSizeSmall
|
||||
spacing: 1
|
||||
onSelectionChanged: (index, selected) => root.setVisibleEntryAction(index, selected)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
|
||||
@@ -23,9 +23,9 @@ Item {
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
tags: ["niri", "layout", "gaps", "radius", "window", "border"]
|
||||
title: I18n.tr("Niri Layout Overrides").replace("Niri", "niri")
|
||||
title: I18n.tr("Niri Layout Overrides")
|
||||
settingKey: "niriLayout"
|
||||
iconName: "crop_square"
|
||||
iconName: "layers"
|
||||
visible: CompositorService.isNiri
|
||||
|
||||
SettingsToggleRow {
|
||||
@@ -145,7 +145,7 @@ Item {
|
||||
tags: ["hyprland", "gaps", "override"]
|
||||
settingKey: "hyprlandLayoutGapsOverride"
|
||||
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
|
||||
value: Math.max(0, SettingsData.hyprlandLayoutGapsOverride)
|
||||
minimum: 0
|
||||
@@ -159,7 +159,7 @@ Item {
|
||||
tags: ["hyprland", "radius", "override", "rounding"]
|
||||
settingKey: "hyprlandLayoutRadiusOverrideEnabled"
|
||||
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
|
||||
onToggled: checked => {
|
||||
if (checked) {
|
||||
@@ -173,8 +173,8 @@ Item {
|
||||
SettingsSliderRow {
|
||||
tags: ["hyprland", "radius", "override", "rounding"]
|
||||
settingKey: "hyprlandLayoutRadiusOverride"
|
||||
text: I18n.tr("Window Rounding")
|
||||
description: I18n.tr("Rounded corners for windows (decoration.rounding)")
|
||||
text: I18n.tr("Window Corner Radius")
|
||||
description: I18n.tr("Rounded corners for windows") + " (decoration.rounding)"
|
||||
visible: SettingsData.hyprlandLayoutRadiusOverride >= 0
|
||||
value: Math.max(0, SettingsData.hyprlandLayoutRadiusOverride)
|
||||
minimum: 0
|
||||
@@ -203,7 +203,7 @@ Item {
|
||||
tags: ["hyprland", "border", "override"]
|
||||
settingKey: "hyprlandLayoutBorderSize"
|
||||
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
|
||||
value: Math.max(0, SettingsData.hyprlandLayoutBorderSize)
|
||||
minimum: 0
|
||||
@@ -229,7 +229,7 @@ Item {
|
||||
title: I18n.tr("MangoWC Layout Overrides")
|
||||
settingKey: "mangoLayout"
|
||||
iconName: "crop_square"
|
||||
visible: CompositorService.isDwl || CompositorService.isMango
|
||||
visible: CompositorService.isMango
|
||||
|
||||
SettingsToggleRow {
|
||||
tags: ["mangowc", "mango", "gaps", "override"]
|
||||
@@ -251,7 +251,7 @@ Item {
|
||||
tags: ["mangowc", "mango", "gaps", "override"]
|
||||
settingKey: "mangoLayoutGapsOverride"
|
||||
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
|
||||
value: Math.max(0, SettingsData.mangoLayoutGapsOverride)
|
||||
minimum: 0
|
||||
@@ -280,7 +280,7 @@ Item {
|
||||
tags: ["mangowc", "mango", "radius", "override"]
|
||||
settingKey: "mangoLayoutRadiusOverride"
|
||||
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
|
||||
value: Math.max(0, SettingsData.mangoLayoutRadiusOverride)
|
||||
minimum: 0
|
||||
@@ -309,7 +309,7 @@ Item {
|
||||
tags: ["mangowc", "mango", "border", "override"]
|
||||
settingKey: "mangoLayoutBorderSize"
|
||||
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
|
||||
value: Math.max(0, SettingsData.mangoLayoutBorderSize)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,8 +41,8 @@ Item {
|
||||
SettingsData.barConfigs;
|
||||
const index = SettingsData.barConfigs.findIndex(config => config.id === selectedBarId);
|
||||
if (index < 0)
|
||||
return I18n.tr("Bar");
|
||||
return SettingsData.barConfigs[index].name || I18n.tr("Bar %1").arg(index + 1);
|
||||
return I18n.tr("Bar", "fallback name for an unnamed bar");
|
||||
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: {
|
||||
@@ -164,6 +164,7 @@ Item {
|
||||
scrollEnabled: defaultBar.scrollEnabled ?? true,
|
||||
scrollXBehavior: defaultBar.scrollXBehavior ?? "column",
|
||||
scrollYBehavior: defaultBar.scrollYBehavior ?? "workspace",
|
||||
hoverPopouts: defaultBar.hoverPopouts ?? false,
|
||||
shadowIntensity: defaultBar.shadowIntensity ?? 0,
|
||||
shadowOpacity: defaultBar.shadowOpacity ?? 60,
|
||||
shadowDirectionMode: defaultBar.shadowDirectionMode ?? "inherit",
|
||||
@@ -796,18 +797,81 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
tab: "appearance"
|
||||
iconName: "opacity"
|
||||
title: I18n.tr("Opacity")
|
||||
settingKey: "barTransparency"
|
||||
visible: dankBarTab.appearanceOnly && selectedBarConfig?.enabled
|
||||
|
||||
SettingsSliderRow {
|
||||
id: barTransparencySlider
|
||||
visible: !SettingsData.frameEnabled
|
||||
text: I18n.tr("Bar Opacity")
|
||||
description: I18n.tr("Controls opacity of the bar background")
|
||||
value: (selectedBarConfig?.transparency ?? 1.0) * 100
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
unit: "%"
|
||||
defaultValue: 100
|
||||
onSliderDragFinished: finalValue => {
|
||||
SettingsData.updateBarConfig(selectedBarId, {
|
||||
transparency: finalValue / 100
|
||||
});
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: barTransparencySlider
|
||||
property: "value"
|
||||
value: (selectedBarConfig?.transparency ?? 1.0) * 100
|
||||
restoreMode: Binding.RestoreBinding
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
id: widgetTransparencySlider
|
||||
text: I18n.tr("Widget Opacity")
|
||||
description: I18n.tr("Controls opacity of widget backgrounds")
|
||||
value: (selectedBarConfig?.widgetTransparency ?? 1.0) * 100
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
unit: "%"
|
||||
defaultValue: 100
|
||||
onSliderDragFinished: finalValue => {
|
||||
SettingsData.updateBarConfig(selectedBarId, {
|
||||
widgetTransparency: finalValue / 100
|
||||
});
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: widgetTransparencySlider
|
||||
property: "value"
|
||||
value: (selectedBarConfig?.widgetTransparency ?? 1.0) * 100
|
||||
restoreMode: Binding.RestoreBinding
|
||||
}
|
||||
}
|
||||
|
||||
SettingsControlledByFrame {
|
||||
visible: SettingsData.frameEnabled
|
||||
parentModal: dankBarTab.parentModal
|
||||
settingLabel: I18n.tr("Bar Opacity")
|
||||
reason: I18n.tr("Managed by Frame")
|
||||
}
|
||||
}
|
||||
|
||||
SettingsControlledByFrame {
|
||||
visible: !dankBarTab.appearanceOnly && SettingsData.frameEnabled
|
||||
visible: dankBarTab.appearanceOnly && SettingsData.frameEnabled
|
||||
parentModal: dankBarTab.parentModal
|
||||
settingLabel: I18n.tr("Bar spacing and size")
|
||||
reason: I18n.tr("Managed by Frame")
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
tab: "appearance"
|
||||
iconName: "space_bar"
|
||||
title: I18n.tr("Spacing")
|
||||
settingKey: "barSpacing"
|
||||
visible: !dankBarTab.appearanceOnly && (selectedBarConfig?.enabled ?? false) && !SettingsData.frameEnabled
|
||||
visible: dankBarTab.appearanceOnly && (selectedBarConfig?.enabled ?? false) && !SettingsData.frameEnabled
|
||||
|
||||
SettingsSliderRow {
|
||||
id: edgeSpacingSlider
|
||||
@@ -956,68 +1020,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
tab: "appearance"
|
||||
iconName: "opacity"
|
||||
title: I18n.tr("Transparency")
|
||||
settingKey: "barTransparency"
|
||||
visible: dankBarTab.appearanceOnly && selectedBarConfig?.enabled
|
||||
|
||||
SettingsSliderRow {
|
||||
id: barTransparencySlider
|
||||
visible: !SettingsData.frameEnabled
|
||||
text: I18n.tr("Bar Transparency")
|
||||
description: I18n.tr("Opacity of the bar background")
|
||||
value: (selectedBarConfig?.transparency ?? 1.0) * 100
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
unit: "%"
|
||||
defaultValue: 100
|
||||
onSliderDragFinished: finalValue => {
|
||||
SettingsData.updateBarConfig(selectedBarId, {
|
||||
transparency: finalValue / 100
|
||||
});
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: barTransparencySlider
|
||||
property: "value"
|
||||
value: (selectedBarConfig?.transparency ?? 1.0) * 100
|
||||
restoreMode: Binding.RestoreBinding
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
id: widgetTransparencySlider
|
||||
text: I18n.tr("Widget Transparency")
|
||||
description: I18n.tr("Opacity of widget backgrounds")
|
||||
value: (selectedBarConfig?.widgetTransparency ?? 1.0) * 100
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
unit: "%"
|
||||
defaultValue: 100
|
||||
onSliderDragFinished: finalValue => {
|
||||
SettingsData.updateBarConfig(selectedBarId, {
|
||||
widgetTransparency: finalValue / 100
|
||||
});
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: widgetTransparencySlider
|
||||
property: "value"
|
||||
value: (selectedBarConfig?.widgetTransparency ?? 1.0) * 100
|
||||
restoreMode: Binding.RestoreBinding
|
||||
}
|
||||
}
|
||||
|
||||
SettingsControlledByFrame {
|
||||
visible: SettingsData.frameEnabled
|
||||
parentModal: dankBarTab.parentModal
|
||||
settingLabel: I18n.tr("Bar Transparency")
|
||||
reason: I18n.tr("Managed by Frame")
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSliderCard {
|
||||
id: fontScaleSliderCard
|
||||
tab: "appearance"
|
||||
@@ -1358,7 +1360,7 @@ Item {
|
||||
SettingsSliderRow {
|
||||
id: borderOpacitySlider
|
||||
text: I18n.tr("Opacity")
|
||||
description: I18n.tr("Transparency of the border")
|
||||
description: I18n.tr("Controls opacity of the border")
|
||||
value: (selectedBarConfig?.borderOpacity ?? 1.0) * 100
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
@@ -1453,7 +1455,7 @@ Item {
|
||||
SettingsSliderRow {
|
||||
id: widgetOutlineOpacitySlider
|
||||
text: I18n.tr("Opacity")
|
||||
description: I18n.tr("Transparency of the widget outline")
|
||||
description: I18n.tr("Controls opacity of the widget outline")
|
||||
value: (selectedBarConfig?.widgetOutlineOpacity ?? 1.0) * 100
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
@@ -1562,7 +1564,7 @@ Item {
|
||||
SettingsSliderRow {
|
||||
visible: shadowCard.shadowActive
|
||||
text: I18n.tr("Opacity")
|
||||
description: I18n.tr("Transparency of the shadow layer")
|
||||
description: I18n.tr("Controls opacity of the shadow layer")
|
||||
minimum: 10
|
||||
maximum: 100
|
||||
unit: "%"
|
||||
@@ -1740,6 +1742,19 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
SettingsToggleCard {
|
||||
iconName: "touch_app"
|
||||
title: I18n.tr("Hover Popouts")
|
||||
description: I18n.tr("Open widget popouts by hovering over the bar. Moving to another widget switches the popout.")
|
||||
visible: !dankBarTab.appearanceOnly && selectedBarConfig?.enabled
|
||||
enabled: !(selectedBarConfig?.clickThrough ?? false)
|
||||
opacity: (selectedBarConfig?.clickThrough ?? false) ? 0.5 : 1.0
|
||||
checked: selectedBarConfig?.hoverPopouts ?? false
|
||||
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
||||
hoverPopouts: checked
|
||||
})
|
||||
}
|
||||
|
||||
SettingsToggleCard {
|
||||
iconName: "mouse"
|
||||
title: I18n.tr("Scroll Wheel")
|
||||
@@ -1754,6 +1769,9 @@ Item {
|
||||
text: I18n.tr("Y Axis")
|
||||
description: I18n.tr("Action performed when scrolling vertically on the bar")
|
||||
model: CompositorService.isNiri ? [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")] : [I18n.tr("None"), I18n.tr("Workspace")]
|
||||
buttonPadding: Theme.spacingS
|
||||
minButtonWidth: 44
|
||||
textSize: Theme.fontSizeSmall
|
||||
currentIndex: {
|
||||
switch (selectedBarConfig?.scrollYBehavior || "workspace") {
|
||||
case "none":
|
||||
@@ -1792,6 +1810,9 @@ Item {
|
||||
description: I18n.tr("Action performed when scrolling horizontally on the bar")
|
||||
visible: CompositorService.isNiri
|
||||
model: [I18n.tr("None"), I18n.tr("Workspace"), I18n.tr("Column")]
|
||||
buttonPadding: Theme.spacingS
|
||||
minButtonWidth: 44
|
||||
textSize: Theme.fontSizeSmall
|
||||
currentIndex: {
|
||||
switch (selectedBarConfig?.scrollXBehavior || "column") {
|
||||
case "none":
|
||||
|
||||
@@ -1023,7 +1023,6 @@ Singleton {
|
||||
return parseNiriOutputs(content);
|
||||
case "hyprland":
|
||||
return parseHyprlandOutputs(content);
|
||||
case "dwl":
|
||||
case "mango":
|
||||
return parseMangoOutputs(content);
|
||||
default:
|
||||
@@ -1362,7 +1361,6 @@ Singleton {
|
||||
"grepPattern": "dms.outputs",
|
||||
"includeLine": "require(\"dms.outputs\")"
|
||||
};
|
||||
case "dwl":
|
||||
case "mango":
|
||||
return {
|
||||
"configFile": configDir + "/mango/config.conf",
|
||||
@@ -1377,7 +1375,7 @@ Singleton {
|
||||
|
||||
function checkIncludeStatus() {
|
||||
const compositor = CompositorService.compositor;
|
||||
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "dwl" && compositor !== "mango") {
|
||||
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "mango") {
|
||||
includeStatus = {
|
||||
"exists": false,
|
||||
"included": false,
|
||||
@@ -1388,8 +1386,7 @@ Singleton {
|
||||
}
|
||||
|
||||
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 === "dwl" || compositor === "mango") ? "mangowc" : compositor;
|
||||
const compositorArg = (compositor === "mango") ? "mangowc" : compositor;
|
||||
|
||||
checkingInclude = true;
|
||||
Proc.runCommand("check-outputs-include", ["dms", "config", "resolve-include", compositorArg, filename], (output, exitCode) => {
|
||||
@@ -1589,9 +1586,6 @@ Singleton {
|
||||
case "mango":
|
||||
MangoService.generateOutputsConfig(outputsData, finish);
|
||||
break;
|
||||
case "dwl":
|
||||
DwlService.generateOutputsConfig(outputsData, finish);
|
||||
break;
|
||||
default:
|
||||
WlrOutputService.applyOutputsConfig(outputsData, outputs);
|
||||
finish(true);
|
||||
|
||||
@@ -317,7 +317,7 @@ StyledRect {
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
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: {
|
||||
const pendingVrr = DisplayConfigState.getPendingValue(root.outputName, "vrr");
|
||||
if (pendingVrr !== undefined)
|
||||
|
||||
@@ -500,7 +500,7 @@ Item {
|
||||
|
||||
Column {
|
||||
id: displayFormatColumn
|
||||
visible: !CompositorService.isDwl && !CompositorService.isMango
|
||||
visible: !CompositorService.isMango
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
|
||||
@@ -282,8 +282,6 @@ Item {
|
||||
modes.push("niri");
|
||||
} else if (CompositorService.isHyprland) {
|
||||
modes.push("Hyprland");
|
||||
} else if (CompositorService.isDwl) {
|
||||
modes.push("mango");
|
||||
} else if (CompositorService.isMango) {
|
||||
modes.push("mango");
|
||||
} else if (CompositorService.isSway) {
|
||||
@@ -645,19 +643,19 @@ Item {
|
||||
SettingsControlledByFrame {
|
||||
visible: root.connectedFrameModeActive
|
||||
parentModal: root.parentModal
|
||||
settingLabel: I18n.tr("Dock margin, transparency, and border")
|
||||
settingLabel: I18n.tr("Dock margin, opacity, and border")
|
||||
reason: I18n.tr("Managed by Frame in Connected Mode")
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "opacity"
|
||||
title: I18n.tr("Transparency")
|
||||
title: I18n.tr("Opacity")
|
||||
settingKey: "dockTransparency"
|
||||
visible: !root.connectedFrameModeActive
|
||||
|
||||
SettingsSliderRow {
|
||||
text: I18n.tr("Dock Transparency")
|
||||
text: I18n.tr("Dock Opacity")
|
||||
value: Math.round(SettingsData.dockTransparency * 100)
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
|
||||
@@ -205,6 +205,9 @@ Item {
|
||||
tags: ["frame", "border", "color", "theme", "primary", "surface", "default"]
|
||||
text: I18n.tr("Border Color")
|
||||
model: [I18n.tr("Default"), I18n.tr("Primary"), I18n.tr("Surface"), I18n.tr("Custom")]
|
||||
buttonPadding: Theme.spacingS
|
||||
minButtonWidth: 44
|
||||
textSize: Theme.fontSizeSmall
|
||||
currentIndex: {
|
||||
const fc = SettingsData.frameColor;
|
||||
if (!fc || fc === "default")
|
||||
|
||||
@@ -151,7 +151,7 @@ Item {
|
||||
|
||||
function runGreeterInstallAction() {
|
||||
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;
|
||||
greeterInstallActionProcess.running = true;
|
||||
}
|
||||
@@ -188,7 +188,7 @@ Item {
|
||||
greeterSudoProbeStderr = "";
|
||||
greeterTerminalFallbackStderr = "";
|
||||
greeterTerminalFallbackFromPrecheck = false;
|
||||
greeterStatusText = I18n.tr("Checking whether sudo authentication is needed…");
|
||||
greeterStatusText = I18n.tr("Checking whether sudo authentication is needed...");
|
||||
greeterSyncRunning = true;
|
||||
greeterSudoProbeProcess.running = true;
|
||||
}
|
||||
@@ -327,7 +327,7 @@ Item {
|
||||
onExited: exitCode => {
|
||||
const err = (root.greeterSudoProbeStderr || "").trim();
|
||||
if (exitCode === 0) {
|
||||
root.greeterStatusText = I18n.tr("Running greeter sync…");
|
||||
root.greeterStatusText = I18n.tr("Running greeter sync...");
|
||||
greeterSyncProcess.running = true;
|
||||
return;
|
||||
}
|
||||
@@ -468,7 +468,7 @@ Item {
|
||||
id: statusTextArea
|
||||
anchors.fill: parent
|
||||
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.family: "monospace"
|
||||
color: root.greeterStatusRunning ? Theme.surfaceVariantText : Theme.surfaceText
|
||||
|
||||
@@ -304,8 +304,6 @@ Item {
|
||||
modes.push("niri");
|
||||
} else if (CompositorService.isHyprland) {
|
||||
modes.push("Hyprland");
|
||||
} else if (CompositorService.isDwl) {
|
||||
modes.push("mango");
|
||||
} else if (CompositorService.isMango) {
|
||||
modes.push("mango");
|
||||
} else if (CompositorService.isSway) {
|
||||
|
||||
@@ -643,41 +643,9 @@ Item {
|
||||
height: NetworkService.networkWiredInfoLoading ? 40 : 0
|
||||
visible: NetworkService.networkWiredInfoLoading
|
||||
|
||||
Row {
|
||||
DankSpinner {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
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
|
||||
}
|
||||
size: 20
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1360,41 +1328,9 @@ Item {
|
||||
height: NetworkService.networkInfoLoading ? 40 : 0
|
||||
visible: NetworkService.networkInfoLoading
|
||||
|
||||
Row {
|
||||
DankSpinner {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
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
|
||||
}
|
||||
size: 20
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1849,41 +1785,9 @@ Item {
|
||||
height: VPNService.configLoading ? 40 : 0
|
||||
visible: VPNService.configLoading
|
||||
|
||||
Row {
|
||||
DankSpinner {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
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
|
||||
}
|
||||
size: 20
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -200,12 +200,40 @@ Item {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "notifications"
|
||||
title: I18n.tr("Notification Popups")
|
||||
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 {
|
||||
settingKey: "notificationPopupPosition"
|
||||
tags: ["notification", "popup", "position", "screen", "location"]
|
||||
@@ -273,6 +301,15 @@ Item {
|
||||
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 {
|
||||
settingKey: "notificationDedupeEnabled"
|
||||
tags: ["notification", "duplicate", "dedupe", "stack", "coalesce", "repeat"]
|
||||
|
||||
@@ -33,11 +33,31 @@ FloatingWindow {
|
||||
}
|
||||
|
||||
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: "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 }
|
||||
{
|
||||
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: "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) {
|
||||
@@ -107,11 +127,13 @@ FloatingWindow {
|
||||
counts[cat] = (counts[cat] || 0) + 1;
|
||||
}
|
||||
var keys = Object.keys(counts).sort();
|
||||
var options = [{
|
||||
key: "all",
|
||||
label: I18n.tr("All", "plugin browser category filter"),
|
||||
count: plugins.length
|
||||
}];
|
||||
var options = [
|
||||
{
|
||||
key: "all",
|
||||
label: I18n.tr("All", "plugin browser category filter"),
|
||||
count: plugins.length
|
||||
}
|
||||
];
|
||||
for (var j = 0; j < keys.length; j++) {
|
||||
var key = keys[j];
|
||||
options.push({
|
||||
@@ -726,32 +748,9 @@ FloatingWindow {
|
||||
anchors.fill: parent
|
||||
visible: root.isLoading
|
||||
|
||||
Column {
|
||||
DankSpinner {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
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
|
||||
}
|
||||
running: root.isLoading
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -335,32 +335,9 @@ FloatingWindow {
|
||||
anchors.fill: parent
|
||||
visible: root.isLoading
|
||||
|
||||
Column {
|
||||
DankSpinner {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
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
|
||||
}
|
||||
running: root.isLoading
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ Item {
|
||||
"grepPattern": "dms.cursor",
|
||||
"includeLine": "require(\"dms.cursor\")"
|
||||
};
|
||||
case "dwl":
|
||||
case "mango":
|
||||
return {
|
||||
"configFile": configDir + "/mango/config.conf",
|
||||
@@ -63,7 +62,7 @@ Item {
|
||||
|
||||
function checkCursorIncludeStatus() {
|
||||
const compositor = CompositorService.compositor;
|
||||
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "dwl" && compositor !== "mango") {
|
||||
if (compositor !== "niri" && compositor !== "hyprland" && compositor !== "mango") {
|
||||
cursorIncludeStatus = {
|
||||
"exists": false,
|
||||
"included": false,
|
||||
@@ -74,7 +73,7 @@ Item {
|
||||
}
|
||||
|
||||
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;
|
||||
Proc.runCommand("check-cursor-include", ["dms", "config", "resolve-include", compositorArg, filename], (output, exitCode) => {
|
||||
@@ -194,7 +193,7 @@ Item {
|
||||
themeColorsTab.templateDetection = JSON.parse(output.trim());
|
||||
} catch (e) {}
|
||||
});
|
||||
if (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango)
|
||||
if (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango)
|
||||
checkCursorIncludeStatus();
|
||||
}
|
||||
|
||||
@@ -1640,7 +1639,7 @@ Item {
|
||||
SettingsControlledByFrame {
|
||||
visible: themeColorsTab.connectedFrameModeActive
|
||||
parentModal: themeColorsTab.parentModal
|
||||
settingLabel: I18n.tr("Transparency")
|
||||
settingLabel: I18n.tr("Surface Opacity")
|
||||
reason: I18n.tr("Managed by Frame in Connected Mode")
|
||||
}
|
||||
|
||||
@@ -1648,8 +1647,8 @@ Item {
|
||||
tab: "theme"
|
||||
tags: ["surface", "popup", "transparency", "opacity", "modal"]
|
||||
settingKey: "popupTransparency"
|
||||
text: I18n.tr("Transparency")
|
||||
description: I18n.tr("Controls opacity of all popouts, modals, and their content layers")
|
||||
text: I18n.tr("Surface Opacity")
|
||||
description: I18n.tr("Controls opacity of shell surfaces, popouts, and modals")
|
||||
visible: !themeColorsTab.connectedFrameModeActive
|
||||
value: Math.round(SettingsData.popupTransparency * 100)
|
||||
minimum: 0
|
||||
@@ -1672,6 +1671,113 @@ Item {
|
||||
defaultValue: 12
|
||||
onSliderValueChanged: newValue => SettingsData.setCornerRadius(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
tab: "theme"
|
||||
tags: ["blur", "background", "transparency", "glass", "frosted"]
|
||||
title: I18n.tr("Background Blur")
|
||||
settingKey: "blurEnabled"
|
||||
iconName: "blur_on"
|
||||
|
||||
SettingsToggleRow {
|
||||
tab: "theme"
|
||||
tags: ["blur", "background", "transparency", "glass", "frosted"]
|
||||
settingKey: "blurEnabled"
|
||||
text: I18n.tr("Background Blur")
|
||||
description: !BlurService.available ? I18n.tr("Your compositor does not support background blur (ext-background-effect-v1)") : I18n.tr("Blur the background behind bars, popouts, modals, and notifications. Requires compositor support. Adjust Opacity accordingly.")
|
||||
checked: SettingsData.blurEnabled ?? false
|
||||
enabled: BlurService.available
|
||||
onToggled: checked => SettingsData.set("blurEnabled", checked)
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
tab: "theme"
|
||||
tags: ["blur", "foreground", "layers", "contrast", "glass", "frosted"]
|
||||
settingKey: "blurForegroundLayers"
|
||||
text: I18n.tr("Foreground Layers")
|
||||
description: I18n.tr("Show foreground surfaces on blurred panels for stronger contrast")
|
||||
checked: SettingsData.blurForegroundLayers ?? true
|
||||
visible: BlurService.available && (SettingsData.blurEnabled ?? false)
|
||||
enabled: BlurService.available
|
||||
onToggled: checked => SettingsData.set("blurForegroundLayers", checked)
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
tab: "theme"
|
||||
tags: ["blur", "foreground", "layers", "outline", "border", "cards", "widgets", "notifications", "control center"]
|
||||
settingKey: "blurLayerOutlineOpacity"
|
||||
text: I18n.tr("Layer Outline Opacity")
|
||||
description: I18n.tr("Controls outlines around blurred foreground cards, pills, and notification cards")
|
||||
visible: BlurService.available && (SettingsData.blurEnabled ?? false)
|
||||
value: Math.round((SettingsData.blurLayerOutlineOpacity ?? 0.12) * 100)
|
||||
minimum: 0
|
||||
maximum: 40
|
||||
unit: "%"
|
||||
defaultValue: 12
|
||||
onSliderValueChanged: newValue => SettingsData.set("blurLayerOutlineOpacity", newValue / 100)
|
||||
}
|
||||
|
||||
SettingsDropdownRow {
|
||||
tab: "theme"
|
||||
tags: ["blur", "border", "outline", "edge"]
|
||||
settingKey: "blurBorderColor"
|
||||
text: I18n.tr("Blur Border Color")
|
||||
description: I18n.tr("Border color around blurred surfaces")
|
||||
visible: SettingsData.blurEnabled
|
||||
options: [I18n.tr("Outline", "blur border color"), I18n.tr("Primary", "blur border color"), I18n.tr("Secondary", "blur border color"), I18n.tr("Text Color", "blur border color"), I18n.tr("Custom", "blur border color")]
|
||||
currentValue: {
|
||||
switch (SettingsData.blurBorderColor) {
|
||||
case "primary":
|
||||
return I18n.tr("Primary", "blur border color");
|
||||
case "secondary":
|
||||
return I18n.tr("Secondary", "blur border color");
|
||||
case "surfaceText":
|
||||
return I18n.tr("Text Color", "blur border color");
|
||||
case "custom":
|
||||
return I18n.tr("Custom", "blur border color");
|
||||
default:
|
||||
return I18n.tr("Outline", "blur border color");
|
||||
}
|
||||
}
|
||||
onValueChanged: value => {
|
||||
if (value === I18n.tr("Primary", "blur border color")) {
|
||||
SettingsData.set("blurBorderColor", "primary");
|
||||
} else if (value === I18n.tr("Secondary", "blur border color")) {
|
||||
SettingsData.set("blurBorderColor", "secondary");
|
||||
} else if (value === I18n.tr("Text Color", "blur border color")) {
|
||||
SettingsData.set("blurBorderColor", "surfaceText");
|
||||
} else if (value === I18n.tr("Custom", "blur border color")) {
|
||||
SettingsData.set("blurBorderColor", "custom");
|
||||
openBlurBorderColorPicker();
|
||||
} else {
|
||||
SettingsData.set("blurBorderColor", "outline");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
tab: "theme"
|
||||
tags: ["blur", "border", "opacity"]
|
||||
settingKey: "blurBorderOpacity"
|
||||
text: I18n.tr("Blur Border Opacity")
|
||||
description: I18n.tr("Controls the outer edge of protocol-blurred windows")
|
||||
visible: SettingsData.blurEnabled
|
||||
value: Math.round((SettingsData.blurBorderOpacity ?? 0.35) * 100)
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
unit: "%"
|
||||
defaultValue: 35
|
||||
onSliderValueChanged: newValue => SettingsData.set("blurBorderOpacity", newValue / 100)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
tab: "theme"
|
||||
tags: ["elevation", "shadow", "lift", "m3", "material"]
|
||||
title: I18n.tr("Shadows")
|
||||
settingKey: "m3ElevationEnabled"
|
||||
iconName: "layers"
|
||||
|
||||
SettingsToggleRow {
|
||||
tab: "theme"
|
||||
@@ -1703,7 +1809,7 @@ Item {
|
||||
tags: ["elevation", "shadow", "opacity", "transparency", "m3"]
|
||||
settingKey: "m3ElevationOpacity"
|
||||
text: I18n.tr("Shadow Opacity")
|
||||
description: I18n.tr("Controls the transparency of the shadow")
|
||||
description: I18n.tr("Controls the opacity of the shadow")
|
||||
value: SettingsData.m3ElevationOpacity ?? 30
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
@@ -1857,105 +1963,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
tab: "theme"
|
||||
tags: ["blur", "background", "transparency", "glass", "frosted"]
|
||||
title: I18n.tr("Background Blur")
|
||||
settingKey: "blurEnabled"
|
||||
iconName: "blur_on"
|
||||
|
||||
SettingsToggleRow {
|
||||
tab: "theme"
|
||||
tags: ["blur", "background", "transparency", "glass", "frosted"]
|
||||
settingKey: "blurEnabled"
|
||||
text: I18n.tr("Background Blur")
|
||||
description: !BlurService.available ? I18n.tr("Your compositor does not support background blur (ext-background-effect-v1)") : I18n.tr("Blur the background behind bars, popouts, modals, and notifications. Requires compositor support and configuration.")
|
||||
checked: SettingsData.blurEnabled ?? false
|
||||
enabled: BlurService.available
|
||||
onToggled: checked => SettingsData.set("blurEnabled", checked)
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
tab: "theme"
|
||||
tags: ["blur", "foreground", "layers", "contrast", "glass", "frosted"]
|
||||
settingKey: "blurForegroundLayers"
|
||||
text: I18n.tr("Foreground Layers")
|
||||
description: I18n.tr("Show foreground surfaces on blurred panels for stronger contrast")
|
||||
checked: SettingsData.blurForegroundLayers ?? true
|
||||
visible: BlurService.available && (SettingsData.blurEnabled ?? false)
|
||||
enabled: BlurService.available
|
||||
onToggled: checked => SettingsData.set("blurForegroundLayers", checked)
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
tab: "theme"
|
||||
tags: ["blur", "foreground", "layers", "outline", "border", "cards", "widgets", "notifications", "control center"]
|
||||
settingKey: "blurLayerOutlineOpacity"
|
||||
text: I18n.tr("Layer Outline Opacity")
|
||||
description: I18n.tr("Controls outlines around blurred foreground cards, pills, and notification cards")
|
||||
visible: BlurService.available && (SettingsData.blurEnabled ?? false)
|
||||
value: Math.round((SettingsData.blurLayerOutlineOpacity ?? 0.12) * 100)
|
||||
minimum: 0
|
||||
maximum: 40
|
||||
unit: "%"
|
||||
defaultValue: 12
|
||||
onSliderValueChanged: newValue => SettingsData.set("blurLayerOutlineOpacity", newValue / 100)
|
||||
}
|
||||
|
||||
SettingsDropdownRow {
|
||||
tab: "theme"
|
||||
tags: ["blur", "border", "outline", "edge"]
|
||||
settingKey: "blurBorderColor"
|
||||
text: I18n.tr("Blur Border Color")
|
||||
description: I18n.tr("Border color around blurred surfaces")
|
||||
visible: SettingsData.blurEnabled
|
||||
options: [I18n.tr("Outline", "blur border color"), I18n.tr("Primary", "blur border color"), I18n.tr("Secondary", "blur border color"), I18n.tr("Text Color", "blur border color"), I18n.tr("Custom", "blur border color")]
|
||||
currentValue: {
|
||||
switch (SettingsData.blurBorderColor) {
|
||||
case "primary":
|
||||
return I18n.tr("Primary", "blur border color");
|
||||
case "secondary":
|
||||
return I18n.tr("Secondary", "blur border color");
|
||||
case "surfaceText":
|
||||
return I18n.tr("Text Color", "blur border color");
|
||||
case "custom":
|
||||
return I18n.tr("Custom", "blur border color");
|
||||
default:
|
||||
return I18n.tr("Outline", "blur border color");
|
||||
}
|
||||
}
|
||||
onValueChanged: value => {
|
||||
if (value === I18n.tr("Primary", "blur border color")) {
|
||||
SettingsData.set("blurBorderColor", "primary");
|
||||
} else if (value === I18n.tr("Secondary", "blur border color")) {
|
||||
SettingsData.set("blurBorderColor", "secondary");
|
||||
} else if (value === I18n.tr("Text Color", "blur border color")) {
|
||||
SettingsData.set("blurBorderColor", "surfaceText");
|
||||
} else if (value === I18n.tr("Custom", "blur border color")) {
|
||||
SettingsData.set("blurBorderColor", "custom");
|
||||
openBlurBorderColorPicker();
|
||||
} else {
|
||||
SettingsData.set("blurBorderColor", "outline");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
tab: "theme"
|
||||
tags: ["blur", "border", "opacity"]
|
||||
settingKey: "blurBorderOpacity"
|
||||
text: I18n.tr("Blur Border Opacity")
|
||||
description: I18n.tr("Controls the outer edge of protocol-blurred windows")
|
||||
visible: SettingsData.blurEnabled
|
||||
value: Math.round((SettingsData.blurBorderOpacity ?? 0.35) * 100)
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
unit: "%"
|
||||
defaultValue: 35
|
||||
onSliderValueChanged: newValue => SettingsData.set("blurBorderOpacity", newValue / 100)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
tab: "theme"
|
||||
tags: ["modal", "darken", "background", "overlay"]
|
||||
@@ -2016,7 +2023,7 @@ Item {
|
||||
title: I18n.tr("Cursor Theme")
|
||||
settingKey: "cursorTheme"
|
||||
iconName: "mouse"
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isMango
|
||||
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isMango
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
@@ -2181,8 +2188,6 @@ Item {
|
||||
return SettingsData.cursorSettings.niri?.hideAfterInactiveMs || 0;
|
||||
if (CompositorService.isHyprland)
|
||||
return SettingsData.cursorSettings.hyprland?.inactiveTimeout || 0;
|
||||
if (CompositorService.isDwl)
|
||||
return SettingsData.cursorSettings.dwl?.cursorHideTimeout || 0;
|
||||
if (CompositorService.isMango)
|
||||
return SettingsData.cursorSettings.mango?.cursorHideTimeout || 0;
|
||||
return 0;
|
||||
@@ -2201,10 +2206,6 @@ Item {
|
||||
if (!updated.hyprland)
|
||||
updated.hyprland = {};
|
||||
updated.hyprland.inactiveTimeout = newValue;
|
||||
} else if (CompositorService.isDwl) {
|
||||
if (!updated.dwl)
|
||||
updated.dwl = {};
|
||||
updated.dwl.cursorHideTimeout = newValue;
|
||||
} else if (CompositorService.isMango) {
|
||||
if (!updated.mango)
|
||||
updated.mango = {};
|
||||
@@ -2688,7 +2689,7 @@ Item {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "folder"
|
||||
name: "settings"
|
||||
size: 16
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
@@ -139,7 +139,7 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: UsersService.refreshing ? I18n.tr("Refreshing…") : ""
|
||||
text: UsersService.refreshing ? I18n.tr("Refreshing...") : ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -474,7 +474,7 @@ Item {
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankButton {
|
||||
text: root.operationPending ? I18n.tr("Working…") : I18n.tr("Create User")
|
||||
text: root.operationPending ? I18n.tr("Working...") : I18n.tr("Create User")
|
||||
iconName: "person_add"
|
||||
backgroundColor: Theme.primary
|
||||
textColor: Theme.primaryText
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user