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

Compare commits

...

12 Commits

Author SHA1 Message Date
bbedward cc307aff98 restore overlay option and tweak inset padding behavior 2026-07-22 14:48:58 -04:00
bbedward cee7957e15 fix hover popouts in frame mode 2026-07-22 13:21:43 -04:00
bbedward 602697ef51 frame: host bar and dock inside the frame surface in connected mode
Previously the bar-in-frame was rendered on the overlay layer always.
Now they are rendered inside of the frames top-layer surface, which lets
the compositor handle layer-shell layering natively.
2026-07-22 13:21:43 -04:00
bbedward ce548cb29c update CONTRIBUTING for submodule recurse option recommendation 2026-07-22 12:32:59 -04:00
bbedward 61510c2186 theme: support for QT_PLATFORMTHEME=kde 2026-07-22 12:20:27 -04:00
Thomas Kroll 237fb57d5e fix(lock): hold sleep inhibitor until compositor confirms lock (#2907)
lock-before-suspend released the logind sleep delay inhibitor as soon as Qt
reported the lock rendered (afterAnimating/afterRendering). That signal fires
before the compositor has committed/presented the ext-session-lock surface, so
the machine could freeze ~1 frame later with the desktop still the last
presented frame. On resume the desktop was briefly visible before the lock
appeared (both suspend and hibernate).

Gate lockerReady on WlSessionLock.secure (the ext-session-lock `locked` event),
so the inhibitor is held until the compositor confirms the session is locked and
the desktop is hidden. LockSurface passes the WlSessionLock down as sessionLock;
the readiness check returns early until secure, and re-fires on secureChanged.
The Go-side fallback timer in loginctl monitor remains as the backstop.

Reproduced on niri / s2idle with 3 outputs: debug log showed lockerReady sent
18 ms before `PM: suspend entry`; with this change the desktop no longer flashes
on resume.

Signed-off-by: Thomas Kroll <99196436+tkroll-ionos@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 09:24:25 -04:00
bbedward fe561f2b5d location: prevent calls when weather auto-locate is disabled
port 1.5
2026-07-21 23:35:33 -04:00
purian23 1c01254e9b fix(void): update working templates on dms-git
Port 1.5
2026-07-21 19:28:26 -04:00
bbedward 958672badd lock/pam: attempt to fix fprintd retry spam after resume 2026-07-21 17:53:58 -04:00
bbedward 93c3030a1e popout: fix lifecycle of blur with various popouts
fixes #2901
port 1.5
2026-07-21 17:46:43 -04:00
dms-ci[bot] 53c729f685 nix: update vendorHash for go.mod changes 2026-07-21 16:51:00 +00:00
bbedward e0480bf383 core: bump dankgo 2026-07-21 12:48:32 -04:00
37 changed files with 2774 additions and 2149 deletions
+6
View File
@@ -14,6 +14,12 @@ git clone --recurse-submodules https://github.com/AvengeMedia/DankMaterialShell.
git submodule update --init git submodule update --init
``` ```
To have `git pull` keep the submodule in sync automatically (moving it to the commit this repo points at, no separate `git submodule update` step), set:
```bash
git config submodule.recurse true
```
Install [prek](https://prek.j178.dev/) then activate pre-commit hooks: Install [prek](https://prek.j178.dev/) then activate pre-commit hooks:
```bash ```bash
+1 -1
View File
@@ -69,7 +69,7 @@ require (
) )
require ( require (
github.com/AvengeMedia/dankgo v0.0.0-20260718184203-ed59ffc6e599 github.com/AvengeMedia/dankgo v0.0.0-20260721162324-38d48943054d
github.com/atotto/clipboard v0.1.4 // indirect github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect github.com/charmbracelet/colorprofile v0.4.3 // indirect
+2 -2
View File
@@ -1,7 +1,7 @@
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/AvengeMedia/dankgo v0.0.0-20260718184203-ed59ffc6e599 h1:hxy5SRGREsQ6v4xUOxWnWVPWD6ZU+H0jA+LHcTwXbCg= github.com/AvengeMedia/dankgo v0.0.0-20260721162324-38d48943054d h1:EZN2x2uAX975Uvw4MByqWaaVTp2fOJrNTl4V4EQbSn8=
github.com/AvengeMedia/dankgo v0.0.0-20260718184203-ed59ffc6e599/go.mod h1:7p7cfydr4WM1G6eOPFlANXF3IV5du3FoA4CbDPprHAo= github.com/AvengeMedia/dankgo v0.0.0-20260721162324-38d48943054d/go.mod h1:7p7cfydr4WM1G6eOPFlANXF3IV5du3FoA4CbDPprHAo=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
+45
View File
@@ -390,6 +390,10 @@ func buildOnce(opts *Options) (bool, error) {
refreshGTK4() refreshGTK4()
} }
if isDMSKDEColorSchemeActive(opts.ConfigDir) {
applyKDEColorScheme(opts.Mode)
}
if !opts.ShouldSkipTemplate("qt6ct") && appExists(opts.AppChecker, []string{"qt6ct"}, nil) { if !opts.ShouldSkipTemplate("qt6ct") && appExists(opts.AppChecker, []string{"qt6ct"}, nil) {
refreshQt6ct() refreshQt6ct()
} }
@@ -923,6 +927,47 @@ func isDMSGTKActive(configDir string) bool {
return err == nil && strings.Contains(string(data), "dank-colors.css") return err == nil && strings.Contains(string(data), "dank-colors.css")
} }
// isDMSKDEColorSchemeActive only flips the scheme when the user is already on a
// DankMatugen one, leaving Breeze (or anything else) untouched.
func isDMSKDEColorSchemeActive(configDir string) bool {
data, err := os.ReadFile(filepath.Join(configDir, "kdeglobals"))
if err != nil {
return false
}
inGeneral := false
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "[") {
inGeneral = line == "[General]"
continue
}
if !inGeneral {
continue
}
if name, ok := strings.CutPrefix(line, "ColorScheme="); ok {
return strings.HasPrefix(strings.TrimSpace(name), "DankMatugen")
}
}
return false
}
func applyKDEColorScheme(mode ColorMode) {
if !utils.CommandExists("plasma-apply-colorscheme") {
return
}
scheme := "DankMatugenDark"
if mode == ColorModeLight {
scheme = "DankMatugenLight"
}
log.Infof("Applying KDE color scheme: %s", scheme)
if err := exec.Command("plasma-apply-colorscheme", scheme).Run(); err != nil {
log.Warnf("Failed to apply KDE color scheme: %v", err)
}
}
func refreshGTK(mode ColorMode) { func refreshGTK(mode ColorMode) {
if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", ""); err != nil { if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", ""); err != nil {
log.Warnf("Failed to reset gtk-theme: %v", err) log.Warnf("Failed to reset gtk-theme: %v", err)
+1 -1
View File
@@ -16,7 +16,7 @@ revision=1
create_wrksrc=yes create_wrksrc=yes
build_style=go build_style=go
build_wrksrc="core" build_wrksrc="core"
hostmakedepends="make" hostmakedepends="make tar"
go_import_path="github.com/AvengeMedia/DankMaterialShell/core" go_import_path="github.com/AvengeMedia/DankMaterialShell/core"
go_package="${go_import_path}/cmd/dms" go_package="${go_import_path}/cmd/dms"
go_build_tags="distro_binary withshell" go_build_tags="distro_binary withshell"
+1 -1
View File
@@ -111,7 +111,7 @@
inherit version; inherit version;
pname = "dms-shell"; pname = "dms-shell";
src = ./core; src = ./core;
vendorHash = "sha256-//ya3Yb25/kEBeT6M4bO8Egi+Tb7iiUwevXlzTyIgqA="; vendorHash = "sha256-G0KCUaNwjWfLcSnb68TMXjpB2xxwheXgrBkFEzcSZTc=";
subPackages = [ "cmd/dms" ]; subPackages = [ "cmd/dms" ];
+21 -2
View File
@@ -65,16 +65,35 @@ Singleton {
function _openPopout(popout) { function _openPopout(popout) {
if (popout.dashVisible !== undefined) { if (popout.dashVisible !== undefined) {
if (popout.dashVisible && !popout.shouldBeVisible && !popout.isClosing) let flagStayedTrue = popout.dashVisible === true;
if (popout.dashVisible && !popout.shouldBeVisible && !popout.isClosing) {
popout.dashVisible = false; popout.dashVisible = false;
flagStayedTrue = false;
}
popout.dashVisible = true; popout.dashVisible = true;
// Flag already true (e.g. retargeting to another monitor) won't re-fire
// the change handler, so drive the surface open explicitly.
if (flagStayedTrue)
_ensureSurfaceOpen(popout);
return; return;
} }
if (popout.notificationHistoryVisible !== undefined) { if (popout.notificationHistoryVisible !== undefined) {
const flagStayedTrue = popout.notificationHistoryVisible === true;
popout.notificationHistoryVisible = true; popout.notificationHistoryVisible = true;
if (flagStayedTrue)
_ensureSurfaceOpen(popout);
return; return;
} }
popout.open(); _ensureSurfaceOpen(popout);
}
function _ensureSurfaceOpen(popout) {
if (typeof popout.present === "function") {
popout.present();
return;
}
if (typeof popout.open === "function")
popout.open();
} }
function _closePopout(popout) { function _closePopout(popout) {
+29
View File
@@ -2735,6 +2735,35 @@ Singleton {
return edges; return edges;
} }
function getOverlayBarEdgesForScreen(screen) {
if (!screen)
return [];
var edges = [];
for (var i = 0; i < barConfigs.length; i++) {
var bc = barConfigs[i];
if (!bc.enabled || !(bc.useOverlayLayer ?? false))
continue;
var prefs = bc.screenPreferences || ["all"];
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs))
continue;
switch (bc.position ?? 0) {
case SettingsData.Position.Top:
edges.push("top");
break;
case SettingsData.Position.Bottom:
edges.push("bottom");
break;
case SettingsData.Position.Left:
edges.push("left");
break;
case SettingsData.Position.Right:
edges.push("right");
break;
}
}
return edges;
}
function frameEdgeInsetForSide(screen, side) { function frameEdgeInsetForSide(screen, side) {
if (!frameEnabled || !screen) if (!frameEnabled || !screen)
return 0; return 0;
+11
View File
@@ -314,6 +314,17 @@ Item {
} }
} }
Binding {
target: BarWidgetService
property: "dockContextMenu"
value: dockContextMenuLoader.item
}
Binding {
target: BarWidgetService
property: "dockTrashContextMenu"
value: dockTrashContextMenuLoader.item
}
Connections { Connections {
target: SettingsData target: SettingsData
function onDockShowTrashChanged() { function onDockShowTrashChanged() {
@@ -77,6 +77,12 @@ DankPopout {
readonly property color _containerBg: Theme.nestedSurface readonly property color _containerBg: Theme.nestedSurface
// Defer open one tick so screen-change geometry settles before the surface
// maps; a synchronous open churns the surface and loses the blur on a switch.
function present() {
Qt.callLater(open);
}
function openWithSection(section) { function openWithSection(section) {
StateUtils.openWithSection(root, section); StateUtils.openWithSection(root, section);
} }
+14 -7
View File
@@ -13,6 +13,9 @@ Item {
signal colorPickerRequested signal colorPickerRequested
signal barReady(var barConfig) signal barReady(var barConfig)
Component.onCompleted: BarWidgetService.registerDankBarItem(root.barConfig?.id, root)
Component.onDestruction: BarWidgetService.unregisterDankBarItem(root.barConfig?.id, root)
property alias barVariants: barVariants property alias barVariants: barVariants
property var hyprlandOverviewLoader: null property var hyprlandOverviewLoader: null
property bool systemTrayMenuOpen: false property bool systemTrayMenuOpen: false
@@ -161,14 +164,18 @@ Item {
id: barVariants id: barVariants
model: { model: {
const prefs = root.barConfig?.screenPreferences || ["all"]; const prefs = root.barConfig?.screenPreferences || ["all"];
if (prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all")) { const wantsAll = prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all");
return Quickshell.screens; let base;
if (wantsAll) {
base = Quickshell.screens;
} else {
base = Quickshell.screens.filter(screen => SettingsData.isScreenInPreferences(screen, prefs));
if (base.length === 0 && root.barConfig?.showOnLastDisplay && Quickshell.screens.length === 1)
base = Quickshell.screens;
} }
const filtered = Quickshell.screens.filter(screen => SettingsData.isScreenInPreferences(screen, prefs)); // Connected frame mode renders the bar inside the frame surface; skip the standalone window there
if (filtered.length === 0 && root.barConfig?.showOnLastDisplay && Quickshell.screens.length === 1) { // unless this bar wants the overlay layer, which the frame surface cannot provide.
return Quickshell.screens; return base.filter(screen => !CompositorService.frameHostsBarForConfig(screen, root.barConfig));
}
return filtered;
} }
delegate: DankBarWindow { delegate: DankBarWindow {
File diff suppressed because it is too large Load Diff
+17 -6
View File
@@ -38,37 +38,44 @@ Item {
readonly property real _barInsetPaddingRaw: SettingsData.barInsetPaddingSyncAll ? SettingsData.barInsetPaddingShared : (barConfig?.barInsetPadding ?? -1) readonly property real _barInsetPaddingRaw: SettingsData.barInsetPaddingSyncAll ? SettingsData.barInsetPaddingShared : (barConfig?.barInsetPadding ?? -1)
readonly property real _barInsetPaddingAuto: _barIsVertical ? Theme.spacingXS : _edgeBaseMargin readonly property real _barInsetPaddingAuto: _barIsVertical ? Theme.spacingXS : _edgeBaseMargin
readonly property real _barInsetPadding: _barInsetPaddingRaw < 0 ? _barInsetPaddingAuto : _barInsetPaddingRaw readonly property real _barInsetPadding: _barInsetPaddingRaw < 0 ? _barInsetPaddingAuto : _barInsetPaddingRaw
// Connected-frame Bar Inset Padding: absolute free-end inset (auto < 0 = frameThickness, 0 = edge-to-edge). // Connected-frame Bar Inset Padding: absolute free-end gap the hosted bar spans full-width into
// FrameExclusions already moves a free bar end inward by frameThickness so use frame inset to mangage the gap // (auto < 0 = frameThickness so widgets align with the interior cutout, 0 = edge-to-edge). The extra
// beyond frameThickness is what an adjacent bar end adds on top of its corner alignment.
readonly property real _frameInsetResolved: SettingsData.frameBarInsetPadding < 0 ? SettingsData.frameThickness : SettingsData.frameBarInsetPadding readonly property real _frameInsetResolved: SettingsData.frameBarInsetPadding < 0 ? SettingsData.frameThickness : SettingsData.frameBarInsetPadding
readonly property real _frameInsetExtra: Math.max(0, _frameInsetResolved - SettingsData.frameThickness) readonly property real _frameInsetExtra: Math.max(0, _frameInsetResolved - SettingsData.frameThickness)
// Horizontal bars span the full width and own the corners; the perpendicular vertical bar
// tucks in below/above. Where they meet, inset the corner widget so it centres in the
// frameBarSize corner cell, aligning it with the vertical bar's widget column.
readonly property real _widgetThicknessValue: _hasBarWindow ? barWindow.widgetThickness : 30
readonly property real _cornerAlignInset: Math.max(_frameInsetExtra, (SettingsData.frameBarSize - _widgetThicknessValue) / 2)
readonly property real _leftMargin: { readonly property real _leftMargin: {
if (_barIsVertical) if (_barIsVertical)
return _edgeBaseMargin; return _edgeBaseMargin;
if (_usesFrameBarChrome) if (_usesFrameBarChrome)
return hasAdjacentLeftBarLive ? (_edgeBaseMargin + SettingsData.frameBarSize + _frameInsetExtra) : _frameInsetExtra; return hasAdjacentLeftBarLive ? _cornerAlignInset : _frameInsetResolved;
return Math.max(0, _barInsetPadding); return Math.max(0, _barInsetPadding);
} }
readonly property real _rightMargin: { readonly property real _rightMargin: {
if (_barIsVertical) if (_barIsVertical)
return _edgeBaseMargin; return _edgeBaseMargin;
if (_usesFrameBarChrome) if (_usesFrameBarChrome)
return hasAdjacentRightBarLive ? (_edgeBaseMargin + SettingsData.frameBarSize + _frameInsetExtra) : _frameInsetExtra; return hasAdjacentRightBarLive ? _cornerAlignInset : _frameInsetResolved;
return Math.max(0, _barInsetPadding); return Math.max(0, _barInsetPadding);
} }
readonly property real _topMargin: { readonly property real _topMargin: {
if (!_barIsVertical) if (!_barIsVertical)
return 0; return 0;
if (_usesFrameBarChrome) if (_usesFrameBarChrome)
return hasAdjacentTopBarLive ? (outlineThickness + SettingsData.frameThickness + _frameInsetExtra) : _frameInsetExtra; return hasAdjacentTopBarLive ? (_edgeBaseMargin + SettingsData.frameBarSize + _frameInsetExtra) : _frameInsetResolved;
return Math.max(0, _barInsetPadding); return Math.max(0, _barInsetPadding);
} }
readonly property real _bottomMargin: { readonly property real _bottomMargin: {
if (!_barIsVertical) if (!_barIsVertical)
return 0; return 0;
if (_usesFrameBarChrome) if (_usesFrameBarChrome)
return hasAdjacentBottomBarLive ? (outlineThickness + SettingsData.frameThickness + _frameInsetExtra) : _frameInsetExtra; return hasAdjacentBottomBarLive ? (_edgeBaseMargin + SettingsData.frameBarSize + _frameInsetExtra) : _frameInsetResolved;
return Math.max(0, _barInsetPadding); return Math.max(0, _barInsetPadding);
} }
@@ -677,6 +684,7 @@ Item {
LeftSection { LeftSection {
id: hLeftSection id: hLeftSection
objectName: "leftSection" objectName: "leftSection"
edgeIsScreenEdge: !topBarContent.hasAdjacentLeftBarLive
overrideAxisLayout: true overrideAxisLayout: true
forceVerticalLayout: false forceVerticalLayout: false
anchors { anchors {
@@ -710,6 +718,7 @@ Item {
RightSection { RightSection {
id: hRightSection id: hRightSection
objectName: "rightSection" objectName: "rightSection"
edgeIsScreenEdge: !topBarContent.hasAdjacentRightBarLive
overrideAxisLayout: true overrideAxisLayout: true
forceVerticalLayout: false forceVerticalLayout: false
anchors { anchors {
@@ -782,6 +791,7 @@ Item {
LeftSection { LeftSection {
id: vLeftSection id: vLeftSection
objectName: "leftSection" objectName: "leftSection"
edgeIsScreenEdge: !topBarContent.hasAdjacentTopBarLive
overrideAxisLayout: true overrideAxisLayout: true
forceVerticalLayout: true forceVerticalLayout: true
width: parent.width width: parent.width
@@ -850,6 +860,7 @@ Item {
RightSection { RightSection {
id: vRightSection id: vRightSection
objectName: "rightSection" objectName: "rightSection"
edgeIsScreenEdge: !topBarContent.hasAdjacentBottomBarLive
overrideAxisLayout: true overrideAxisLayout: true
forceVerticalLayout: true forceVerticalLayout: true
width: parent.width width: parent.width
@@ -930,7 +930,8 @@ Item {
if (_barHovered) if (_barHovered)
return; return;
const excludedBar = _barExitPending ? barWindow : null; const excludedBar = _barExitPending ? barWindow : null;
if (cursorOverHoverChain(gx, gy, excludedBar)) const staleOverExitedBar = excludedBar?.containsGlobalPoint?.(gx, gy, 0) ?? false;
if (!staleOverExitedBar && cursorOverHoverChain(gx, gy, excludedBar))
return; return;
_barExitPending = false; _barExitPending = false;
closeHoverSurfaces(); closeHoverSurfaces();
File diff suppressed because it is too large Load Diff
+5 -2
View File
@@ -17,6 +17,9 @@ Item {
property real sectionAvailablePrimarySize: 0 property real sectionAvailablePrimarySize: 0
property bool overrideAxisLayout: false property bool overrideAxisLayout: false
property bool forceVerticalLayout: false property bool forceVerticalLayout: false
// False when a perpendicular bar occupies this edge, so the first widget's click area
// doesn't extend past the bar into the neighbour (matters most in the frame surface).
property bool edgeIsScreenEdge: true
readonly property bool isVertical: overrideAxisLayout ? forceVerticalLayout : (axis?.isVertical ?? false) readonly property bool isVertical: overrideAxisLayout ? forceVerticalLayout : (axis?.isVertical ?? false)
property alias widgetLayoutLoader: layoutLoader property alias widgetLayoutLoader: layoutLoader
@@ -68,7 +71,7 @@ Item {
isFirst: index === 0 isFirst: index === 0
isLast: index === rowRepeater.count - 1 isLast: index === rowRepeater.count - 1
sectionSpacing: parent.rowSpacing sectionSpacing: parent.rowSpacing
isLeftBarEdge: true isLeftBarEdge: root.edgeIsScreenEdge
isRightBarEdge: false isRightBarEdge: false
} }
} }
@@ -115,7 +118,7 @@ Item {
isFirst: index === 0 isFirst: index === 0
isLast: index === columnRepeater.count - 1 isLast: index === columnRepeater.count - 1
sectionSpacing: parent.columnSpacing sectionSpacing: parent.columnSpacing
isTopBarEdge: true isTopBarEdge: root.edgeIsScreenEdge
isBottomBarEdge: false isBottomBarEdge: false
} }
} }
+5 -2
View File
@@ -17,6 +17,9 @@ Item {
property real sectionAvailablePrimarySize: 0 property real sectionAvailablePrimarySize: 0
property bool overrideAxisLayout: false property bool overrideAxisLayout: false
property bool forceVerticalLayout: false property bool forceVerticalLayout: false
// False when a perpendicular bar occupies this edge, so the last widget's click area
// doesn't extend past the bar into the neighbour (matters most in the frame surface).
property bool edgeIsScreenEdge: true
readonly property bool isVertical: overrideAxisLayout ? forceVerticalLayout : (axis?.isVertical ?? false) readonly property bool isVertical: overrideAxisLayout ? forceVerticalLayout : (axis?.isVertical ?? false)
property alias widgetLayoutLoader: layoutLoader property alias widgetLayoutLoader: layoutLoader
@@ -71,7 +74,7 @@ Item {
isLast: index === rowRepeater.count - 1 isLast: index === rowRepeater.count - 1
sectionSpacing: parent.rowSpacing sectionSpacing: parent.rowSpacing
isLeftBarEdge: false isLeftBarEdge: false
isRightBarEdge: true isRightBarEdge: root.edgeIsScreenEdge
} }
} }
} }
@@ -118,7 +121,7 @@ Item {
isLast: index === columnRepeater.count - 1 isLast: index === columnRepeater.count - 1
sectionSpacing: parent.columnSpacing sectionSpacing: parent.columnSpacing
isTopBarEdge: false isTopBarEdge: false
isBottomBarEdge: true isBottomBarEdge: root.edgeIsScreenEdge
} }
} }
} }
@@ -63,8 +63,8 @@ BasePill {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onPressed: mouse => root.triggerRipple(this, mouse.x, mouse.y) onPressed: function (mouse) {
onClicked: function (mouse) { root.triggerRipple(this, mouse.x, mouse.y);
switch (mouse.button) { switch (mouse.button) {
case Qt.RightButton: case Qt.RightButton:
openContextMenu(); openContextMenu();
+43 -834
View File
@@ -1,8 +1,6 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Shapes
import Quickshell import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Services import qs.Services
@@ -10,7 +8,8 @@ import qs.Widgets
Variants { Variants {
id: dockVariants id: dockVariants
model: SettingsData.getFilteredScreens("dock") // Connected frame mode renders the dock inside the frame surface; skip the standalone window there.
model: SettingsData.getFilteredScreens("dock").filter(screen => !CompositorService.frameHostsDockForScreen(screen))
property var contextMenu property var contextMenu
property var trashContextMenu property var trashContextMenu
@@ -18,864 +17,74 @@ Variants {
delegate: PanelWindow { delegate: PanelWindow {
id: dock id: dock
WindowBlur {
targetWindow: dock
blurEnabled: dock.effectiveBlurEnabled && !dock.usesConnectedFrameChrome
blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x
blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y
blurWidth: dock.hasApps && dock.reveal ? dockBackground.width : 0
blurHeight: dock.hasApps && dock.reveal ? dockBackground.height : 0
blurRadius: dock.usesConnectedFrameChrome ? Theme.connectedCornerRadius : dock.surfaceRadius
}
WlrLayershell.namespace: "dms:dock"
WlrLayershell.layer: dock.usesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top
readonly property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
anchors {
top: !isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top) : true
bottom: !isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom) : true
left: !isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Left)
right: !isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Right)
}
property var modelData: item property var modelData: item
property bool autoHide: SettingsData.dockAutoHide || SettingsData.dockSmartAutoHide
property real backgroundTransparency: SettingsData.dockTransparency
property bool groupByApp: SettingsData.dockGroupByApp
readonly property int borderThickness: SettingsData.dockBorderEnabled ? SettingsData.dockBorderThickness : 0
readonly property string connectedBarSide: SettingsData.dockPosition === SettingsData.Position.Top ? "top" : SettingsData.dockPosition === SettingsData.Position.Bottom ? "bottom" : SettingsData.dockPosition === SettingsData.Position.Left ? "left" : "right"
readonly property bool frameDockExclusionActive: dockGeometry.frameExclusionActive
readonly property bool connectedBarActiveOnEdge: dockGeometry.connectedBarActiveOnEdge
readonly property real connectedJoinInset: dockGeometry.connectedJoinInset
readonly property real dockFrameInset: dockGeometry.frameInset
readonly property real surfaceRadius: usesConnectedFrameChrome ? Theme.connectedSurfaceRadius : Theme.cornerRadius
readonly property color surfaceColor: usesConnectedFrameChrome ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency)
readonly property color surfaceBorderColor: usesConnectedFrameChrome ? Theme.withAlpha(BlurService.borderColor, 0) : BlurService.borderColor
readonly property real surfaceBorderWidth: usesConnectedFrameChrome ? 0 : BlurService.borderWidth
readonly property real surfaceTopLeftRadius: usesConnectedFrameChrome && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius
readonly property real surfaceTopRightRadius: usesConnectedFrameChrome && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius
readonly property real surfaceBottomLeftRadius: usesConnectedFrameChrome && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius
readonly property real surfaceBottomRightRadius: usesConnectedFrameChrome && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius
readonly property real horizontalConnectorExtent: usesConnectedFrameChrome && !isVertical ? Theme.connectedCornerRadius : 0
readonly property real verticalConnectorExtent: usesConnectedFrameChrome && isVertical ? Theme.connectedCornerRadius : 0
readonly property int hasApps: dockApps.implicitWidth > 0 || dockApps.implicitHeight > 0
readonly property real widgetHeight: SettingsData.dockIconSize
readonly property real effectiveBarHeight: dockGeometry.visualThickness
function getBarHeight(barConfig) {
if (!barConfig)
return 0;
const innerPadding = barConfig.innerPadding ?? 4;
const widgetThickness = Math.max(20, 26 + innerPadding * 0.6);
const barThickness = Math.max(widgetThickness + innerPadding + 4, Theme.barHeight - 4 - (8 - innerPadding));
const spacing = barConfig.spacing ?? 4;
const bottomGap = barConfig.bottomGap ?? 0;
return barThickness + spacing + bottomGap;
}
readonly property real barSpacing: {
const defaultBar = SettingsData.barConfigs[0] || SettingsData.getBarConfig("default");
if (!defaultBar)
return 0;
const barPos = defaultBar.position ?? SettingsData.Position.Top;
const barIsHorizontal = (barPos === SettingsData.Position.Top || barPos === SettingsData.Position.Bottom);
const barIsVertical = (barPos === SettingsData.Position.Left || barPos === SettingsData.Position.Right);
const samePosition = (SettingsData.dockPosition === barPos);
const dockIsHorizontal = !isVertical;
const dockIsVertical = isVertical;
if (!(defaultBar.visible ?? true))
return 0;
const spacing = defaultBar.spacing ?? 4;
const bottomGap = defaultBar.bottomGap ?? 0;
if (dockIsHorizontal && barIsHorizontal && samePosition) {
return spacing + effectiveBarHeight + bottomGap;
}
if (dockIsVertical && barIsVertical && samePosition) {
return spacing + effectiveBarHeight + bottomGap;
}
return 0;
}
readonly property real adjacentTopBarHeight: {
if (!isVertical || autoHide)
return 0;
const screenName = dock.modelData?.name ?? "";
const topBar = SettingsData.barConfigs.find(bc => {
if (!bc.enabled || bc.autoHide || !(bc.visible ?? true))
return false;
if (bc.position !== SettingsData.Position.Top && bc.position !== 0)
return false;
const onThisScreen = bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all") || bc.screenPreferences.includes(screenName);
return onThisScreen;
});
return getBarHeight(topBar);
}
readonly property real adjacentLeftBarWidth: {
if (isVertical || autoHide)
return 0;
const screenName = dock.modelData?.name ?? "";
const leftBar = SettingsData.barConfigs.find(bc => {
if (!bc.enabled || bc.autoHide || !(bc.visible ?? true))
return false;
if (bc.position !== SettingsData.Position.Left && bc.position !== 2)
return false;
const onThisScreen = bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all") || bc.screenPreferences.includes(screenName);
return onThisScreen;
});
return getBarHeight(leftBar);
}
readonly property real dockMargin: SettingsData.dockMargin
readonly property bool effectiveBlurEnabled: Theme.connectedSurfaceBlurEnabled
readonly property real effectiveDockBottomGap: dockGeometry.visualOffset
readonly property real effectiveDockMargin: dockGeometry.effectiveMargin
readonly property real positionSpacing: barSpacing + effectiveDockBottomGap + effectiveDockMargin
readonly property real joinedEdgeMargin: dockGeometry.joinedEdgeMargin
readonly property real _dpr: (dock.screen && dock.screen.devicePixelRatio) ? dock.screen.devicePixelRatio : 1
function px(v) {
return Math.round(v * _dpr) / _dpr;
}
DockGeometry {
id: dockGeometry
screen: dock.screen || dock.modelData
edge: dock.connectedBarSide
dockVisible: dock.visible
autoHide: dock.autoHide
iconSize: dock.widgetHeight
spacing: SettingsData.dockSpacing
borderThickness: dock.borderThickness
offset: SettingsData.dockBottomGap
margin: SettingsData.dockMargin
barSpacing: dock.barSpacing
dpr: dock._dpr
}
// Dock window origin in screen-relative coordinates (FrameWindow space).
function _dockWindowOriginX() {
if (!dock.isVertical)
return 0;
if (SettingsData.dockPosition === SettingsData.Position.Right)
return (dock.screen ? dock.screen.width : 0) - dock.width;
return 0;
}
function _dockWindowOriginY() {
if (dock.isVertical)
return 0;
if (SettingsData.dockPosition === SettingsData.Position.Bottom)
return (dock.screen ? dock.screen.height : 0) - dock.height;
return 0;
}
readonly property string _dockScreenName: dock.modelData ? dock.modelData.name : (dock.screen ? dock.screen.name : "")
readonly property bool usesConnectedFrameChrome: CompositorService.usesConnectedFrameChromeForScreen(dock._dockScreenName)
readonly property bool usesOverlayLayer: CompositorService.framePeerSurfacesUseOverlayForScreen(dock._dockScreenName) || SettingsData.dockUseOverlayLayer
function _syncDockChromeState() {
if (!dock._dockScreenName)
return;
if (!dock.usesConnectedFrameChrome) {
ConnectedModeState.clearDockState(dock._dockScreenName);
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, {
"kind": "dock",
"screenName": dock._dockScreenName,
"phase": phase,
"visible": presented,
"presented": presented,
"reveal": presented,
"barSide": dock.connectedBarSide,
"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
});
}
function _syncDockSlide() {
if (!dock._dockScreenName || !dock.usesConnectedFrameChrome)
return;
ConnectedModeState.setDockSlide(dock._dockScreenName, dockSlide.x, dockSlide.y);
}
DeferredAction {
id: dockSlideSync
enabled: dock.usesConnectedFrameChrome
onTriggered: dock._syncDockSlide()
}
function _queueSlideSync() {
if (!dock.usesConnectedFrameChrome)
return;
dockSlideSync.schedule();
}
DeferredAction {
id: dockChromeSync
onTriggered: dock._syncDockChromeState()
}
property bool contextMenuOpen: (dockVariants.contextMenu && dockVariants.contextMenu.visible && dockVariants.contextMenu.screen === modelData)
property bool revealSticky: false
readonly property bool shouldHideForWindows: {
if (!SettingsData.dockSmartAutoHide)
return false;
if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango)
return false;
const screenName = dock.modelData?.name ?? "";
const dockThickness = dockGeometry.motionThickness;
const screenWidth = dock.screen?.width ?? 0;
const screenHeight = dock.screen?.height ?? 0;
if (CompositorService.isNiri) {
NiriService.windows;
let currentWorkspaceId = null;
for (let i = 0; i < NiriService.allWorkspaces.length; i++) {
const ws = NiriService.allWorkspaces[i];
if (ws.output === screenName && ws.is_active) {
currentWorkspaceId = ws.id;
break;
}
}
if (currentWorkspaceId === null)
return false;
for (let i = 0; i < NiriService.windows.length; i++) {
const win = NiriService.windows[i];
if (win.workspace_id !== currentWorkspaceId)
continue;
// Get window position and size from layout data
const tilePos = win.layout?.tile_pos_in_workspace_view;
const winSize = win.layout?.window_size || win.layout?.tile_size;
if (tilePos && winSize) {
const winX = tilePos[0];
const winY = tilePos[1];
const winW = winSize[0];
const winH = winSize[1];
switch (SettingsData.dockPosition) {
case SettingsData.Position.Top:
if (winY < dockThickness)
return true;
break;
case SettingsData.Position.Bottom:
if (winY + winH > screenHeight - dockThickness)
return true;
break;
case SettingsData.Position.Left:
if (winX < dockThickness)
return true;
break;
case SettingsData.Position.Right:
if (winX + winW > screenWidth - dockThickness)
return true;
break;
}
} else if (!win.is_floating) {
return true;
}
}
return false;
}
if (CompositorService.isMango) {
MangoService.windows;
MangoService.outputs;
return CompositorService.mangoDockOverlapForSmartAutoHide(screenName, SettingsData.dockPosition, dockThickness, screenWidth, screenHeight);
}
// Hyprland implementation (current workspace + visible special workspaces)
Hyprland.focusedWorkspace;
Hyprland.toplevels;
return CompositorService.hyprlandDockOverlapForSmartAutoHide(screenName, SettingsData.dockPosition, dockThickness, screenWidth, screenHeight);
}
Timer {
id: revealHold
interval: 250
repeat: false
onTriggered: dock.revealSticky = false
}
// Flip `reveal` false when a modal claims this edge; reuses the slide animation
readonly property bool _modalRetractActive: {
if (!dock._dockScreenName)
return false;
return ConnectedModeState.dockRetractActiveForSide(dock._dockScreenName, dock.connectedBarSide);
}
property bool startupRevealDone: false
Timer {
id: startupRevealTimer
interval: 200
running: true
onTriggered: dock.startupRevealDone = true
}
property bool reveal: {
if (!startupRevealDone)
return false;
if (_modalRetractActive)
return false;
if (CompositorService.isNiri && NiriService.inOverview && SettingsData.dockOpenOnOverview) {
return true;
}
// Smart auto-hide: show dock when no windows overlap, hide when they do
if (SettingsData.dockSmartAutoHide) {
if (shouldHideForWindows)
return dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen || revealSticky;
return true; // No overlapping windows - show dock
}
// Regular auto-hide: always hide unless hovering
return !autoHide || dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen || revealSticky;
}
onContextMenuOpenChanged: {
if (!contextMenuOpen && autoHide && !dockMouseArea.containsMouse) {
revealSticky = true;
revealHold.restart();
}
}
Component.onCompleted: dockChromeSync.schedule()
Component.onDestruction: {
dockChromeSync.cancel();
dockSlideSync.cancel();
ConnectedModeState.clearDockState(dock._dockScreenName);
}
onRevealChanged: dock._syncDockChromeState()
onWidthChanged: dock._syncDockChromeState()
onHeightChanged: dock._syncDockChromeState()
onVisibleChanged: dock._syncDockChromeState()
onHasAppsChanged: dock._syncDockChromeState()
onConnectedBarSideChanged: dock._syncDockChromeState()
onUsesConnectedFrameChromeChanged: dock._syncDockChromeState()
Connections {
target: SettingsData
function onConnectedFrameModeActiveChanged() {
dockSlideSync.cancel();
dock._syncDockChromeState();
}
}
Connections {
target: SettingsData
function onDockTransparencyChanged() {
dock.backgroundTransparency = SettingsData.dockTransparency;
}
}
screen: modelData screen: modelData
color: "transparent"
WlrLayershell.namespace: "dms:dock"
WlrLayershell.layer: body.usesOverlayLayer ? WlrLayer.Overlay : WlrLayer.Top
anchors {
top: !body.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top) : true
bottom: !body.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom) : true
left: !body.isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Left)
right: !body.isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Right)
}
visible: { visible: {
if (CompositorService.isNiri && NiriService.inOverview) { if (CompositorService.isNiri && NiriService.inOverview) {
return SettingsData.dockOpenOnOverview; return SettingsData.dockOpenOnOverview;
} }
return SettingsData.showDock; return SettingsData.showDock;
} }
color: "transparent"
readonly property real dockReserveZone: dockGeometry.reserveZone implicitWidth: body.surfaceImplicitWidth
readonly property bool shouldReserveDockSpace: dockGeometry.shouldReserveSpace implicitHeight: body.surfaceImplicitHeight
exclusiveZone: body.surfaceExclusiveZone
exclusiveZone: { WindowBlur {
if (!dock.shouldReserveDockSpace) targetWindow: dock
return -1; blurEnabled: body.effectiveBlurEnabled && !body.usesConnectedFrameChrome
if (dock.frameDockExclusionActive) blurX: body.blurX
return -1; blurY: body.blurY
return dock.dockReserveZone; blurWidth: body.blurWidth
} blurHeight: body.blurHeight
blurRadius: body.blurRadius
property real animationHeadroom: Math.ceil(SettingsData.dockIconSize * 0.35)
implicitWidth: isVertical ? (px(dockGeometry.surfaceThickness + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
implicitHeight: !isVertical ? (px(dockGeometry.surfaceThickness + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
Item {
id: maskItem
parent: dock.contentItem
visible: false
readonly property bool expanded: dock.reveal
x: {
const baseX = dockCore.x + dockMouseArea.x;
if (isVertical && SettingsData.dockPosition === SettingsData.Position.Right)
return baseX - (expanded ? animationHeadroom + borderThickness + dock.horizontalConnectorExtent : 0);
return baseX - (expanded ? borderThickness + dock.horizontalConnectorExtent : 0);
}
y: {
const baseY = dockCore.y + dockMouseArea.y;
if (!isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom)
return baseY - (expanded ? animationHeadroom + borderThickness + dock.verticalConnectorExtent : 0);
return baseY - (expanded ? borderThickness + dock.verticalConnectorExtent : 0);
}
width: dockMouseArea.width + (isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.horizontalConnectorExtent * 2 : 0)
height: dockMouseArea.height + (!isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.verticalConnectorExtent * 2 : 0)
} }
mask: Region { mask: Region {
item: maskItem item: body.inputMaskItem
}
DockBody {
id: body
anchors.fill: parent
hostWindow: dock
modelData: dock.modelData
contextMenu: dockVariants.contextMenu
trashContextMenu: dockVariants.trashContextMenu
} }
PanelWindow { PanelWindow {
id: dockExclusion id: dockExclusion
screen: dock.screen || dock.modelData screen: dock.screen || dock.modelData
visible: dock.frameDockExclusionActive && dock.shouldReserveDockSpace visible: body.frameDockExclusionActive && body.shouldReserveDockSpace
color: "transparent" color: "transparent"
mask: Region {} mask: Region {}
implicitWidth: dock.isVertical ? dock.dockReserveZone : 1 implicitWidth: body.isVertical ? body.dockReserveZone : 1
implicitHeight: dock.isVertical ? 1 : dock.dockReserveZone implicitHeight: body.isVertical ? 1 : body.dockReserveZone
exclusiveZone: visible ? dock.dockReserveZone : -1 exclusiveZone: visible ? body.dockReserveZone : -1
WlrLayershell.namespace: "dms:dock-exclusion" WlrLayershell.namespace: "dms:dock-exclusion"
WlrLayershell.layer: WlrLayer.Top WlrLayershell.layer: WlrLayer.Top
anchors { anchors {
top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top) : true top: !body.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top) : true
bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom) : true bottom: !body.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom) : true
left: !dock.isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Left) left: !body.isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Left)
right: !dock.isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Right) right: !body.isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Right)
}
}
property var hoveredButton: {
if (!dockApps.children[0]) {
return null;
}
const layoutItem = dockApps.children[0];
const flowLayout = layoutItem.children[0];
let repeater = null;
for (var i = 0; i < flowLayout.children.length; i++) {
const child = flowLayout.children[i];
if (child && typeof child.count !== "undefined" && typeof child.itemAt === "function") {
repeater = child;
break;
}
}
if (!repeater || !repeater.itemAt) {
return null;
}
for (var i = 0; i < repeater.count; i++) {
const item = repeater.itemAt(i);
if (item && item.dockButton && item.dockButton.showTooltip) {
return item.dockButton;
}
}
return null;
}
DankTooltip {
id: dockTooltip
targetScreen: dock.screen
}
Timer {
id: tooltipRevealDelay
interval: 250
repeat: false
onTriggered: dock.showTooltipForHoveredButton()
}
function showTooltipForHoveredButton() {
dockTooltip.hide();
if (!dock.hoveredButton || !dock.reveal || slideXAnimation.running || slideYAnimation.running)
return;
const buttonLocalPos = dock.hoveredButton.mapToItem(null, 0, 0);
const tooltipText = dock.hoveredButton.tooltipText || "";
if (!tooltipText)
return;
const screenHeight = dock.screen ? dock.screen.height : 0;
const gap = Theme.spacingS;
const bgMargin = dockGeometry.bodyEdgeMargin;
const btnW = dock.hoveredButton.width;
const btnH = dock.hoveredButton.height;
if (!dock.isVertical) {
const isBottom = SettingsData.dockPosition === SettingsData.Position.Bottom;
const tooltipX = buttonLocalPos.x + btnW / 2 + adjacentLeftBarWidth;
const tooltipHeight = 32;
const totalFromEdge = bgMargin + dockBackground.height + dock.borderThickness + gap;
const screenRelativeY = isBottom ? (screenHeight - totalFromEdge - tooltipHeight) : totalFromEdge;
dockTooltip.show(tooltipText, tooltipX, screenRelativeY, dock.screen, false, false);
return;
}
const isLeft = SettingsData.dockPosition === SettingsData.Position.Left;
const screenWidth = dock.screen ? dock.screen.width : 0;
const totalFromEdge = bgMargin + dockBackground.width + dock.borderThickness + gap;
const tooltipX = isLeft ? totalFromEdge : (screenWidth - totalFromEdge);
const screenRelativeY = buttonLocalPos.y + btnH / 2 + adjacentTopBarHeight;
dockTooltip.show(tooltipText, tooltipX, screenRelativeY, dock.screen, isLeft, !isLeft);
}
Connections {
target: dock
function onRevealChanged() {
if (!dock.reveal) {
tooltipRevealDelay.stop();
dockTooltip.hide();
} else {
tooltipRevealDelay.restart();
}
}
function onHoveredButtonChanged() {
dock.showTooltipForHoveredButton();
}
}
Item {
id: dockCore
anchors.fill: parent
x: isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? animationHeadroom : 0
y: !isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? animationHeadroom : 0
Connections {
target: dockMouseArea
function onContainsMouseChanged() {
if (dockMouseArea.containsMouse) {
dock.revealSticky = true;
revealHold.stop();
} else {
if (dock.autoHide && !dock.contextMenuOpen) {
revealHold.restart();
}
}
}
}
MouseArea {
id: dockMouseArea
property real currentScreen: modelData ? modelData : dock.screen
property real screenWidth: currentScreen ? currentScreen.geometry.width : 1920
property real screenHeight: currentScreen ? currentScreen.geometry.height : 1080
property real maxDockWidth: screenWidth * 0.98
property real maxDockHeight: screenHeight * 0.98
height: {
if (dock.isVertical) {
// Keep the taller hit area regardless of the reveal state to prevent shrinking loop
return Math.min(Math.max(dockBackground.height + 64, 200), maxDockHeight);
}
return dock.reveal ? px(dockGeometry.motionThickness) : 1;
}
width: {
if (dock.isVertical) {
return dock.reveal ? px(dockGeometry.motionThickness) : 1;
}
// Keep the wider hit area regardless of the reveal state to prevent shrinking loop
return Math.min(dockBackground.width + 8 + dock.borderThickness, maxDockWidth);
}
anchors {
top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? undefined : parent.top) : undefined
bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined) : undefined
horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined
left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? undefined : parent.left) : undefined
right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined
verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
}
hoverEnabled: true
acceptedButtons: Qt.NoButton
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Easing.OutCubic
}
}
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Easing.OutCubic
}
}
Item {
id: dockContainer
anchors.fill: parent
clip: false
opacity: dock.startupRevealDone ? 1 : 0
transform: Translate {
id: dockSlide
x: {
if (!dock.isVertical)
return 0;
if (dock.reveal)
return 0;
if (dock.usesConnectedFrameChrome) {
const retractDist = dockBackground.width + SettingsData.dockSpacing + 10;
return SettingsData.dockPosition === SettingsData.Position.Right ? retractDist : -retractDist;
}
const hideDistance = dockGeometry.motionThickness + 10;
if (SettingsData.dockPosition === SettingsData.Position.Right) {
return hideDistance;
} else {
return -hideDistance;
}
}
y: {
if (dock.isVertical)
return 0;
if (dock.reveal)
return 0;
if (dock.usesConnectedFrameChrome) {
const retractDist = dockBackground.height + SettingsData.dockSpacing + 10;
return SettingsData.dockPosition === SettingsData.Position.Bottom ? retractDist : -retractDist;
}
const hideDistance = dockGeometry.motionThickness + 10;
if (SettingsData.dockPosition === SettingsData.Position.Bottom) {
return hideDistance;
} else {
return -hideDistance;
}
}
Behavior on x {
NumberAnimation {
id: slideXAnimation
duration: dock.usesConnectedFrameChrome ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration
easing.type: dock.usesConnectedFrameChrome ? Easing.BezierSpline : Easing.OutCubic
easing.bezierCurve: dock.usesConnectedFrameChrome ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : []
onRunningChanged: if (!running)
dock._syncDockChromeState()
}
}
Behavior on y {
NumberAnimation {
id: slideYAnimation
duration: dock.usesConnectedFrameChrome ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration
easing.type: dock.usesConnectedFrameChrome ? Easing.BezierSpline : Easing.OutCubic
easing.bezierCurve: dock.usesConnectedFrameChrome ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : []
onRunningChanged: if (!running)
dock._syncDockChromeState()
}
}
onXChanged: dock._queueSlideSync()
onYChanged: dock._queueSlideSync()
}
Item {
id: dockBackground
objectName: "dockBackground"
visible: dock.hasApps
anchors {
top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top ? parent.top : undefined) : undefined
bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined) : undefined
horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined
left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined) : undefined
right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined
verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
}
anchors.topMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Top ? dockGeometry.bodyEdgeMargin : 0
anchors.bottomMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? dockGeometry.bodyEdgeMargin : 0
anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? dockGeometry.bodyEdgeMargin : 0
anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? dockGeometry.bodyEdgeMargin : 0
implicitWidth: dock.isVertical ? (dockApps.implicitHeight + SettingsData.dockSpacing * 2) : (dockApps.implicitWidth + SettingsData.dockSpacing * 2)
implicitHeight: dock.isVertical ? (dockApps.implicitWidth + SettingsData.dockSpacing * 2) : (dockApps.implicitHeight + SettingsData.dockSpacing * 2)
width: implicitWidth
height: implicitHeight
// Avoid an offscreen texture seam where the connected dock meets the frame.
layer.enabled: !usesConnectedFrameChrome
clip: false
Rectangle {
anchors.fill: parent
visible: !usesConnectedFrameChrome && (!FrameTransitionState.effectiveConnectedFrameModeActive || dock.reveal)
color: dock.surfaceColor
topLeftRadius: dock.surfaceTopLeftRadius
topRightRadius: dock.surfaceTopRightRadius
bottomLeftRadius: dock.surfaceBottomLeftRadius
bottomRightRadius: dock.surfaceBottomRightRadius
}
Rectangle {
anchors.fill: parent
visible: !usesConnectedFrameChrome && (!FrameTransitionState.effectiveConnectedFrameModeActive || dock.reveal)
color: "transparent"
topLeftRadius: dock.surfaceTopLeftRadius
topRightRadius: dock.surfaceTopRightRadius
bottomLeftRadius: dock.surfaceBottomLeftRadius
bottomRightRadius: dock.surfaceBottomRightRadius
border.color: dock.surfaceBorderColor
border.width: dock.surfaceBorderWidth
z: 100
}
// Sync dockBackground geometry to ConnectedModeState
onXChanged: dock._syncDockChromeState()
onYChanged: dock._syncDockChromeState()
onWidthChanged: dock._syncDockChromeState()
onHeightChanged: dock._syncDockChromeState()
}
Item {
id: dockConnectedChrome
visible: Theme.isConnectedEffect && dock.reveal && dock.hasApps && !FrameTransitionState.effectiveConnectedFrameModeActive
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 {
id: dockBorderShape
x: dockBackground.x - borderThickness
y: dockBackground.y - borderThickness
width: dockBackground.width + borderThickness * 2
height: dockBackground.height + borderThickness * 2
visible: SettingsData.dockBorderEnabled && dock.hasApps && !usesConnectedFrameChrome
preferredRendererType: Shape.CurveRenderer
readonly property real borderThickness: Math.max(1, dock.borderThickness)
readonly property real i: borderThickness / 2
readonly property real cr: dock.surfaceRadius
readonly property real w: dockBackground.width
readonly property real h: dockBackground.height
readonly property color borderColor: {
const opacity = SettingsData.dockBorderOpacity;
switch (SettingsData.dockBorderColor) {
case "secondary":
return Theme.withAlpha(Theme.secondary, opacity);
case "primary":
return Theme.withAlpha(Theme.primary, opacity);
default:
return Theme.withAlpha(Theme.surfaceText, opacity);
}
}
ShapePath {
fillColor: "transparent"
strokeColor: dockBorderShape.borderColor
strokeWidth: dockBorderShape.borderThickness
joinStyle: ShapePath.RoundJoin
capStyle: ShapePath.FlatCap
PathSvg {
path: {
const bt = dockBorderShape.borderThickness;
const i = dockBorderShape.i;
const cr = dockBorderShape.cr + bt - i;
const w = dockBorderShape.w;
const h = dockBorderShape.h;
let d = `M ${i + cr} ${i}`;
d += ` L ${i + w + 2 * (bt - i) - cr} ${i}`;
if (cr > 0)
d += ` A ${cr} ${cr} 0 0 1 ${i + w + 2 * (bt - i)} ${i + cr}`;
d += ` L ${i + w + 2 * (bt - i)} ${i + h + 2 * (bt - i) - cr}`;
if (cr > 0)
d += ` A ${cr} ${cr} 0 0 1 ${i + w + 2 * (bt - i) - cr} ${i + h + 2 * (bt - i)}`;
d += ` L ${i + cr} ${i + h + 2 * (bt - i)}`;
if (cr > 0)
d += ` A ${cr} ${cr} 0 0 1 ${i} ${i + h + 2 * (bt - i) - cr}`;
d += ` L ${i} ${i + cr}`;
if (cr > 0)
d += ` A ${cr} ${cr} 0 0 1 ${i + cr} ${i}`;
d += " Z";
return d;
}
}
}
}
DockApps {
id: dockApps
anchors.top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top ? dockBackground.top : undefined) : undefined
anchors.bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? dockBackground.bottom : undefined) : undefined
anchors.horizontalCenter: !dock.isVertical ? dockBackground.horizontalCenter : undefined
anchors.left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Left ? dockBackground.left : undefined) : undefined
anchors.right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? dockBackground.right : undefined) : undefined
anchors.verticalCenter: dock.isVertical ? dockBackground.verticalCenter : undefined
anchors.topMargin: !dock.isVertical ? SettingsData.dockSpacing : 0
anchors.bottomMargin: !dock.isVertical ? SettingsData.dockSpacing : 0
anchors.leftMargin: dock.isVertical ? SettingsData.dockSpacing : 0
anchors.rightMargin: dock.isVertical ? SettingsData.dockSpacing : 0
contextMenu: dockVariants.contextMenu
trashContextMenu: dockVariants.trashContextMenu
groupByApp: dock.groupByApp
isVertical: dock.isVertical
dockScreen: dock.screen
iconSize: dock.widgetHeight
usesOverlayLayer: dock.usesOverlayLayer
}
}
} }
} }
} }
+829
View File
@@ -0,0 +1,829 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Shapes
import Quickshell.Hyprland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: dock
required property var hostWindow
required property var modelData
readonly property var screen: modelData
property var contextMenu
property var trashContextMenu
readonly property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
property bool autoHide: SettingsData.dockAutoHide || SettingsData.dockSmartAutoHide
property real backgroundTransparency: SettingsData.dockTransparency
property bool groupByApp: SettingsData.dockGroupByApp
readonly property int borderThickness: SettingsData.dockBorderEnabled ? SettingsData.dockBorderThickness : 0
readonly property string connectedBarSide: SettingsData.dockPosition === SettingsData.Position.Top ? "top" : SettingsData.dockPosition === SettingsData.Position.Bottom ? "bottom" : SettingsData.dockPosition === SettingsData.Position.Left ? "left" : "right"
readonly property bool frameDockExclusionActive: dockGeometry.frameExclusionActive
readonly property bool connectedBarActiveOnEdge: dockGeometry.connectedBarActiveOnEdge
readonly property real connectedJoinInset: dockGeometry.connectedJoinInset
readonly property real dockFrameInset: dockGeometry.frameInset
readonly property real surfaceRadius: usesConnectedFrameChrome ? Theme.connectedSurfaceRadius : Theme.cornerRadius
readonly property color surfaceColor: usesConnectedFrameChrome ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency)
readonly property color surfaceBorderColor: usesConnectedFrameChrome ? Theme.withAlpha(BlurService.borderColor, 0) : BlurService.borderColor
readonly property real surfaceBorderWidth: usesConnectedFrameChrome ? 0 : BlurService.borderWidth
readonly property real surfaceTopLeftRadius: usesConnectedFrameChrome && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius
readonly property real surfaceTopRightRadius: usesConnectedFrameChrome && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius
readonly property real surfaceBottomLeftRadius: usesConnectedFrameChrome && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius
readonly property real surfaceBottomRightRadius: usesConnectedFrameChrome && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius
readonly property real horizontalConnectorExtent: usesConnectedFrameChrome && !isVertical ? Theme.connectedCornerRadius : 0
readonly property real verticalConnectorExtent: usesConnectedFrameChrome && isVertical ? Theme.connectedCornerRadius : 0
readonly property int hasApps: dockApps.implicitWidth > 0 || dockApps.implicitHeight > 0
readonly property real widgetHeight: SettingsData.dockIconSize
readonly property real effectiveBarHeight: dockGeometry.visualThickness
function getBarHeight(barConfig) {
if (!barConfig)
return 0;
const innerPadding = barConfig.innerPadding ?? 4;
const widgetThickness = Math.max(20, 26 + innerPadding * 0.6);
const barThickness = Math.max(widgetThickness + innerPadding + 4, Theme.barHeight - 4 - (8 - innerPadding));
const spacing = barConfig.spacing ?? 4;
const bottomGap = barConfig.bottomGap ?? 0;
return barThickness + spacing + bottomGap;
}
readonly property real barSpacing: {
const defaultBar = SettingsData.barConfigs[0] || SettingsData.getBarConfig("default");
if (!defaultBar)
return 0;
const barPos = defaultBar.position ?? SettingsData.Position.Top;
const barIsHorizontal = (barPos === SettingsData.Position.Top || barPos === SettingsData.Position.Bottom);
const barIsVertical = (barPos === SettingsData.Position.Left || barPos === SettingsData.Position.Right);
const samePosition = (SettingsData.dockPosition === barPos);
const dockIsHorizontal = !isVertical;
const dockIsVertical = isVertical;
if (!(defaultBar.visible ?? true))
return 0;
const spacing = defaultBar.spacing ?? 4;
const bottomGap = defaultBar.bottomGap ?? 0;
if (dockIsHorizontal && barIsHorizontal && samePosition) {
return spacing + effectiveBarHeight + bottomGap;
}
if (dockIsVertical && barIsVertical && samePosition) {
return spacing + effectiveBarHeight + bottomGap;
}
return 0;
}
readonly property real adjacentTopBarHeight: {
if (!isVertical || autoHide)
return 0;
const screenName = dock.modelData?.name ?? "";
const topBar = SettingsData.barConfigs.find(bc => {
if (!bc.enabled || bc.autoHide || !(bc.visible ?? true))
return false;
if (bc.position !== SettingsData.Position.Top && bc.position !== 0)
return false;
const onThisScreen = bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all") || bc.screenPreferences.includes(screenName);
return onThisScreen;
});
return getBarHeight(topBar);
}
readonly property real adjacentLeftBarWidth: {
if (isVertical || autoHide)
return 0;
const screenName = dock.modelData?.name ?? "";
const leftBar = SettingsData.barConfigs.find(bc => {
if (!bc.enabled || bc.autoHide || !(bc.visible ?? true))
return false;
if (bc.position !== SettingsData.Position.Left && bc.position !== 2)
return false;
const onThisScreen = bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all") || bc.screenPreferences.includes(screenName);
return onThisScreen;
});
return getBarHeight(leftBar);
}
readonly property real dockMargin: SettingsData.dockMargin
readonly property bool effectiveBlurEnabled: Theme.connectedSurfaceBlurEnabled
readonly property real effectiveDockBottomGap: dockGeometry.visualOffset
readonly property real effectiveDockMargin: dockGeometry.effectiveMargin
readonly property real positionSpacing: barSpacing + effectiveDockBottomGap + effectiveDockMargin
readonly property real joinedEdgeMargin: dockGeometry.joinedEdgeMargin
readonly property real _dpr: (dock.screen && dock.screen.devicePixelRatio) ? dock.screen.devicePixelRatio : 1
function px(v) {
return Math.round(v * _dpr) / _dpr;
}
DockGeometry {
id: dockGeometry
screen: dock.screen || dock.modelData
edge: dock.connectedBarSide
dockVisible: dock.visible
autoHide: dock.autoHide
iconSize: dock.widgetHeight
spacing: SettingsData.dockSpacing
borderThickness: dock.borderThickness
offset: SettingsData.dockBottomGap
margin: SettingsData.dockMargin
barSpacing: dock.barSpacing
dpr: dock._dpr
}
// Dock window origin in screen-relative coordinates (FrameWindow space).
function _dockWindowOriginX() {
if (!dock.isVertical)
return 0;
if (SettingsData.dockPosition === SettingsData.Position.Right)
return (dock.screen ? dock.screen.width : 0) - dock.width;
return 0;
}
function _dockWindowOriginY() {
if (dock.isVertical)
return 0;
if (SettingsData.dockPosition === SettingsData.Position.Bottom)
return (dock.screen ? dock.screen.height : 0) - dock.height;
return 0;
}
readonly property string _dockScreenName: dock.modelData ? dock.modelData.name : (dock.screen ? dock.screen.name : "")
readonly property bool usesConnectedFrameChrome: CompositorService.usesConnectedFrameChromeForScreen(dock._dockScreenName)
readonly property bool usesOverlayLayer: CompositorService.framePeerSurfacesUseOverlayForScreen(dock._dockScreenName) || SettingsData.dockUseOverlayLayer
function _syncDockChromeState() {
if (!dock._dockScreenName)
return;
if (!dock.usesConnectedFrameChrome) {
ConnectedModeState.clearDockState(dock._dockScreenName);
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, {
"kind": "dock",
"screenName": dock._dockScreenName,
"phase": phase,
"visible": presented,
"presented": presented,
"reveal": presented,
"barSide": dock.connectedBarSide,
"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
});
}
function _syncDockSlide() {
if (!dock._dockScreenName || !dock.usesConnectedFrameChrome)
return;
ConnectedModeState.setDockSlide(dock._dockScreenName, dockSlide.x, dockSlide.y);
}
DeferredAction {
id: dockSlideSync
enabled: dock.usesConnectedFrameChrome
onTriggered: dock._syncDockSlide()
}
function _queueSlideSync() {
if (!dock.usesConnectedFrameChrome)
return;
dockSlideSync.schedule();
}
DeferredAction {
id: dockChromeSync
onTriggered: dock._syncDockChromeState()
}
property bool contextMenuOpen: (dock.contextMenu && dock.contextMenu.visible && dock.contextMenu.screen === modelData)
property bool revealSticky: false
readonly property bool shouldHideForWindows: {
if (!SettingsData.dockSmartAutoHide)
return false;
if (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isMango)
return false;
const screenName = dock.modelData?.name ?? "";
const dockThickness = dockGeometry.motionThickness;
const screenWidth = dock.screen?.width ?? 0;
const screenHeight = dock.screen?.height ?? 0;
if (CompositorService.isNiri) {
NiriService.windows;
let currentWorkspaceId = null;
for (let i = 0; i < NiriService.allWorkspaces.length; i++) {
const ws = NiriService.allWorkspaces[i];
if (ws.output === screenName && ws.is_active) {
currentWorkspaceId = ws.id;
break;
}
}
if (currentWorkspaceId === null)
return false;
for (let i = 0; i < NiriService.windows.length; i++) {
const win = NiriService.windows[i];
if (win.workspace_id !== currentWorkspaceId)
continue;
// Get window position and size from layout data
const tilePos = win.layout?.tile_pos_in_workspace_view;
const winSize = win.layout?.window_size || win.layout?.tile_size;
if (tilePos && winSize) {
const winX = tilePos[0];
const winY = tilePos[1];
const winW = winSize[0];
const winH = winSize[1];
switch (SettingsData.dockPosition) {
case SettingsData.Position.Top:
if (winY < dockThickness)
return true;
break;
case SettingsData.Position.Bottom:
if (winY + winH > screenHeight - dockThickness)
return true;
break;
case SettingsData.Position.Left:
if (winX < dockThickness)
return true;
break;
case SettingsData.Position.Right:
if (winX + winW > screenWidth - dockThickness)
return true;
break;
}
} else if (!win.is_floating) {
return true;
}
}
return false;
}
if (CompositorService.isMango) {
MangoService.windows;
MangoService.outputs;
return CompositorService.mangoDockOverlapForSmartAutoHide(screenName, SettingsData.dockPosition, dockThickness, screenWidth, screenHeight);
}
// Hyprland implementation (current workspace + visible special workspaces)
Hyprland.focusedWorkspace;
Hyprland.toplevels;
return CompositorService.hyprlandDockOverlapForSmartAutoHide(screenName, SettingsData.dockPosition, dockThickness, screenWidth, screenHeight);
}
Timer {
id: revealHold
interval: 250
repeat: false
onTriggered: dock.revealSticky = false
}
// Flip `reveal` false when a modal claims this edge; reuses the slide animation
readonly property bool _modalRetractActive: {
if (!dock._dockScreenName)
return false;
return ConnectedModeState.dockRetractActiveForSide(dock._dockScreenName, dock.connectedBarSide);
}
property bool startupRevealDone: false
Timer {
id: startupRevealTimer
interval: 200
running: true
onTriggered: dock.startupRevealDone = true
}
property bool reveal: {
if (!startupRevealDone)
return false;
if (_modalRetractActive)
return false;
if (CompositorService.isNiri && NiriService.inOverview && SettingsData.dockOpenOnOverview) {
return true;
}
// Smart auto-hide: show dock when no windows overlap, hide when they do
if (SettingsData.dockSmartAutoHide) {
if (shouldHideForWindows)
return dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen || revealSticky;
return true; // No overlapping windows - show dock
}
// Regular auto-hide: always hide unless hovering
return !autoHide || dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen || revealSticky;
}
onContextMenuOpenChanged: {
if (!contextMenuOpen && autoHide && !dockMouseArea.containsMouse) {
revealSticky = true;
revealHold.restart();
}
}
Component.onCompleted: dockChromeSync.schedule()
Component.onDestruction: {
dockChromeSync.cancel();
dockSlideSync.cancel();
ConnectedModeState.clearDockState(dock._dockScreenName);
}
onRevealChanged: dock._syncDockChromeState()
onWidthChanged: dock._syncDockChromeState()
onHeightChanged: dock._syncDockChromeState()
onVisibleChanged: dock._syncDockChromeState()
onHasAppsChanged: dock._syncDockChromeState()
onConnectedBarSideChanged: dock._syncDockChromeState()
onUsesConnectedFrameChromeChanged: dock._syncDockChromeState()
Connections {
target: SettingsData
function onConnectedFrameModeActiveChanged() {
dockSlideSync.cancel();
dock._syncDockChromeState();
}
}
Connections {
target: SettingsData
function onDockTransparencyChanged() {
dock.backgroundTransparency = SettingsData.dockTransparency;
}
}
readonly property real dockReserveZone: dockGeometry.reserveZone
readonly property bool shouldReserveDockSpace: dockGeometry.shouldReserveSpace
readonly property real surfaceExclusiveZone: {
if (!dock.shouldReserveDockSpace)
return -1;
if (dock.frameDockExclusionActive)
return -1;
return dock.dockReserveZone;
}
property real animationHeadroom: Math.ceil(SettingsData.dockIconSize * 0.35)
readonly property real surfaceImplicitWidth: isVertical ? (px(dockGeometry.surfaceThickness + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
readonly property real surfaceImplicitHeight: !isVertical ? (px(dockGeometry.surfaceThickness + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
readonly property real blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x
readonly property real blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y
readonly property real blurWidth: dock.hasApps && dock.reveal ? dockBackground.width : 0
readonly property real blurHeight: dock.hasApps && dock.reveal ? dockBackground.height : 0
readonly property real blurRadius: dock.usesConnectedFrameChrome ? Theme.connectedCornerRadius : dock.surfaceRadius
Item {
id: maskItem
visible: false
readonly property bool expanded: dock.reveal
x: {
const baseX = dockCore.x + dockMouseArea.x;
if (isVertical && SettingsData.dockPosition === SettingsData.Position.Right)
return baseX - (expanded ? animationHeadroom + borderThickness + dock.horizontalConnectorExtent : 0);
return baseX - (expanded ? borderThickness + dock.horizontalConnectorExtent : 0);
}
y: {
const baseY = dockCore.y + dockMouseArea.y;
if (!isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom)
return baseY - (expanded ? animationHeadroom + borderThickness + dock.verticalConnectorExtent : 0);
return baseY - (expanded ? borderThickness + dock.verticalConnectorExtent : 0);
}
width: dockMouseArea.width + (isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.horizontalConnectorExtent * 2 : 0)
height: dockMouseArea.height + (!isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.verticalConnectorExtent * 2 : 0)
}
readonly property alias inputMaskItem: maskItem
property var hoveredButton: {
if (!dockApps.children[0]) {
return null;
}
const layoutItem = dockApps.children[0];
const flowLayout = layoutItem.children[0];
let repeater = null;
for (var i = 0; i < flowLayout.children.length; i++) {
const child = flowLayout.children[i];
if (child && typeof child.count !== "undefined" && typeof child.itemAt === "function") {
repeater = child;
break;
}
}
if (!repeater || !repeater.itemAt) {
return null;
}
for (var i = 0; i < repeater.count; i++) {
const item = repeater.itemAt(i);
if (item && item.dockButton && item.dockButton.showTooltip) {
return item.dockButton;
}
}
return null;
}
DankTooltip {
id: dockTooltip
targetScreen: dock.screen
}
Timer {
id: tooltipRevealDelay
interval: 250
repeat: false
onTriggered: dock.showTooltipForHoveredButton()
}
function showTooltipForHoveredButton() {
dockTooltip.hide();
if (!dock.hoveredButton || !dock.reveal || slideXAnimation.running || slideYAnimation.running)
return;
const buttonLocalPos = dock.hoveredButton.mapToItem(null, 0, 0);
const tooltipText = dock.hoveredButton.tooltipText || "";
if (!tooltipText)
return;
const screenHeight = dock.screen ? dock.screen.height : 0;
const gap = Theme.spacingS;
const bgMargin = dockGeometry.bodyEdgeMargin;
const btnW = dock.hoveredButton.width;
const btnH = dock.hoveredButton.height;
if (!dock.isVertical) {
const isBottom = SettingsData.dockPosition === SettingsData.Position.Bottom;
const tooltipX = buttonLocalPos.x + btnW / 2 + adjacentLeftBarWidth;
const tooltipHeight = 32;
const totalFromEdge = bgMargin + dockBackground.height + dock.borderThickness + gap;
const screenRelativeY = isBottom ? (screenHeight - totalFromEdge - tooltipHeight) : totalFromEdge;
dockTooltip.show(tooltipText, tooltipX, screenRelativeY, dock.screen, false, false);
return;
}
const isLeft = SettingsData.dockPosition === SettingsData.Position.Left;
const screenWidth = dock.screen ? dock.screen.width : 0;
const totalFromEdge = bgMargin + dockBackground.width + dock.borderThickness + gap;
const tooltipX = isLeft ? totalFromEdge : (screenWidth - totalFromEdge);
const screenRelativeY = buttonLocalPos.y + btnH / 2 + adjacentTopBarHeight;
dockTooltip.show(tooltipText, tooltipX, screenRelativeY, dock.screen, isLeft, !isLeft);
}
Connections {
target: dock
function onRevealChanged() {
if (!dock.reveal) {
tooltipRevealDelay.stop();
dockTooltip.hide();
} else {
tooltipRevealDelay.restart();
}
}
function onHoveredButtonChanged() {
dock.showTooltipForHoveredButton();
}
}
Item {
id: dockCore
anchors.fill: parent
x: isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? animationHeadroom : 0
y: !isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? animationHeadroom : 0
Connections {
target: dockMouseArea
function onContainsMouseChanged() {
if (dockMouseArea.containsMouse) {
dock.revealSticky = true;
revealHold.stop();
} else {
if (dock.autoHide && !dock.contextMenuOpen) {
revealHold.restart();
}
}
}
}
MouseArea {
id: dockMouseArea
property real currentScreen: modelData ? modelData : dock.screen
property real screenWidth: currentScreen ? currentScreen.geometry.width : 1920
property real screenHeight: currentScreen ? currentScreen.geometry.height : 1080
property real maxDockWidth: screenWidth * 0.98
property real maxDockHeight: screenHeight * 0.98
height: {
if (dock.isVertical) {
// Keep the taller hit area regardless of the reveal state to prevent shrinking loop
return Math.min(Math.max(dockBackground.height + 64, 200), maxDockHeight);
}
return dock.reveal ? px(dockGeometry.motionThickness) : 1;
}
width: {
if (dock.isVertical) {
return dock.reveal ? px(dockGeometry.motionThickness) : 1;
}
// Keep the wider hit area regardless of the reveal state to prevent shrinking loop
return Math.min(dockBackground.width + 8 + dock.borderThickness, maxDockWidth);
}
anchors {
top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? undefined : parent.top) : undefined
bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined) : undefined
horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined
left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? undefined : parent.left) : undefined
right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined
verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
}
hoverEnabled: true
acceptedButtons: Qt.NoButton
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Easing.OutCubic
}
}
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Easing.OutCubic
}
}
Item {
id: dockContainer
anchors.fill: parent
clip: false
opacity: dock.startupRevealDone ? 1 : 0
transform: Translate {
id: dockSlide
x: {
if (!dock.isVertical)
return 0;
if (dock.reveal)
return 0;
if (dock.usesConnectedFrameChrome) {
const retractDist = dockBackground.width + SettingsData.dockSpacing + 10;
return SettingsData.dockPosition === SettingsData.Position.Right ? retractDist : -retractDist;
}
const hideDistance = dockGeometry.motionThickness + 10;
if (SettingsData.dockPosition === SettingsData.Position.Right) {
return hideDistance;
} else {
return -hideDistance;
}
}
y: {
if (dock.isVertical)
return 0;
if (dock.reveal)
return 0;
if (dock.usesConnectedFrameChrome) {
const retractDist = dockBackground.height + SettingsData.dockSpacing + 10;
return SettingsData.dockPosition === SettingsData.Position.Bottom ? retractDist : -retractDist;
}
const hideDistance = dockGeometry.motionThickness + 10;
if (SettingsData.dockPosition === SettingsData.Position.Bottom) {
return hideDistance;
} else {
return -hideDistance;
}
}
Behavior on x {
NumberAnimation {
id: slideXAnimation
duration: dock.usesConnectedFrameChrome ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration
easing.type: dock.usesConnectedFrameChrome ? Easing.BezierSpline : Easing.OutCubic
easing.bezierCurve: dock.usesConnectedFrameChrome ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : []
onRunningChanged: if (!running)
dock._syncDockChromeState()
}
}
Behavior on y {
NumberAnimation {
id: slideYAnimation
duration: dock.usesConnectedFrameChrome ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) : Theme.shortDuration
easing.type: dock.usesConnectedFrameChrome ? Easing.BezierSpline : Easing.OutCubic
easing.bezierCurve: dock.usesConnectedFrameChrome ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : []
onRunningChanged: if (!running)
dock._syncDockChromeState()
}
}
onXChanged: dock._queueSlideSync()
onYChanged: dock._queueSlideSync()
}
Item {
id: dockBackground
objectName: "dockBackground"
visible: dock.hasApps
anchors {
top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top ? parent.top : undefined) : undefined
bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined) : undefined
horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined
left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined) : undefined
right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined
verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
}
anchors.topMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Top ? dockGeometry.bodyEdgeMargin : 0
anchors.bottomMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? dockGeometry.bodyEdgeMargin : 0
anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? dockGeometry.bodyEdgeMargin : 0
anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? dockGeometry.bodyEdgeMargin : 0
implicitWidth: dock.isVertical ? (dockApps.implicitHeight + SettingsData.dockSpacing * 2) : (dockApps.implicitWidth + SettingsData.dockSpacing * 2)
implicitHeight: dock.isVertical ? (dockApps.implicitWidth + SettingsData.dockSpacing * 2) : (dockApps.implicitHeight + SettingsData.dockSpacing * 2)
width: implicitWidth
height: implicitHeight
// Avoid an offscreen texture seam where the connected dock meets the frame.
layer.enabled: !usesConnectedFrameChrome
clip: false
Rectangle {
anchors.fill: parent
visible: !usesConnectedFrameChrome && (!FrameTransitionState.effectiveConnectedFrameModeActive || dock.reveal)
color: dock.surfaceColor
topLeftRadius: dock.surfaceTopLeftRadius
topRightRadius: dock.surfaceTopRightRadius
bottomLeftRadius: dock.surfaceBottomLeftRadius
bottomRightRadius: dock.surfaceBottomRightRadius
}
Rectangle {
anchors.fill: parent
visible: !usesConnectedFrameChrome && (!FrameTransitionState.effectiveConnectedFrameModeActive || dock.reveal)
color: "transparent"
topLeftRadius: dock.surfaceTopLeftRadius
topRightRadius: dock.surfaceTopRightRadius
bottomLeftRadius: dock.surfaceBottomLeftRadius
bottomRightRadius: dock.surfaceBottomRightRadius
border.color: dock.surfaceBorderColor
border.width: dock.surfaceBorderWidth
z: 100
}
// Sync dockBackground geometry to ConnectedModeState
onXChanged: dock._syncDockChromeState()
onYChanged: dock._syncDockChromeState()
onWidthChanged: dock._syncDockChromeState()
onHeightChanged: dock._syncDockChromeState()
}
Item {
id: dockConnectedChrome
visible: Theme.isConnectedEffect && dock.reveal && dock.hasApps && !FrameTransitionState.effectiveConnectedFrameModeActive
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 {
id: dockBorderShape
x: dockBackground.x - borderThickness
y: dockBackground.y - borderThickness
width: dockBackground.width + borderThickness * 2
height: dockBackground.height + borderThickness * 2
visible: SettingsData.dockBorderEnabled && dock.hasApps && !usesConnectedFrameChrome
preferredRendererType: Shape.CurveRenderer
readonly property real borderThickness: Math.max(1, dock.borderThickness)
readonly property real i: borderThickness / 2
readonly property real cr: dock.surfaceRadius
readonly property real w: dockBackground.width
readonly property real h: dockBackground.height
readonly property color borderColor: {
const opacity = SettingsData.dockBorderOpacity;
switch (SettingsData.dockBorderColor) {
case "secondary":
return Theme.withAlpha(Theme.secondary, opacity);
case "primary":
return Theme.withAlpha(Theme.primary, opacity);
default:
return Theme.withAlpha(Theme.surfaceText, opacity);
}
}
ShapePath {
fillColor: "transparent"
strokeColor: dockBorderShape.borderColor
strokeWidth: dockBorderShape.borderThickness
joinStyle: ShapePath.RoundJoin
capStyle: ShapePath.FlatCap
PathSvg {
path: {
const bt = dockBorderShape.borderThickness;
const i = dockBorderShape.i;
const cr = dockBorderShape.cr + bt - i;
const w = dockBorderShape.w;
const h = dockBorderShape.h;
let d = `M ${i + cr} ${i}`;
d += ` L ${i + w + 2 * (bt - i) - cr} ${i}`;
if (cr > 0)
d += ` A ${cr} ${cr} 0 0 1 ${i + w + 2 * (bt - i)} ${i + cr}`;
d += ` L ${i + w + 2 * (bt - i)} ${i + h + 2 * (bt - i) - cr}`;
if (cr > 0)
d += ` A ${cr} ${cr} 0 0 1 ${i + w + 2 * (bt - i) - cr} ${i + h + 2 * (bt - i)}`;
d += ` L ${i + cr} ${i + h + 2 * (bt - i)}`;
if (cr > 0)
d += ` A ${cr} ${cr} 0 0 1 ${i} ${i + h + 2 * (bt - i) - cr}`;
d += ` L ${i} ${i + cr}`;
if (cr > 0)
d += ` A ${cr} ${cr} 0 0 1 ${i + cr} ${i}`;
d += " Z";
return d;
}
}
}
}
DockApps {
id: dockApps
anchors.top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top ? dockBackground.top : undefined) : undefined
anchors.bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? dockBackground.bottom : undefined) : undefined
anchors.horizontalCenter: !dock.isVertical ? dockBackground.horizontalCenter : undefined
anchors.left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Left ? dockBackground.left : undefined) : undefined
anchors.right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? dockBackground.right : undefined) : undefined
anchors.verticalCenter: dock.isVertical ? dockBackground.verticalCenter : undefined
anchors.topMargin: !dock.isVertical ? SettingsData.dockSpacing : 0
anchors.bottomMargin: !dock.isVertical ? SettingsData.dockSpacing : 0
anchors.leftMargin: dock.isVertical ? SettingsData.dockSpacing : 0
anchors.rightMargin: dock.isVertical ? SettingsData.dockSpacing : 0
contextMenu: dock.contextMenu
trashContextMenu: dock.trashContextMenu
groupByApp: dock.groupByApp
isVertical: dock.isVertical
dockScreen: dock.screen
iconSize: dock.widgetHeight
usesOverlayLayer: dock.usesOverlayLayer
}
}
}
}
}
@@ -61,6 +61,11 @@ PanelWindow {
const actualDockHeight = dockBackground ? dockBackground.height : root.dockVisibleHeight; const actualDockHeight = dockBackground ? dockBackground.height : root.dockVisibleHeight;
const actualDockWidth = dockBackground ? dockBackground.width : dockWindow.width; const actualDockWidth = dockBackground ? dockBackground.width : dockWindow.width;
const dockMargin = SettingsData.dockMargin + 16; const dockMargin = SettingsData.dockMargin + 16;
// Connected mode hosts the dock inside the fullscreen frame surface, inset from the
// screen edge. Anchor off the dock body's real position instead of the screen edge.
const dockBgPos = dockBackground ? dockBackground.mapToItem(dockWindow.contentItem, 0, 0) : null;
const frameHosted = dockBgPos && dockWindow.width >= screen.width - 1 && dockWindow.height >= screen.height - 1;
let x = 0; let x = 0;
let y = 0; let y = 0;
@@ -68,14 +73,14 @@ PanelWindow {
case SettingsData.Position.Left: case SettingsData.Position.Left:
{ {
const dockTopMargin = Math.round((screen.height - dockWindow.height) / 2); const dockTopMargin = Math.round((screen.height - dockWindow.height) / 2);
x = actualDockWidth + dockMargin + 20; x = frameHosted ? (dockBgPos.x + actualDockWidth + dockMargin + 20) : (actualDockWidth + dockMargin + 20);
y = dockTopMargin + buttonPosInDock.y + anchorItem.height / 2; y = dockTopMargin + buttonPosInDock.y + anchorItem.height / 2;
break; break;
} }
case SettingsData.Position.Right: case SettingsData.Position.Right:
{ {
const dockTopMargin = Math.round((screen.height - dockWindow.height) / 2); const dockTopMargin = Math.round((screen.height - dockWindow.height) / 2);
x = screen.width - actualDockWidth - dockMargin - 20; x = frameHosted ? (dockBgPos.x - dockMargin - 20) : (screen.width - actualDockWidth - dockMargin - 20);
y = dockTopMargin + buttonPosInDock.y + anchorItem.height / 2; y = dockTopMargin + buttonPosInDock.y + anchorItem.height / 2;
break; break;
} }
@@ -83,7 +88,7 @@ PanelWindow {
{ {
const dockLeftMargin = Math.round((screen.width - dockWindow.width) / 2); const dockLeftMargin = Math.round((screen.width - dockWindow.width) / 2);
x = dockLeftMargin + buttonPosInDock.x + anchorItem.width / 2; x = dockLeftMargin + buttonPosInDock.x + anchorItem.width / 2;
y = actualDockHeight + dockMargin + 20; y = frameHosted ? (dockBgPos.y + actualDockHeight + dockMargin + 20) : (actualDockHeight + dockMargin + 20);
break; break;
} }
case SettingsData.Position.Bottom: case SettingsData.Position.Bottom:
@@ -91,7 +96,7 @@ PanelWindow {
{ {
const dockLeftMargin = Math.round((screen.width - dockWindow.width) / 2); const dockLeftMargin = Math.round((screen.width - dockWindow.width) / 2);
x = dockLeftMargin + buttonPosInDock.x + anchorItem.width / 2; x = dockLeftMargin + buttonPosInDock.x + anchorItem.width / 2;
y = screen.height - actualDockHeight - dockMargin - 20; y = frameHosted ? (dockBgPos.y - dockMargin - 20) : (screen.height - actualDockHeight - dockMargin - 20);
break; break;
} }
} }
+114
View File
@@ -0,0 +1,114 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Modules.DankBar
import qs.Services
// Renders the bar(s) inside the frame surface in connected mode: one DankBarBody per
// active bar edge, positioned in the frame's cutout band. Reuses the existing DankBar
// item (per bar config) as rootWindow so colour-picker, overview loader and widget
// models are shared with the standalone path.
Item {
id: host
required property var frameWindow
required property var targetScreen
readonly property string screenName: targetScreen ? targetScreen.name : ""
readonly property var barSlots: {
SettingsData.barConfigs;
const out = [];
const configs = SettingsData.barConfigs || [];
for (let i = 0; i < configs.length; i++) {
const bc = configs[i];
if (!bc.enabled || (bc.useOverlayLayer ?? false))
continue;
const prefs = bc.screenPreferences || ["all"];
if (!prefs.includes("all") && !SettingsData.isScreenInPreferences(host.targetScreen, prefs))
continue;
let edge = "top";
switch (bc.position ?? 0) {
case SettingsData.Position.Bottom:
edge = "bottom";
break;
case SettingsData.Position.Left:
edge = "left";
break;
case SettingsData.Position.Right:
edge = "right";
break;
}
out.push({
"barId": bc.id,
"edge": edge
});
}
// Horizontal bars own the corners, so render them last (on top) their edge widget
// must receive the corner click over the vertical bar's hover area beneath it.
out.sort((a, b) => {
const rank = e => (e === "left" || e === "right") ? 0 : 1;
return rank(a.edge) - rank(b.edge);
});
return out;
}
Repeater {
model: host.barSlots
delegate: Item {
id: slot
required property var modelData
readonly property string edge: modelData.edge
readonly property var dankBarItem: BarWidgetService.dankBarItems[modelData.barId] ?? null
readonly property var slotBarConfig: dankBarItem?.barConfig ?? SettingsData.getBarConfig(modelData.barId)
// Each bar spans its full edge (matching the standalone window extent) so
// DankBarContent's own adjacency margins inset it, rather than double-insetting
// against a pre-carved strip.
x: edge === "right" ? host.frameWindow._windowRegionWidth - host.frameWindow.cutoutRightInset : 0
y: edge === "bottom" ? host.frameWindow._windowRegionHeight - host.frameWindow.cutoutBottomInset : 0
width: {
switch (edge) {
case "left":
return host.frameWindow.cutoutLeftInset;
case "right":
return host.frameWindow.cutoutRightInset;
default:
return host.frameWindow._windowRegionWidth;
}
}
height: {
switch (edge) {
case "top":
return host.frameWindow.cutoutTopInset;
case "bottom":
return host.frameWindow.cutoutBottomInset;
default:
return host.frameWindow._windowRegionHeight;
}
}
Loader {
anchors.fill: parent
active: slot.dankBarItem !== null && slot.slotBarConfig !== null
sourceComponent: DankBarBody {
hostWindow: host.frameWindow
modelData: host.targetScreen
rootWindow: slot.dankBarItem
barConfig: slot.slotBarConfig
leftWidgetsModel: slot.dankBarItem?.leftWidgetsModel ?? null
centerWidgetsModel: slot.dankBarItem?.centerWidgetsModel ?? null
rightWidgetsModel: slot.dankBarItem?.rightWidgetsModel ?? null
Component.onCompleted: BarWidgetService.registerFrameBar(host.screenName, this)
Component.onDestruction: BarWidgetService.unregisterFrameBar(host.screenName, this)
}
}
}
}
}
@@ -0,0 +1,30 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Modules.Dock
import qs.Services
// Renders the dock inside the frame surface in connected mode: a single DockBody
// positioned by SettingsData.dockPosition. It fills the frame and positions its own
// content via its internal anchors/geometry using hostWindow.
Item {
id: host
required property var frameWindow
required property var targetScreen
readonly property string screenName: targetScreen ? targetScreen.name : ""
// Exposed so FrameWindow can add the dock's hover strip to its input region.
readonly property alias dockMaskItem: dockBody.inputMaskItem
DockBody {
id: dockBody
anchors.fill: parent
hostWindow: host.frameWindow
modelData: host.targetScreen
contextMenu: BarWidgetService.dockContextMenu
trashContextMenu: BarWidgetService.dockTrashContextMenu
}
}
+32 -7
View File
@@ -16,15 +16,33 @@ Scope {
return SettingsData.getActiveBarEdgesForScreen(screen); return SettingsData.getActiveBarEdgesForScreen(screen);
} }
// One thin invisible PanelWindow per edge. // Overlay-layer bars stay standalone even in connected mode and reserve their own edge.
// Skips any edge where a bar already provides its own exclusiveZone. readonly property var overlayBarEdges: {
SettingsData.barConfigs;
return SettingsData.getOverlayBarEdgesForScreen(root.screen);
}
// One thin invisible PanelWindow per edge. A standalone bar reserves its own edge, so
// that edge is skipped unless the frame hosts the bar (connected mode), where no bar
// window exists and the exclusion must reserve the full bar thickness itself.
readonly property bool screenEnabled: CompositorService.frameWindowVisibleForScreen(root.screen) readonly property bool screenEnabled: CompositorService.frameWindowVisibleForScreen(root.screen)
readonly property bool hostsBar: CompositorService.frameHostsSurfacesForScreen(root.screen)
function hostsBandForEdge(edge) {
return root.hostsBar && !root.overlayBarEdges.includes(edge);
}
function exclusionSizeForEdge(edge) {
return root.barEdges.includes(edge) ? SettingsData.frameBarSize : SettingsData.frameThickness;
}
Loader { Loader {
active: root.screenEnabled && !root.barEdges.includes("top") readonly property bool isBarEdge: root.barEdges.includes("top")
active: root.screenEnabled && (!isBarEdge || root.hostsBandForEdge("top"))
sourceComponent: EdgeExclusion { sourceComponent: EdgeExclusion {
targetScreen: root.screen targetScreen: root.screen
exclusionSize: root.exclusionSizeForEdge("top")
anchorTop: true anchorTop: true
anchorLeft: true anchorLeft: true
anchorRight: true anchorRight: true
@@ -32,9 +50,11 @@ Scope {
} }
Loader { Loader {
active: root.screenEnabled && !root.barEdges.includes("bottom") readonly property bool isBarEdge: root.barEdges.includes("bottom")
active: root.screenEnabled && (!isBarEdge || root.hostsBandForEdge("bottom"))
sourceComponent: EdgeExclusion { sourceComponent: EdgeExclusion {
targetScreen: root.screen targetScreen: root.screen
exclusionSize: root.exclusionSizeForEdge("bottom")
anchorBottom: true anchorBottom: true
anchorLeft: true anchorLeft: true
anchorRight: true anchorRight: true
@@ -42,9 +62,11 @@ Scope {
} }
Loader { Loader {
active: root.screenEnabled && !root.barEdges.includes("left") readonly property bool isBarEdge: root.barEdges.includes("left")
active: root.screenEnabled && (!isBarEdge || root.hostsBandForEdge("left"))
sourceComponent: EdgeExclusion { sourceComponent: EdgeExclusion {
targetScreen: root.screen targetScreen: root.screen
exclusionSize: root.exclusionSizeForEdge("left")
anchorLeft: true anchorLeft: true
anchorTop: true anchorTop: true
anchorBottom: true anchorBottom: true
@@ -52,9 +74,11 @@ Scope {
} }
Loader { Loader {
active: root.screenEnabled && !root.barEdges.includes("right") readonly property bool isBarEdge: root.barEdges.includes("right")
active: root.screenEnabled && (!isBarEdge || root.hostsBandForEdge("right"))
sourceComponent: EdgeExclusion { sourceComponent: EdgeExclusion {
targetScreen: root.screen targetScreen: root.screen
exclusionSize: root.exclusionSizeForEdge("right")
anchorRight: true anchorRight: true
anchorTop: true anchorTop: true
anchorBottom: true anchorBottom: true
@@ -63,6 +87,7 @@ Scope {
component EdgeExclusion: PanelWindow { component EdgeExclusion: PanelWindow {
required property var targetScreen required property var targetScreen
property real exclusionSize: SettingsData.frameThickness
screen: targetScreen screen: targetScreen
property bool anchorTop: false property bool anchorTop: false
@@ -72,7 +97,7 @@ Scope {
WlrLayershell.namespace: "dms:frame-exclusion" WlrLayershell.namespace: "dms:frame-exclusion"
WlrLayershell.layer: WlrLayer.Top WlrLayershell.layer: WlrLayer.Top
exclusiveZone: SettingsData.frameThickness exclusiveZone: exclusionSize
color: "transparent" color: "transparent"
mask: Region {} mask: Region {}
implicitWidth: 1 implicitWidth: 1
+95 -1
View File
@@ -32,7 +32,41 @@ PanelWindow {
} }
color: "transparent" color: "transparent"
mask: Region {} // Click-through everywhere except the bar strips it hosts in connected mode.
mask: Region {
Region {
readonly property bool present: win._connectedActive && win.barEdges.includes("top")
x: 0
y: 0
width: present ? win._windowRegionWidth : 0
height: present ? win.cutoutTopInset : 0
}
Region {
readonly property bool present: win._connectedActive && win.barEdges.includes("bottom")
x: 0
y: present ? win._windowRegionHeight - win.cutoutBottomInset : 0
width: present ? win._windowRegionWidth : 0
height: present ? win.cutoutBottomInset : 0
}
Region {
readonly property bool present: win._connectedActive && win.barEdges.includes("left")
x: 0
y: 0
width: present ? win.cutoutLeftInset : 0
height: present ? win._windowRegionHeight : 0
}
Region {
readonly property bool present: win._connectedActive && win.barEdges.includes("right")
x: present ? win._windowRegionWidth - win.cutoutRightInset : 0
y: 0
width: present ? win.cutoutRightInset : 0
height: present ? win._windowRegionHeight : 0
}
// The frame-hosted dock's hover strip, so hover-reveal works through the frame surface.
Region {
item: frameDockHostLoader.item ? frameDockHostLoader.item.dockMaskItem : null
}
}
readonly property var barEdges: { readonly property var barEdges: {
SettingsData.barConfigs; SettingsData.barConfigs;
@@ -51,6 +85,16 @@ PanelWindow {
readonly property var _modalDescriptor: ConnectedModeState.surfaceDescriptor(win._screenName, "modal") readonly property var _modalDescriptor: ConnectedModeState.surfaceDescriptor(win._screenName, "modal")
readonly property bool _connectedActive: CompositorService.usesConnectedFrameChromeForScreen(win.targetScreen) readonly property bool _connectedActive: CompositorService.usesConnectedFrameChromeForScreen(win.targetScreen)
readonly property bool _dockHostedHere: {
if (!win._connectedActive || !SettingsData.showDock || SettingsData.dockUseOverlayLayer)
return false;
const screens = SettingsData.getFilteredScreens("dock");
for (let i = 0; i < screens.length; i++) {
if (screens[i] && screens[i].name === win._screenName)
return true;
}
return false;
}
readonly property string _barSide: { readonly property string _barSide: {
const edges = win.barEdges; const edges = win.barEdges;
if (edges.includes("top")) if (edges.includes("top"))
@@ -191,6 +235,33 @@ PanelWindow {
return Math.max(0, Math.round(Theme.px(value, win._dpr))); return Math.max(0, Math.round(Theme.px(value, win._dpr)));
} }
function _pointInBand(lx, ly, bx, by, bw, bh, pad) {
return lx >= bx - pad && lx < bx + bw + pad && ly >= by - pad && ly < by + bh + pad;
}
function containsGlobalPoint(gx, gy, padding) {
if (!win._connectedActive || !win.contentItem)
return false;
const origin = win.contentItem.mapToItem(null, 0, 0);
if (!origin)
return false;
const pad = padding !== undefined ? padding : 16;
const lx = gx - origin.x;
const ly = gy - origin.y;
const w = win._windowRegionWidth;
const h = win._windowRegionHeight;
const edges = win.barEdges;
if (edges.includes("top") && win._pointInBand(lx, ly, 0, 0, w, win.cutoutTopInset, pad))
return true;
if (edges.includes("bottom") && win._pointInBand(lx, ly, 0, h - win.cutoutBottomInset, w, win.cutoutBottomInset, pad))
return true;
if (edges.includes("left") && win._pointInBand(lx, ly, 0, 0, win.cutoutLeftInset, h, pad))
return true;
if (edges.includes("right") && win._pointInBand(lx, ly, w - win.cutoutRightInset, 0, win.cutoutRightInset, h, pad))
return true;
return false;
}
readonly property int cutoutTopInset: win._regionInt(barEdges.includes("top") ? SettingsData.frameBarSize : SettingsData.frameThickness) readonly property int cutoutTopInset: win._regionInt(barEdges.includes("top") ? SettingsData.frameBarSize : SettingsData.frameThickness)
readonly property int cutoutBottomInset: win._regionInt(barEdges.includes("bottom") ? SettingsData.frameBarSize : SettingsData.frameThickness) readonly property int cutoutBottomInset: win._regionInt(barEdges.includes("bottom") ? SettingsData.frameBarSize : SettingsData.frameThickness)
readonly property int cutoutLeftInset: win._regionInt(barEdges.includes("left") ? SettingsData.frameBarSize : SettingsData.frameThickness) readonly property int cutoutLeftInset: win._regionInt(barEdges.includes("left") ? SettingsData.frameBarSize : SettingsData.frameThickness)
@@ -1049,10 +1120,12 @@ PanelWindow {
} }
Component.onCompleted: { Component.onCompleted: {
KeyboardFocus.registerBarWindow(win);
win._scheduleBlurRebuild(); win._scheduleBlurRebuild();
win._scheduleSurfaceRefresh(); win._scheduleSurfaceRefresh();
} }
Component.onDestruction: { Component.onDestruction: {
KeyboardFocus.unregisterBarWindow(win);
blurRebuildAction.cancel(); blurRebuildAction.cancel();
surfaceRefreshAction.cancel(); surfaceRefreshAction.cancel();
win._teardownBlur(); win._teardownBlur();
@@ -1101,4 +1174,25 @@ PanelWindow {
property vector4d chromeK3: win._sdfSlots[3].k property vector4d chromeK3: win._sdfSlots[3].k
property vector4d chromeParam3: win._sdfSlots[3].param property vector4d chromeParam3: win._sdfSlots[3].param
} }
Loader {
anchors.fill: parent
z: 1
active: win._connectedActive
sourceComponent: FrameBarHost {
frameWindow: win
targetScreen: win.targetScreen
}
}
Loader {
id: frameDockHostLoader
anchors.fill: parent
z: 1
active: win._dockHostedHere
sourceComponent: FrameDockHost {
frameWindow: win
targetScreen: win.targetScreen
}
}
} }
+17 -1
View File
@@ -36,6 +36,7 @@ Item {
property int hyprlandLayoutCount: 0 property int hyprlandLayoutCount: 0
property bool lockerReadySent: false property bool lockerReadySent: false
property bool lockerReadyArmed: false property bool lockerReadyArmed: false
property var sessionLock: null
readonly property bool hasCustomWallpaper: SettingsData.lockScreenWallpaperPath !== "" readonly property bool hasCustomWallpaper: SettingsData.lockScreenWallpaperPath !== ""
readonly property string lockFontFamily: SettingsData.lockScreenFontFamily readonly property string lockFontFamily: SettingsData.lockScreenFontFamily
@@ -134,12 +135,27 @@ Item {
return; return;
if (!root.visible || root.opacity <= 0) if (!root.visible || root.opacity <= 0)
return; return;
// Don't report ready until the compositor has confirmed the session is
// locked (ext-session-lock `locked` event). Qt's afterRendering fires
// before the lock surface is committed/presented, so releasing the sleep
// inhibitor on it lets the machine freeze with the desktop still on screen,
// which then flashes on resume. secure=true guarantees the desktop is hidden.
if (root.sessionLock && !root.sessionLock.secure)
return;
Qt.callLater(() => { Qt.callLater(() => {
if (root.visible && root.opacity > 0 && !root.unlocking) if (root.visible && root.opacity > 0 && !root.unlocking)
sendLockerReadyOnce(); sendLockerReadyOnce();
}); });
} }
Connections {
target: root.sessionLock
enabled: target !== null
function onSecureChanged() {
root.maybeSend();
}
}
Connections { Connections {
target: root.Window.window target: root.Window.window
enabled: target !== null enabled: target !== null
@@ -779,7 +795,7 @@ Item {
return "passkey"; return "passkey";
if (pam.fprint.tries >= SettingsData.maxFprintTries) if (pam.fprint.tries >= SettingsData.maxFprintTries)
return "fingerprint_off"; return "fingerprint_off";
if (pam.fprint.active) if (pam.fprint.active || pam.fprint.retrying)
return "fingerprint"; return "fingerprint";
if (pam.u2f.active) if (pam.u2f.active)
return "passkey"; return "passkey";
+1
View File
@@ -33,6 +33,7 @@ FocusScope {
anchors.fill: parent anchors.fill: parent
demoMode: false demoMode: false
sessionLock: root.lock
pam: root.pam pam: root.pam
passwordBuffer: root.sharedPasswordBuffer passwordBuffer: root.sharedPasswordBuffer
screenName: root.screenName screenName: root.screenName
+8 -1
View File
@@ -262,9 +262,11 @@ Scope {
property bool available: SettingsData.lockFingerprintReady property bool available: SettingsData.lockFingerprintReady
property int tries property int tries
property int errorTries property int errorTries
property bool retrying: false
function checkAvail(): void { function checkAvail(): void {
if (!available || !SettingsData.enableFprint || !root.lockSecured || root.fprintSuppressedByPrimaryPam) { if (!available || !SettingsData.enableFprint || !root.lockSecured || root.fprintSuppressedByPrimaryPam) {
retrying = false;
abort(); abort();
return; return;
} }
@@ -273,6 +275,7 @@ Scope {
tries = 0; tries = 0;
errorTries = 0; errorTries = 0;
retrying = false;
start(); start();
} }
@@ -285,6 +288,7 @@ Scope {
switch (res) { switch (res) {
case PamResult.Success: case PamResult.Success:
retrying = false;
if (!root.unlockInProgress) { if (!root.unlockInProgress) {
passwd.abort(); passwd.abort();
root.proceedAfterPrimaryAuth(); root.proceedAfterPrimaryAuth();
@@ -293,13 +297,16 @@ Scope {
case PamResult.Error: case PamResult.Error:
errorTries++; errorTries++;
if (errorTries < 200) { if (errorTries < 200) {
retrying = true;
abort(); abort();
errorRetry.restart(); errorRetry.restart();
return; return;
} }
retrying = false;
abort(); abort();
return; return;
case PamResult.MaxTries: case PamResult.MaxTries:
retrying = false;
tries++; tries++;
if (tries < SettingsData.maxFprintTries) { if (tries < SettingsData.maxFprintTries) {
root.fprintState = "fail"; root.fprintState = "fail";
@@ -418,7 +425,7 @@ Scope {
Timer { Timer {
id: errorRetry id: errorRetry
interval: 1500 interval: Math.min(1500 * Math.pow(2, Math.max(0, fprint.errorTries - 1)), 30000)
onTriggered: fprint.start() onTriggered: fprint.start()
} }
@@ -61,6 +61,11 @@ DankPopout {
notificationHistoryVisible = !notificationHistoryVisible; notificationHistoryVisible = !notificationHistoryVisible;
} }
// Re-open without toggling the flag (used when retargeting to another monitor).
function present() {
openSized();
}
function openSized() { function openSized() {
if (!notificationHistoryVisible) if (!notificationHistoryVisible)
return; return;
@@ -1256,6 +1256,8 @@ Item {
} }
SettingsToggleCard { SettingsToggleCard {
settingKey: "hoverPopouts"
tags: ["bar", "hover", "popout", "reveal", "widget", "delay"]
iconName: "touch_app" iconName: "touch_app"
title: I18n.tr("Hover Popouts") title: I18n.tr("Hover Popouts")
description: I18n.tr("Open widget popouts by hovering over the bar. Moving to another widget switches the popout.") description: I18n.tr("Open widget popouts by hovering over the bar. Moving to another widget switches the popout.")
@@ -210,7 +210,7 @@ Item {
} }
function warnIfMissingQtTheme() { function warnIfMissingQtTheme() {
if (Quickshell.env("QT_QPA_PLATFORMTHEME") === "gtk3" || Quickshell.env("QT_QPA_PLATFORMTHEME") === "qt6ct" || Quickshell.env("QT_QPA_PLATFORMTHEME_QT6") === "qt6ct") if (Quickshell.env("QT_QPA_PLATFORMTHEME") === "gtk3" || Quickshell.env("QT_QPA_PLATFORMTHEME") === "qt6ct" || Quickshell.env("QT_QPA_PLATFORMTHEME_QT6") === "qt6ct" || Quickshell.env("QT_QPA_PLATFORMTHEME") === "kde")
return; return;
ToastService.showError(I18n.tr("Missing Environment Variables", "qt theme env error title"), I18n.tr("You need to set either:\nQT_QPA_PLATFORMTHEME=gtk3 OR\nQT_QPA_PLATFORMTHEME=qt6ct\nas environment variables, and then restart the shell.\n\nqt6ct requires qt6ct-kde to be installed.", "qt theme env error body")); ToastService.showError(I18n.tr("Missing Environment Variables", "qt theme env error title"), I18n.tr("You need to set either:\nQT_QPA_PLATFORMTHEME=gtk3 OR\nQT_QPA_PLATFORMTHEME=qt6ct\nas environment variables, and then restart the shell.\n\nqt6ct requires qt6ct-kde to be installed.", "qt theme env error body"));
} }
+59 -11
View File
@@ -12,6 +12,50 @@ Singleton {
property var widgetRegistry: ({}) property var widgetRegistry: ({})
property var dankBarRepeater: null property var dankBarRepeater: null
// Bars rendered inside the frame surface (connected mode) have no DankBarWindow in the
// repeater, so they self-register here (screenName -> DankBarBody) for trigger routing.
property var frameHostedBars: ({})
// Shared dock context-menu windows (created in DMSShell) so a frame-hosted dock can reach them.
property var dockContextMenu: null
property var dockTrashContextMenu: null
function registerFrameBar(screenName, body) {
if (!screenName || !body)
return;
const next = Object.assign({}, frameHostedBars);
next[screenName] = body;
frameHostedBars = next;
}
function unregisterFrameBar(screenName, body) {
if (!screenName || frameHostedBars[screenName] !== body)
return;
const next = Object.assign({}, frameHostedBars);
delete next[screenName];
frameHostedBars = next;
}
// DankBar items self-register here (barConfig id -> DankBar) so frame-hosted bars can
// resolve their models/rootWindow reactively, independent of repeater load ordering.
property var dankBarItems: ({})
function registerDankBarItem(barId, item) {
if (!barId || !item)
return;
const next = Object.assign({}, dankBarItems);
next[barId] = item;
dankBarItems = next;
}
function unregisterDankBarItem(barId, item) {
if (!barId || dankBarItems[barId] !== item)
return;
const next = Object.assign({}, dankBarItems);
delete next[barId];
dankBarItems = next;
}
signal widgetRegistered(string widgetId, string screenName) signal widgetRegistered(string widgetId, string screenName)
signal widgetUnregistered(string widgetId, string screenName) signal widgetUnregistered(string widgetId, string screenName)
@@ -141,6 +185,9 @@ Singleton {
} }
function getBarWindowForScreen(screenName) { function getBarWindowForScreen(screenName) {
if (frameHostedBars[screenName])
return frameHostedBars[screenName];
if (!dankBarRepeater) if (!dankBarRepeater)
return null; return null;
@@ -170,17 +217,18 @@ Singleton {
} }
function getFirstBarWindow() { function getFirstBarWindow() {
if (!dankBarRepeater || dankBarRepeater.count === 0) if (dankBarRepeater) {
return null; for (var i = 0; i < dankBarRepeater.count; i++) {
const barItem = dankBarRepeater.itemAt(i)?.item;
if (barItem?.barVariants?.instances?.length > 0)
return barItem.barVariants.instances[0];
}
}
const loader = dankBarRepeater.itemAt(0); for (const screenName in frameHostedBars) {
if (!loader?.item) if (frameHostedBars[screenName])
return null; return frameHostedBars[screenName];
}
const barItem = loader.item; return null;
if (!barItem.barVariants?.instances || barItem.barVariants.instances.length === 0)
return null;
return barItem.barVariants.instances[0];
} }
} }
+19 -45
View File
@@ -533,43 +533,6 @@ Singleton {
return false; return false;
} }
function hasFullscreenToplevelOnScreen(screenOrName) {
const screenName = _screenName(screenOrName);
if (!screenName)
return false;
if (isNiri) {
const active = ToplevelManager.activeToplevel;
if (active?.fullscreen && active?.activated && _toplevelOnScreen(active, screenName))
return true;
const filtered = filterCurrentWorkspace(sortedToplevels, screenName);
for (let i = 0; i < filtered.length; i++) {
if (filtered[i]?.fullscreen)
return true;
}
return false;
}
if (isHyprland) {
const filtered = filterCurrentWorkspace(sortedToplevels, screenName);
for (let i = 0; i < filtered.length; i++) {
if (filtered[i]?.fullscreen)
return true;
}
return false;
}
if (!ToplevelManager.toplevels?.values)
return false;
for (const toplevel of ToplevelManager.toplevels.values) {
if (toplevel?.fullscreen && _toplevelOnScreen(toplevel, screenName))
return true;
}
return false;
}
function _hyprlandToplevelMapped(hyprToplevel) { function _hyprlandToplevelMapped(hyprToplevel) {
if (!hyprToplevel) if (!hyprToplevel)
return false; return false;
@@ -630,12 +593,6 @@ Singleton {
// Per-screen cache for connectedFrameBlockedOnScreen to avoid recomputing on every consumer binding. // Per-screen cache for connectedFrameBlockedOnScreen to avoid recomputing on every consumer binding.
property var frameBlockedByScreen: ({}) property var frameBlockedByScreen: ({})
function _computeConnectedFrameBlocked(screenName) {
if (hasFullscreenToplevelOnScreen(screenName))
return true;
return hyprlandSpecialWorkspaceBlocksConnectedFrame(screenName);
}
function _recomputeFrameBlocked() { function _recomputeFrameBlocked() {
const screens = Quickshell.screens || []; const screens = Quickshell.screens || [];
const next = {}; const next = {};
@@ -644,7 +601,7 @@ Singleton {
const name = screens[i]?.name; const name = screens[i]?.name;
if (!name) if (!name)
continue; continue;
const blocked = _computeConnectedFrameBlocked(name); const blocked = hyprlandSpecialWorkspaceBlocksConnectedFrame(name);
next[name] = blocked; next[name] = blocked;
if (frameBlockedByScreen[name] !== blocked) if (frameBlockedByScreen[name] !== blocked)
changed = true; changed = true;
@@ -668,7 +625,7 @@ Singleton {
const cached = frameBlockedByScreen[screenName]; const cached = frameBlockedByScreen[screenName];
if (cached !== undefined) if (cached !== undefined)
return cached; return cached;
return _computeConnectedFrameBlocked(screenName); return hyprlandSpecialWorkspaceBlocksConnectedFrame(screenName);
} }
Connections { Connections {
@@ -730,6 +687,23 @@ Singleton {
return FrameTransitionState.effectiveConnectedFrameModeActive && frameWindowVisibleForScreen(screenOrName); return FrameTransitionState.effectiveConnectedFrameModeActive && frameWindowVisibleForScreen(screenOrName);
} }
// Connected mode renders the bar inside the frame surface. True whenever connected
// chrome is configured for the screen, independent of the fullscreen block, so the
// standalone bar surface stays suppressed while the frame-hosted bar hides with the frame.
function frameHostsSurfacesForScreen(screenOrName) {
return FrameTransitionState.effectiveConnectedFrameModeActive && frameConfiguredForScreen(screenOrName);
}
// A surface set to the overlay layer cannot live inside the frame's single top-layer window,
// so it stays a standalone window (which resolves itself onto the overlay layer) even in connected mode.
function frameHostsBarForConfig(screenOrName, barConfig) {
return frameHostsSurfacesForScreen(screenOrName) && !(barConfig?.useOverlayLayer ?? false);
}
function frameHostsDockForScreen(screenOrName) {
return frameHostsSurfacesForScreen(screenOrName) && !SettingsData.dockUseOverlayLayer;
}
function framePeerSurfacesUseOverlayForScreen(screenOrName) { function framePeerSurfacesUseOverlayForScreen(screenOrName) {
return frameWindowVisibleForScreen(screenOrName); return frameWindowVisibleForScreen(screenOrName);
} }
+1 -1
View File
@@ -69,7 +69,7 @@ Singleton {
property bool screensaverInhibited: false property bool screensaverInhibited: false
property var screensaverInhibitors: [] property var screensaverInhibitors: []
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "theme.auto", "wallpaper", "bluetooth", "bluetooth.pairing", "brightness", "wlroutput", "evdev", "browser", "dbus", "clipboard", "location", "sysupdate"] property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "theme.auto", "wallpaper", "bluetooth", "bluetooth.pairing", "brightness", "wlroutput", "evdev", "browser", "dbus", "clipboard", "sysupdate"]
Component.onCompleted: { Component.onCompleted: {
if (socketPath && socketPath.length > 0) { if (socketPath && socketPath.length > 0) {
+39 -7
View File
@@ -3,10 +3,13 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import qs.Common
import qs.Services
Singleton { Singleton {
id: root id: root
readonly property bool wantsLocation: SettingsData.weatherEnabled && SettingsData.useAutoLocation
readonly property bool locationAvailable: DMSService.isConnected && DMSService.capabilities.includes("location") readonly property bool locationAvailable: DMSService.isConnected && DMSService.capabilities.includes("location")
readonly property bool valid: latitude !== 0 || longitude !== 0 readonly property bool valid: latitude !== 0 || longitude !== 0
@@ -15,19 +18,46 @@ Singleton {
signal locationChanged(var data) signal locationChanged(var data)
onLocationAvailableChanged: { onWantsLocationChanged: {
if (locationAvailable && !valid) if (wantsLocation) {
getState(); ensureSubscription();
} else if (DMSService.activeSubscriptions.includes("location")) {
DMSService.removeSubscription("location");
}
} }
onLocationAvailableChanged: ensureSubscription()
Component.onCompleted: ensureSubscription()
Connections { Connections {
target: DMSService target: DMSService
function onLocationStateUpdate(data) { function onConnectionStateChanged() {
if (!locationAvailable) if (DMSService.isConnected)
return; root.ensureSubscription();
handleStateUpdate(data);
} }
function onLocationStateUpdate(data) {
if (!root.wantsLocation)
return;
root.handleStateUpdate(data);
}
}
function ensureSubscription() {
if (!wantsLocation)
return;
if (!locationAvailable)
return;
if (DMSService.activeSubscriptions.includes("location"))
return;
if (DMSService.activeSubscriptions.includes("all"))
return;
DMSService.addSubscription("location");
if (!valid)
getState();
} }
function handleStateUpdate(data) { function handleStateUpdate(data) {
@@ -42,6 +72,8 @@ Singleton {
} }
function getState() { function getState() {
if (!wantsLocation)
return;
if (!locationAvailable) if (!locationAvailable)
return; return;
+18 -12
View File
@@ -79,6 +79,9 @@ Item {
"rightBar": 0 "rightBar": 0
}) })
property var screen: null property var screen: null
// Output the surface binds to; retargeted only in open() while hidden. A live
// screen change on a mapped surface loses the popup blur on niri.
property var surfaceScreen: null
readonly property bool frameGapStandaloneActive: CompositorService.frameConfiguredForScreen(screen) && !CompositorService.usesConnectedFrameChromeForScreen(screen) readonly property bool frameGapStandaloneActive: CompositorService.frameConfiguredForScreen(screen) && !CompositorService.usesConnectedFrameChromeForScreen(screen)
readonly property bool fluidStandaloneActive: Theme.isDirectionalEffect readonly property bool fluidStandaloneActive: Theme.isDirectionalEffect
readonly property bool backgroundDismissWindowRequired: backgroundInteractive readonly property bool backgroundDismissWindowRequired: backgroundInteractive
@@ -133,8 +136,6 @@ Item {
signal popoutClosed signal popoutClosed
signal backgroundClicked signal backgroundClicked
property var _lastOpenedScreen: null
property int effectiveBarPosition: 0 property int effectiveBarPosition: 0
property real effectiveBarBottomGap: 0 property real effectiveBarBottomGap: 0
readonly property string autoBarShadowDirection: { readonly property string autoBarShadowDirection: {
@@ -214,6 +215,9 @@ Item {
// keep the bg window in active-update mode. // keep the bg window in active-update mode.
property bool _bgCommitWindow: false property bool _bgCommitWindow: false
// True only while the overlay is actually on screen, not merely declared.
readonly property bool _overlayActive: overlayLoader.item?.visible ?? false
Timer { Timer {
id: bgCommitSettleTimer id: bgCommitSettleTimer
interval: 250 interval: 250
@@ -235,6 +239,9 @@ Item {
return; return;
_blurCommitSuppress = true; _blurCommitSuppress = true;
blurCommitPulseTimer.restart(); blurCommitPulseTimer.restart();
// Land the off->on toggle on a commit even after _overlayActive has dropped.
_bgCommitWindow = true;
bgCommitSettleTimer.restart();
} }
Connections { Connections {
@@ -338,14 +345,14 @@ Item {
_frozenMaskWidth = maskWidth; _frozenMaskWidth = maskWidth;
_frozenMaskHeight = maskHeight; _frozenMaskHeight = maskHeight;
const screenChanged = _lastOpenedScreen !== null && _lastOpenedScreen !== screen; const screenChanged = surfaceScreen !== null && surfaceScreen !== screen;
if (screenChanged) { if (screenChanged) {
// Hide on this tick so Qt actually tears down the wl_surface; the show // Unmap before retargeting so the surface is destroyed and recreated
// gets deferred below so the unmap is processed before the remap. // on the new output rather than live-migrated.
contentWindow.visible = false; contentWindow.visible = false;
backgroundWindow.visible = false; backgroundWindow.visible = false;
} }
_lastOpenedScreen = screen; surfaceScreen = screen;
if (contentContainer && !shouldBeVisible) { if (contentContainer && !shouldBeVisible) {
// Snap morph closed only on a fresh open; on screen-change re-open we stay at 1 // Snap morph closed only on a fresh open; on screen-change re-open we stay at 1
@@ -559,13 +566,12 @@ Item {
PanelWindow { PanelWindow {
id: backgroundWindow id: backgroundWindow
screen: root.screen screen: root.surfaceScreen
visible: false visible: false
color: "transparent" color: "transparent"
// Skip buffer updates when there's nothing to render. Briefly flipped // Idle full-screen surface skips buffer updates, else it damages the whole screen
// true via _bgCommitWindow when _surfaceBodyW/H changes so the // every frame. Live while an overlay shows or _bgCommitWindow flushes a mask change.
// contentHoleRect mask carve-out actually commits to the compositor. updatesEnabled: !root._blurCommitSuppress && (root._overlayActive || root._bgCommitWindow)
updatesEnabled: !root._blurCommitSuppress && (root.overlayContent !== null || root._bgCommitWindow)
WlrLayershell.namespace: root.layerNamespace + ":background" WlrLayershell.namespace: root.layerNamespace + ":background"
WlrLayershell.layer: root.effectivePopoutLayer WlrLayershell.layer: root.effectivePopoutLayer
@@ -625,7 +631,7 @@ Item {
PanelWindow { PanelWindow {
id: contentWindow id: contentWindow
screen: root.screen screen: root.surfaceScreen
visible: false visible: false
color: "transparent" color: "transparent"
readonly property bool closeVisualActive: root.shouldBeVisible || root.isClosing readonly property bool closeVisualActive: root.shouldBeVisible || root.isClosing
@@ -772,6 +772,34 @@
], ],
"icon": "display_settings" "icon": "display_settings"
}, },
{
"section": "hoverPopouts",
"label": "Hover Popouts",
"tabIndex": 3,
"category": "Dank Bar",
"keywords": [
"another",
"bar",
"dank",
"delay",
"hover",
"hovering",
"moving",
"open",
"over",
"panel",
"popout",
"popouts",
"reveal",
"statusbar",
"switches",
"taskbar",
"topbar",
"widget"
],
"icon": "touch_app",
"description": "Open widget popouts by hovering over the bar. Moving to another widget switches the popout."
},
{ {
"section": "barPosition", "section": "barPosition",
"label": "Position", "label": "Position",