1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-02 11:38: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
35 changed files with 2728 additions and 2148 deletions
+6
View File
@@ -14,6 +14,12 @@ git clone --recurse-submodules https://github.com/AvengeMedia/DankMaterialShell.
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:
```bash
+1 -1
View File
@@ -69,7 +69,7 @@ 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/aymanbagabas/go-osc52/v2 v2.0.1 // 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/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-20260718184203-ed59ffc6e599/go.mod h1:7p7cfydr4WM1G6eOPFlANXF3IV5du3FoA4CbDPprHAo=
github.com/AvengeMedia/dankgo v0.0.0-20260721162324-38d48943054d h1:EZN2x2uAX975Uvw4MByqWaaVTp2fOJrNTl4V4EQbSn8=
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/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
+1 -1
View File
@@ -16,7 +16,7 @@ revision=1
create_wrksrc=yes
build_style=go
build_wrksrc="core"
hostmakedepends="make"
hostmakedepends="make tar"
go_import_path="github.com/AvengeMedia/DankMaterialShell/core"
go_package="${go_import_path}/cmd/dms"
go_build_tags="distro_binary withshell"
+1 -1
View File
@@ -111,7 +111,7 @@
inherit version;
pname = "dms-shell";
src = ./core;
vendorHash = "sha256-//ya3Yb25/kEBeT6M4bO8Egi+Tb7iiUwevXlzTyIgqA=";
vendorHash = "sha256-G0KCUaNwjWfLcSnb68TMXjpB2xxwheXgrBkFEzcSZTc=";
subPackages = [ "cmd/dms" ];
+21 -2
View File
@@ -65,16 +65,35 @@ Singleton {
function _openPopout(popout) {
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;
flagStayedTrue = false;
}
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;
}
if (popout.notificationHistoryVisible !== undefined) {
const flagStayedTrue = popout.notificationHistoryVisible === true;
popout.notificationHistoryVisible = true;
if (flagStayedTrue)
_ensureSurfaceOpen(popout);
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) {
+29
View File
@@ -2735,6 +2735,35 @@ Singleton {
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) {
if (!frameEnabled || !screen)
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 {
target: SettingsData
function onDockShowTrashChanged() {
@@ -77,6 +77,12 @@ DankPopout {
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) {
StateUtils.openWithSection(root, section);
}
+14 -7
View File
@@ -13,6 +13,9 @@ Item {
signal colorPickerRequested
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 var hyprlandOverviewLoader: null
property bool systemTrayMenuOpen: false
@@ -161,14 +164,18 @@ Item {
id: barVariants
model: {
const prefs = root.barConfig?.screenPreferences || ["all"];
if (prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all")) {
return Quickshell.screens;
const wantsAll = prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all");
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));
if (filtered.length === 0 && root.barConfig?.showOnLastDisplay && Quickshell.screens.length === 1) {
return Quickshell.screens;
}
return filtered;
// Connected frame mode renders the bar inside the frame surface; skip the standalone window there
// unless this bar wants the overlay layer, which the frame surface cannot provide.
return base.filter(screen => !CompositorService.frameHostsBarForConfig(screen, root.barConfig));
}
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 _barInsetPaddingAuto: _barIsVertical ? Theme.spacingXS : _edgeBaseMargin
readonly property real _barInsetPadding: _barInsetPaddingRaw < 0 ? _barInsetPaddingAuto : _barInsetPaddingRaw
// Connected-frame Bar Inset Padding: absolute free-end inset (auto < 0 = frameThickness, 0 = edge-to-edge).
// FrameExclusions already moves a free bar end inward by frameThickness so use frame inset to mangage the gap
// Connected-frame Bar Inset Padding: absolute free-end gap the hosted bar spans full-width into
// (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 _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: {
if (_barIsVertical)
return _edgeBaseMargin;
if (_usesFrameBarChrome)
return hasAdjacentLeftBarLive ? (_edgeBaseMargin + SettingsData.frameBarSize + _frameInsetExtra) : _frameInsetExtra;
return hasAdjacentLeftBarLive ? _cornerAlignInset : _frameInsetResolved;
return Math.max(0, _barInsetPadding);
}
readonly property real _rightMargin: {
if (_barIsVertical)
return _edgeBaseMargin;
if (_usesFrameBarChrome)
return hasAdjacentRightBarLive ? (_edgeBaseMargin + SettingsData.frameBarSize + _frameInsetExtra) : _frameInsetExtra;
return hasAdjacentRightBarLive ? _cornerAlignInset : _frameInsetResolved;
return Math.max(0, _barInsetPadding);
}
readonly property real _topMargin: {
if (!_barIsVertical)
return 0;
if (_usesFrameBarChrome)
return hasAdjacentTopBarLive ? (outlineThickness + SettingsData.frameThickness + _frameInsetExtra) : _frameInsetExtra;
return hasAdjacentTopBarLive ? (_edgeBaseMargin + SettingsData.frameBarSize + _frameInsetExtra) : _frameInsetResolved;
return Math.max(0, _barInsetPadding);
}
readonly property real _bottomMargin: {
if (!_barIsVertical)
return 0;
if (_usesFrameBarChrome)
return hasAdjacentBottomBarLive ? (outlineThickness + SettingsData.frameThickness + _frameInsetExtra) : _frameInsetExtra;
return hasAdjacentBottomBarLive ? (_edgeBaseMargin + SettingsData.frameBarSize + _frameInsetExtra) : _frameInsetResolved;
return Math.max(0, _barInsetPadding);
}
@@ -677,6 +684,7 @@ Item {
LeftSection {
id: hLeftSection
objectName: "leftSection"
edgeIsScreenEdge: !topBarContent.hasAdjacentLeftBarLive
overrideAxisLayout: true
forceVerticalLayout: false
anchors {
@@ -710,6 +718,7 @@ Item {
RightSection {
id: hRightSection
objectName: "rightSection"
edgeIsScreenEdge: !topBarContent.hasAdjacentRightBarLive
overrideAxisLayout: true
forceVerticalLayout: false
anchors {
@@ -782,6 +791,7 @@ Item {
LeftSection {
id: vLeftSection
objectName: "leftSection"
edgeIsScreenEdge: !topBarContent.hasAdjacentTopBarLive
overrideAxisLayout: true
forceVerticalLayout: true
width: parent.width
@@ -850,6 +860,7 @@ Item {
RightSection {
id: vRightSection
objectName: "rightSection"
edgeIsScreenEdge: !topBarContent.hasAdjacentBottomBarLive
overrideAxisLayout: true
forceVerticalLayout: true
width: parent.width
@@ -930,7 +930,8 @@ Item {
if (_barHovered)
return;
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;
_barExitPending = false;
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 bool overrideAxisLayout: 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)
property alias widgetLayoutLoader: layoutLoader
@@ -68,7 +71,7 @@ Item {
isFirst: index === 0
isLast: index === rowRepeater.count - 1
sectionSpacing: parent.rowSpacing
isLeftBarEdge: true
isLeftBarEdge: root.edgeIsScreenEdge
isRightBarEdge: false
}
}
@@ -115,7 +118,7 @@ Item {
isFirst: index === 0
isLast: index === columnRepeater.count - 1
sectionSpacing: parent.columnSpacing
isTopBarEdge: true
isTopBarEdge: root.edgeIsScreenEdge
isBottomBarEdge: false
}
}
+5 -2
View File
@@ -17,6 +17,9 @@ Item {
property real sectionAvailablePrimarySize: 0
property bool overrideAxisLayout: 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)
property alias widgetLayoutLoader: layoutLoader
@@ -71,7 +74,7 @@ Item {
isLast: index === rowRepeater.count - 1
sectionSpacing: parent.rowSpacing
isLeftBarEdge: false
isRightBarEdge: true
isRightBarEdge: root.edgeIsScreenEdge
}
}
}
@@ -118,7 +121,7 @@ Item {
isLast: index === columnRepeater.count - 1
sectionSpacing: parent.columnSpacing
isTopBarEdge: false
isBottomBarEdge: true
isBottomBarEdge: root.edgeIsScreenEdge
}
}
}
@@ -63,8 +63,8 @@ BasePill {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
onPressed: mouse => root.triggerRipple(this, mouse.x, mouse.y)
onClicked: function (mouse) {
onPressed: function (mouse) {
root.triggerRipple(this, mouse.x, mouse.y);
switch (mouse.button) {
case Qt.RightButton:
openContextMenu();
+43 -834
View File
@@ -1,8 +1,6 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Shapes
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import qs.Common
import qs.Services
@@ -10,7 +8,8 @@ import qs.Widgets
Variants {
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 trashContextMenu
@@ -18,864 +17,74 @@ Variants {
delegate: PanelWindow {
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 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
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: {
if (CompositorService.isNiri && NiriService.inOverview) {
return SettingsData.dockOpenOnOverview;
}
return SettingsData.showDock;
}
color: "transparent"
readonly property real dockReserveZone: dockGeometry.reserveZone
readonly property bool shouldReserveDockSpace: dockGeometry.shouldReserveSpace
implicitWidth: body.surfaceImplicitWidth
implicitHeight: body.surfaceImplicitHeight
exclusiveZone: body.surfaceExclusiveZone
exclusiveZone: {
if (!dock.shouldReserveDockSpace)
return -1;
if (dock.frameDockExclusionActive)
return -1;
return dock.dockReserveZone;
}
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)
WindowBlur {
targetWindow: dock
blurEnabled: body.effectiveBlurEnabled && !body.usesConnectedFrameChrome
blurX: body.blurX
blurY: body.blurY
blurWidth: body.blurWidth
blurHeight: body.blurHeight
blurRadius: body.blurRadius
}
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 {
id: dockExclusion
screen: dock.screen || dock.modelData
visible: dock.frameDockExclusionActive && dock.shouldReserveDockSpace
visible: body.frameDockExclusionActive && body.shouldReserveDockSpace
color: "transparent"
mask: Region {}
implicitWidth: dock.isVertical ? dock.dockReserveZone : 1
implicitHeight: dock.isVertical ? 1 : dock.dockReserveZone
exclusiveZone: visible ? dock.dockReserveZone : -1
implicitWidth: body.isVertical ? body.dockReserveZone : 1
implicitHeight: body.isVertical ? 1 : body.dockReserveZone
exclusiveZone: visible ? body.dockReserveZone : -1
WlrLayershell.namespace: "dms:dock-exclusion"
WlrLayershell.layer: WlrLayer.Top
anchors {
top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top) : true
bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom) : true
left: !dock.isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Left)
right: !dock.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
}
}
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)
}
}
}
+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 actualDockWidth = dockBackground ? dockBackground.width : dockWindow.width;
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 y = 0;
@@ -68,14 +73,14 @@ PanelWindow {
case SettingsData.Position.Left:
{
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;
break;
}
case SettingsData.Position.Right:
{
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;
break;
}
@@ -83,7 +88,7 @@ PanelWindow {
{
const dockLeftMargin = Math.round((screen.width - dockWindow.width) / 2);
x = dockLeftMargin + buttonPosInDock.x + anchorItem.width / 2;
y = actualDockHeight + dockMargin + 20;
y = frameHosted ? (dockBgPos.y + actualDockHeight + dockMargin + 20) : (actualDockHeight + dockMargin + 20);
break;
}
case SettingsData.Position.Bottom:
@@ -91,7 +96,7 @@ PanelWindow {
{
const dockLeftMargin = Math.round((screen.width - dockWindow.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;
}
}
+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);
}
// One thin invisible PanelWindow per edge.
// Skips any edge where a bar already provides its own exclusiveZone.
// Overlay-layer bars stay standalone even in connected mode and reserve their own edge.
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 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 {
active: root.screenEnabled && !root.barEdges.includes("top")
readonly property bool isBarEdge: root.barEdges.includes("top")
active: root.screenEnabled && (!isBarEdge || root.hostsBandForEdge("top"))
sourceComponent: EdgeExclusion {
targetScreen: root.screen
exclusionSize: root.exclusionSizeForEdge("top")
anchorTop: true
anchorLeft: true
anchorRight: true
@@ -32,9 +50,11 @@ Scope {
}
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 {
targetScreen: root.screen
exclusionSize: root.exclusionSizeForEdge("bottom")
anchorBottom: true
anchorLeft: true
anchorRight: true
@@ -42,9 +62,11 @@ Scope {
}
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 {
targetScreen: root.screen
exclusionSize: root.exclusionSizeForEdge("left")
anchorLeft: true
anchorTop: true
anchorBottom: true
@@ -52,9 +74,11 @@ Scope {
}
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 {
targetScreen: root.screen
exclusionSize: root.exclusionSizeForEdge("right")
anchorRight: true
anchorTop: true
anchorBottom: true
@@ -63,6 +87,7 @@ Scope {
component EdgeExclusion: PanelWindow {
required property var targetScreen
property real exclusionSize: SettingsData.frameThickness
screen: targetScreen
property bool anchorTop: false
@@ -72,7 +97,7 @@ Scope {
WlrLayershell.namespace: "dms:frame-exclusion"
WlrLayershell.layer: WlrLayer.Top
exclusiveZone: SettingsData.frameThickness
exclusiveZone: exclusionSize
color: "transparent"
mask: Region {}
implicitWidth: 1
+95 -1
View File
@@ -32,7 +32,41 @@ PanelWindow {
}
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: {
SettingsData.barConfigs;
@@ -51,6 +85,16 @@ PanelWindow {
readonly property var _modalDescriptor: ConnectedModeState.surfaceDescriptor(win._screenName, "modal")
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: {
const edges = win.barEdges;
if (edges.includes("top"))
@@ -191,6 +235,33 @@ PanelWindow {
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 cutoutBottomInset: win._regionInt(barEdges.includes("bottom") ? SettingsData.frameBarSize : SettingsData.frameThickness)
readonly property int cutoutLeftInset: win._regionInt(barEdges.includes("left") ? SettingsData.frameBarSize : SettingsData.frameThickness)
@@ -1049,10 +1120,12 @@ PanelWindow {
}
Component.onCompleted: {
KeyboardFocus.registerBarWindow(win);
win._scheduleBlurRebuild();
win._scheduleSurfaceRefresh();
}
Component.onDestruction: {
KeyboardFocus.unregisterBarWindow(win);
blurRebuildAction.cancel();
surfaceRefreshAction.cancel();
win._teardownBlur();
@@ -1101,4 +1174,25 @@ PanelWindow {
property vector4d chromeK3: win._sdfSlots[3].k
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 bool lockerReadySent: false
property bool lockerReadyArmed: false
property var sessionLock: null
readonly property bool hasCustomWallpaper: SettingsData.lockScreenWallpaperPath !== ""
readonly property string lockFontFamily: SettingsData.lockScreenFontFamily
@@ -134,12 +135,27 @@ Item {
return;
if (!root.visible || root.opacity <= 0)
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(() => {
if (root.visible && root.opacity > 0 && !root.unlocking)
sendLockerReadyOnce();
});
}
Connections {
target: root.sessionLock
enabled: target !== null
function onSecureChanged() {
root.maybeSend();
}
}
Connections {
target: root.Window.window
enabled: target !== null
@@ -779,7 +795,7 @@ Item {
return "passkey";
if (pam.fprint.tries >= SettingsData.maxFprintTries)
return "fingerprint_off";
if (pam.fprint.active)
if (pam.fprint.active || pam.fprint.retrying)
return "fingerprint";
if (pam.u2f.active)
return "passkey";
+1
View File
@@ -33,6 +33,7 @@ FocusScope {
anchors.fill: parent
demoMode: false
sessionLock: root.lock
pam: root.pam
passwordBuffer: root.sharedPasswordBuffer
screenName: root.screenName
+8 -1
View File
@@ -262,9 +262,11 @@ Scope {
property bool available: SettingsData.lockFingerprintReady
property int tries
property int errorTries
property bool retrying: false
function checkAvail(): void {
if (!available || !SettingsData.enableFprint || !root.lockSecured || root.fprintSuppressedByPrimaryPam) {
retrying = false;
abort();
return;
}
@@ -273,6 +275,7 @@ Scope {
tries = 0;
errorTries = 0;
retrying = false;
start();
}
@@ -285,6 +288,7 @@ Scope {
switch (res) {
case PamResult.Success:
retrying = false;
if (!root.unlockInProgress) {
passwd.abort();
root.proceedAfterPrimaryAuth();
@@ -293,13 +297,16 @@ Scope {
case PamResult.Error:
errorTries++;
if (errorTries < 200) {
retrying = true;
abort();
errorRetry.restart();
return;
}
retrying = false;
abort();
return;
case PamResult.MaxTries:
retrying = false;
tries++;
if (tries < SettingsData.maxFprintTries) {
root.fprintState = "fail";
@@ -418,7 +425,7 @@ Scope {
Timer {
id: errorRetry
interval: 1500
interval: Math.min(1500 * Math.pow(2, Math.max(0, fprint.errorTries - 1)), 30000)
onTriggered: fprint.start()
}
@@ -61,6 +61,11 @@ DankPopout {
notificationHistoryVisible = !notificationHistoryVisible;
}
// Re-open without toggling the flag (used when retargeting to another monitor).
function present() {
openSized();
}
function openSized() {
if (!notificationHistoryVisible)
return;
@@ -1256,6 +1256,8 @@ Item {
}
SettingsToggleCard {
settingKey: "hoverPopouts"
tags: ["bar", "hover", "popout", "reveal", "widget", "delay"]
iconName: "touch_app"
title: I18n.tr("Hover Popouts")
description: I18n.tr("Open widget popouts by hovering over the bar. Moving to another widget switches the popout.")
+59 -11
View File
@@ -12,6 +12,50 @@ Singleton {
property var widgetRegistry: ({})
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 widgetUnregistered(string widgetId, string screenName)
@@ -141,6 +185,9 @@ Singleton {
}
function getBarWindowForScreen(screenName) {
if (frameHostedBars[screenName])
return frameHostedBars[screenName];
if (!dankBarRepeater)
return null;
@@ -170,17 +217,18 @@ Singleton {
}
function getFirstBarWindow() {
if (!dankBarRepeater || dankBarRepeater.count === 0)
return null;
if (dankBarRepeater) {
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);
if (!loader?.item)
return null;
const barItem = loader.item;
if (!barItem.barVariants?.instances || barItem.barVariants.instances.length === 0)
return null;
return barItem.barVariants.instances[0];
for (const screenName in frameHostedBars) {
if (frameHostedBars[screenName])
return frameHostedBars[screenName];
}
return null;
}
}
+19 -45
View File
@@ -533,43 +533,6 @@ Singleton {
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) {
if (!hyprToplevel)
return false;
@@ -630,12 +593,6 @@ Singleton {
// Per-screen cache for connectedFrameBlockedOnScreen to avoid recomputing on every consumer binding.
property var frameBlockedByScreen: ({})
function _computeConnectedFrameBlocked(screenName) {
if (hasFullscreenToplevelOnScreen(screenName))
return true;
return hyprlandSpecialWorkspaceBlocksConnectedFrame(screenName);
}
function _recomputeFrameBlocked() {
const screens = Quickshell.screens || [];
const next = {};
@@ -644,7 +601,7 @@ Singleton {
const name = screens[i]?.name;
if (!name)
continue;
const blocked = _computeConnectedFrameBlocked(name);
const blocked = hyprlandSpecialWorkspaceBlocksConnectedFrame(name);
next[name] = blocked;
if (frameBlockedByScreen[name] !== blocked)
changed = true;
@@ -668,7 +625,7 @@ Singleton {
const cached = frameBlockedByScreen[screenName];
if (cached !== undefined)
return cached;
return _computeConnectedFrameBlocked(screenName);
return hyprlandSpecialWorkspaceBlocksConnectedFrame(screenName);
}
Connections {
@@ -730,6 +687,23 @@ Singleton {
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) {
return frameWindowVisibleForScreen(screenOrName);
}
+1 -1
View File
@@ -69,7 +69,7 @@ Singleton {
property bool screensaverInhibited: false
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: {
if (socketPath && socketPath.length > 0) {
+39 -7
View File
@@ -3,10 +3,13 @@ pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
import qs.Services
Singleton {
id: root
readonly property bool wantsLocation: SettingsData.weatherEnabled && SettingsData.useAutoLocation
readonly property bool locationAvailable: DMSService.isConnected && DMSService.capabilities.includes("location")
readonly property bool valid: latitude !== 0 || longitude !== 0
@@ -15,19 +18,46 @@ Singleton {
signal locationChanged(var data)
onLocationAvailableChanged: {
if (locationAvailable && !valid)
getState();
onWantsLocationChanged: {
if (wantsLocation) {
ensureSubscription();
} else if (DMSService.activeSubscriptions.includes("location")) {
DMSService.removeSubscription("location");
}
}
onLocationAvailableChanged: ensureSubscription()
Component.onCompleted: ensureSubscription()
Connections {
target: DMSService
function onLocationStateUpdate(data) {
if (!locationAvailable)
return;
handleStateUpdate(data);
function onConnectionStateChanged() {
if (DMSService.isConnected)
root.ensureSubscription();
}
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) {
@@ -42,6 +72,8 @@ Singleton {
}
function getState() {
if (!wantsLocation)
return;
if (!locationAvailable)
return;
+18 -12
View File
@@ -79,6 +79,9 @@ Item {
"rightBar": 0
})
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 fluidStandaloneActive: Theme.isDirectionalEffect
readonly property bool backgroundDismissWindowRequired: backgroundInteractive
@@ -133,8 +136,6 @@ Item {
signal popoutClosed
signal backgroundClicked
property var _lastOpenedScreen: null
property int effectiveBarPosition: 0
property real effectiveBarBottomGap: 0
readonly property string autoBarShadowDirection: {
@@ -214,6 +215,9 @@ Item {
// keep the bg window in active-update mode.
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 {
id: bgCommitSettleTimer
interval: 250
@@ -235,6 +239,9 @@ Item {
return;
_blurCommitSuppress = true;
blurCommitPulseTimer.restart();
// Land the off->on toggle on a commit even after _overlayActive has dropped.
_bgCommitWindow = true;
bgCommitSettleTimer.restart();
}
Connections {
@@ -338,14 +345,14 @@ Item {
_frozenMaskWidth = maskWidth;
_frozenMaskHeight = maskHeight;
const screenChanged = _lastOpenedScreen !== null && _lastOpenedScreen !== screen;
const screenChanged = surfaceScreen !== null && surfaceScreen !== screen;
if (screenChanged) {
// Hide on this tick so Qt actually tears down the wl_surface; the show
// gets deferred below so the unmap is processed before the remap.
// Unmap before retargeting so the surface is destroyed and recreated
// on the new output rather than live-migrated.
contentWindow.visible = false;
backgroundWindow.visible = false;
}
_lastOpenedScreen = screen;
surfaceScreen = screen;
if (contentContainer && !shouldBeVisible) {
// Snap morph closed only on a fresh open; on screen-change re-open we stay at 1
@@ -559,13 +566,12 @@ Item {
PanelWindow {
id: backgroundWindow
screen: root.screen
screen: root.surfaceScreen
visible: false
color: "transparent"
// Skip buffer updates when there's nothing to render. Briefly flipped
// true via _bgCommitWindow when _surfaceBodyW/H changes so the
// contentHoleRect mask carve-out actually commits to the compositor.
updatesEnabled: !root._blurCommitSuppress && (root.overlayContent !== null || root._bgCommitWindow)
// Idle full-screen surface skips buffer updates, else it damages the whole screen
// every frame. Live while an overlay shows or _bgCommitWindow flushes a mask change.
updatesEnabled: !root._blurCommitSuppress && (root._overlayActive || root._bgCommitWindow)
WlrLayershell.namespace: root.layerNamespace + ":background"
WlrLayershell.layer: root.effectivePopoutLayer
@@ -625,7 +631,7 @@ Item {
PanelWindow {
id: contentWindow
screen: root.screen
screen: root.surfaceScreen
visible: false
color: "transparent"
readonly property bool closeVisualActive: root.shouldBeVisible || root.isClosing
@@ -772,6 +772,34 @@
],
"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",
"label": "Position",