1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-05 21:15:38 -05:00
Files
DankMaterialShell/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml
2025-12-05 10:37:24 -05:00

1447 lines
62 KiB
QML

import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Hyprland
import Quickshell.Services.SystemTray
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property bool isVertical: axis?.isVertical ?? false
property var axis: null
property var parentWindow: null
property var parentScreen: null
property real widgetThickness: 30
property real barThickness: 48
property real barSpacing: 4
property bool isAtBottom: false
property var barConfig: null
property bool isAutoHideBar: false
readonly property real horizontalPadding: (barConfig?.noBackground ?? false) ? 2 : Theme.spacingS
readonly property var hiddenTrayIds: {
const envValue = Quickshell.env("DMS_HIDE_TRAYIDS") || "";
return envValue ? envValue.split(",").map(id => id.trim().toLowerCase()) : [];
}
readonly property var allTrayItems: {
if (!hiddenTrayIds.length) {
return SystemTray.items.values;
}
return SystemTray.items.values.filter(item => {
const itemId = item?.id || "";
return !hiddenTrayIds.includes(itemId.toLowerCase());
});
}
readonly property var mainBarItems: allTrayItems.filter(item => !SessionData.isHiddenTrayId(item?.id || ""))
readonly property var hiddenBarItems: allTrayItems.filter(item => SessionData.isHiddenTrayId(item?.id || ""))
readonly property bool hasHiddenItems: allTrayItems.length > mainBarItems.length
readonly property int calculatedSize: {
if (allTrayItems.length === 0)
return 0;
const itemCount = mainBarItems.length + (hasHiddenItems ? 1 : 0);
return itemCount * 24 + horizontalPadding * 2;
}
readonly property real visualWidth: isVertical ? widgetThickness : calculatedSize
readonly property real visualHeight: isVertical ? calculatedSize : widgetThickness
width: isVertical ? barThickness : visualWidth
height: isVertical ? visualHeight : barThickness
visible: allTrayItems.length > 0
readonly property real minTooltipY: {
if (!parentScreen || !isVertical) {
return 0;
}
if (isAutoHideBar) {
return 0;
}
if (parentScreen.y > 0) {
const estimatedTopBarHeight = barThickness + barSpacing;
return estimatedTopBarHeight;
}
return 0;
}
property bool menuOpen: false
property var currentTrayMenu: null
Item {
id: visualBackground
width: root.visualWidth
height: root.visualHeight
anchors.centerIn: parent
Rectangle {
id: outline
anchors.centerIn: parent
width: {
const borderWidth = (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0;
return parent.width + borderWidth * 2;
}
height: {
const borderWidth = (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0;
return parent.height + borderWidth * 2;
}
radius: (barConfig?.noBackground ?? false) ? 0 : Theme.cornerRadius
color: "transparent"
border.width: {
if (barConfig?.widgetOutlineEnabled ?? false) {
return barConfig?.widgetOutlineThickness ?? 1;
}
return 0;
}
border.color: {
if (!(barConfig?.widgetOutlineEnabled ?? false)) {
return "transparent";
}
const colorOption = barConfig?.widgetOutlineColor || "primary";
const opacity = barConfig?.widgetOutlineOpacity ?? 1.0;
switch (colorOption) {
case "surfaceText":
return Theme.withAlpha(Theme.surfaceText, opacity);
case "secondary":
return Theme.withAlpha(Theme.secondary, opacity);
case "primary":
return Theme.withAlpha(Theme.primary, opacity);
default:
return Theme.withAlpha(Theme.primary, opacity);
}
}
}
Rectangle {
id: background
anchors.fill: parent
radius: (barConfig?.noBackground ?? false) ? 0 : Theme.cornerRadius
color: {
if (allTrayItems.length === 0) {
return "transparent";
}
if ((barConfig?.noBackground ?? false)) {
return "transparent";
}
const baseColor = Theme.widgetBaseBackgroundColor;
const transparency = (root.barConfig && root.barConfig.widgetTransparency !== undefined) ? root.barConfig.widgetTransparency : 1.0;
if (Theme.widgetBackgroundHasAlpha) {
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * transparency);
}
return Theme.withAlpha(baseColor, transparency);
}
}
}
Loader {
id: layoutLoader
anchors.centerIn: parent
sourceComponent: root.isVertical ? columnComp : rowComp
}
Component {
id: rowComp
Row {
spacing: 0
Repeater {
model: root.mainBarItems
delegate: Item {
id: delegateRoot
property var trayItem: modelData
property string iconSource: {
let icon = trayItem && trayItem.icon;
if (typeof icon === 'string' || icon instanceof String) {
if (icon === "") {
return "";
}
if (icon.includes("?path=")) {
const split = icon.split("?path=");
if (split.length !== 2) {
return icon;
}
const name = split[0];
const path = split[1];
let fileName = name.substring(name.lastIndexOf("/") + 1);
if (fileName.startsWith("dropboxstatus")) {
fileName = `hicolor/16x16/status/${fileName}`;
}
return `file://${path}/${fileName}`;
}
if (icon.startsWith("/") && !icon.startsWith("file://")) {
return `file://${icon}`;
}
return icon;
}
return "";
}
width: 24
height: root.barThickness
Rectangle {
id: visualContent
width: 24
height: 24
anchors.centerIn: parent
radius: Theme.cornerRadius
color: trayItemArea.containsMouse ? Theme.primaryHover : "transparent"
IconImage {
id: iconImg
anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness)
height: Theme.barIconSize(root.barThickness)
source: delegateRoot.iconSource
asynchronous: true
smooth: true
mipmap: true
visible: status === Image.Ready
}
Text {
anchors.centerIn: parent
visible: !iconImg.visible
text: {
const itemId = trayItem?.id || "";
if (!itemId) {
return "?";
}
return itemId.charAt(0).toUpperCase();
}
font.pixelSize: 10
color: Theme.widgetTextColor
}
}
MouseArea {
id: trayItemArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
onClicked: mouse => {
if (!delegateRoot.trayItem)
return;
if (mouse.button === Qt.LeftButton && !delegateRoot.trayItem.onlyMenu) {
delegateRoot.trayItem.activate();
return;
}
if (!delegateRoot.trayItem.hasMenu)
return;
root.menuOpen = false;
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVertical, root.axis);
}
}
}
}
Item {
width: 24
height: root.barThickness
visible: root.hasHiddenItems
Rectangle {
id: caretButton
width: 24
height: 24
anchors.centerIn: parent
radius: Theme.cornerRadius
color: caretArea.containsMouse ? Theme.primaryHover : "transparent"
DankIcon {
anchors.centerIn: parent
name: root.menuOpen ? "expand_less" : "expand_more"
size: Theme.barIconSize(root.barThickness)
color: Theme.widgetTextColor
}
MouseArea {
id: caretArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.menuOpen = !root.menuOpen
}
}
}
}
}
Component {
id: columnComp
Column {
spacing: 0
Repeater {
model: root.mainBarItems
delegate: Item {
id: delegateRoot
property var trayItem: modelData
property string iconSource: {
let icon = trayItem && trayItem.icon;
if (typeof icon === 'string' || icon instanceof String) {
if (icon === "") {
return "";
}
if (icon.includes("?path=")) {
const split = icon.split("?path=");
if (split.length !== 2) {
return icon;
}
const name = split[0];
const path = split[1];
let fileName = name.substring(name.lastIndexOf("/") + 1);
if (fileName.startsWith("dropboxstatus")) {
fileName = `hicolor/16x16/status/${fileName}`;
}
return `file://${path}/${fileName}`;
}
if (icon.startsWith("/") && !icon.startsWith("file://")) {
return `file://${icon}`;
}
return icon;
}
return "";
}
width: root.barThickness
height: 24
Rectangle {
id: visualContent
width: 24
height: 24
anchors.centerIn: parent
radius: Theme.cornerRadius
color: trayItemArea.containsMouse ? Theme.primaryHover : "transparent"
IconImage {
id: iconImg
anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness)
height: Theme.barIconSize(root.barThickness)
source: delegateRoot.iconSource
asynchronous: true
smooth: true
mipmap: true
visible: status === Image.Ready
}
Text {
anchors.centerIn: parent
visible: !iconImg.visible
text: {
const itemId = trayItem?.id || "";
if (!itemId) {
return "?";
}
return itemId.charAt(0).toUpperCase();
}
font.pixelSize: 10
color: Theme.widgetTextColor
}
}
MouseArea {
id: trayItemArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
onClicked: mouse => {
if (!delegateRoot.trayItem)
return;
if (mouse.button === Qt.LeftButton && !delegateRoot.trayItem.onlyMenu) {
delegateRoot.trayItem.activate();
return;
}
if (!delegateRoot.trayItem.hasMenu)
return;
root.menuOpen = false;
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVertical, root.axis);
}
}
}
}
Item {
width: root.barThickness
height: 24
visible: root.hasHiddenItems
Rectangle {
id: caretButtonVert
width: 24
height: 24
anchors.centerIn: parent
radius: Theme.cornerRadius
color: caretAreaVert.containsMouse ? Theme.primaryHover : "transparent"
DankIcon {
anchors.centerIn: parent
name: {
const edge = root.axis?.edge;
if (edge === "left") {
return root.menuOpen ? "chevron_left" : "chevron_right";
} else {
return root.menuOpen ? "chevron_right" : "chevron_left";
}
}
size: Theme.barIconSize(root.barThickness)
color: Theme.widgetTextColor
}
MouseArea {
id: caretAreaVert
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.menuOpen = !root.menuOpen
}
}
}
}
}
PanelWindow {
id: overflowMenu
visible: root.menuOpen
screen: root.parentScreen
WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: {
if (!root.menuOpen)
return WlrKeyboardFocus.None;
if (CompositorService.useHyprlandFocusGrab)
return WlrKeyboardFocus.OnDemand;
return WlrKeyboardFocus.Exclusive;
}
WlrLayershell.namespace: "dms:tray-overflow-menu"
color: "transparent"
HyprlandFocusGrab {
windows: [overflowMenu]
active: CompositorService.useHyprlandFocusGrab && root.menuOpen
}
Connections {
target: PopoutManager
function onPopoutOpening() {
root.menuOpen = false;
}
}
Component.onDestruction: {
if (root.parentScreen) {
TrayMenuManager.unregisterMenu(root.parentScreen.name);
}
}
function close() {
root.menuOpen = false;
}
anchors {
top: true
left: true
right: true
bottom: true
}
readonly property real dpr: (typeof CompositorService !== "undefined" && CompositorService.getScreenScale) ? CompositorService.getScreenScale(overflowMenu.screen) : (screen?.devicePixelRatio || 1)
property point anchorPos: Qt.point(screen.width / 2, screen.height / 2)
property var barBounds: {
if (!overflowMenu.screen || !root.barConfig) {
return {
"x": 0,
"y": 0,
"width": 0,
"height": 0,
"wingSize": 0
};
}
const barPosition = root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1));
return SettingsData.getBarBounds(overflowMenu.screen, root.barThickness + root.barSpacing, barPosition, root.barConfig);
}
property real barX: barBounds.x
property real barY: barBounds.y
property real barWidth: barBounds.width
property real barHeight: barBounds.height
readonly property int barPosition: root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1))
readonly property var adjacentBarInfo: parentScreen ? SettingsData.getAdjacentBarInfo(parentScreen, barPosition, root.barConfig) : ({
"topBar": 0,
"bottomBar": 0,
"leftBar": 0,
"rightBar": 0
})
readonly property real effectiveBarSize: root.barThickness + root.barSpacing
readonly property real maskX: {
const triggeringBarX = (barPosition === 2) ? effectiveBarSize : 0;
const adjacentLeftBar = adjacentBarInfo?.leftBar ?? 0;
return Math.max(triggeringBarX, adjacentLeftBar);
}
readonly property real maskY: {
const triggeringBarY = (barPosition === 0) ? effectiveBarSize : 0;
const adjacentTopBar = adjacentBarInfo?.topBar ?? 0;
return Math.max(triggeringBarY, adjacentTopBar);
}
readonly property real maskWidth: {
const triggeringBarRight = (barPosition === 3) ? effectiveBarSize : 0;
const adjacentRightBar = adjacentBarInfo?.rightBar ?? 0;
const rightExclusion = Math.max(triggeringBarRight, adjacentRightBar);
return Math.max(100, width - maskX - rightExclusion);
}
readonly property real maskHeight: {
const triggeringBarBottom = (barPosition === 1) ? effectiveBarSize : 0;
const adjacentBottomBar = adjacentBarInfo?.bottomBar ?? 0;
const bottomExclusion = Math.max(triggeringBarBottom, adjacentBottomBar);
return Math.max(100, height - maskY - bottomExclusion);
}
mask: Region {
item: Rectangle {
x: overflowMenu.maskX
y: overflowMenu.maskY
width: overflowMenu.maskWidth
height: overflowMenu.maskHeight
}
}
onVisibleChanged: {
if (visible) {
if (currentTrayMenu) {
currentTrayMenu.showMenu = false;
}
if (root.parentScreen) {
TrayMenuManager.registerMenu(root.parentScreen.name, overflowMenu);
}
PopoutManager.closeAllPopouts();
ModalManager.closeAllModalsExcept(null);
updatePosition();
} else if (!visible && root.parentScreen) {
TrayMenuManager.unregisterMenu(root.parentScreen.name);
}
}
MouseArea {
x: overflowMenu.maskX
y: overflowMenu.maskY
width: overflowMenu.maskWidth
height: overflowMenu.maskHeight
z: -1
enabled: root.menuOpen
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: mouse => {
const clickX = mouse.x + overflowMenu.maskX;
const clickY = mouse.y + overflowMenu.maskY;
const outsideContent = clickX < menuContainer.x || clickX > menuContainer.x + menuContainer.width || clickY < menuContainer.y || clickY > menuContainer.y + menuContainer.height;
if (!outsideContent)
return;
root.menuOpen = false;
}
}
FocusScope {
id: overflowFocusScope
anchors.fill: parent
focus: true
Keys.onEscapePressed: {
root.menuOpen = false;
}
}
function updatePosition() {
const globalPos = root.mapToGlobal(0, 0);
const screenX = screen.x || 0;
const screenY = screen.y || 0;
const relativeX = globalPos.x - screenX;
const relativeY = globalPos.y - screenY;
if (root.isVertical) {
const edge = root.axis?.edge;
let targetX = edge === "left" ? root.barThickness + root.barSpacing + Theme.popupDistance : screen.width - (root.barThickness + root.barSpacing + Theme.popupDistance);
const adjustedY = relativeY + root.height / 2 + root.minTooltipY;
anchorPos = Qt.point(targetX, adjustedY);
} else {
let targetY = root.isAtBottom ? screen.height - (root.barThickness + root.barSpacing + (barConfig?.bottomGap ?? 0) + Theme.popupDistance) : root.barThickness + root.barSpacing + (barConfig?.bottomGap ?? 0) + Theme.popupDistance;
anchorPos = Qt.point(relativeX + root.width / 2, targetY);
}
}
Item {
id: menuContainer
objectName: "overflowMenuContainer"
readonly property real rawWidth: {
const itemCount = root.hiddenBarItems.length;
const cols = Math.min(5, itemCount);
const itemSize = 28;
const spacing = 2;
return cols * itemSize + (cols - 1) * spacing + Theme.spacingS * 2;
}
readonly property real rawHeight: {
const itemCount = root.hiddenBarItems.length;
const cols = Math.min(5, itemCount);
const rows = Math.ceil(itemCount / cols);
const itemSize = 28;
const spacing = 2;
return rows * itemSize + (rows - 1) * spacing + Theme.spacingS * 2;
}
readonly property real alignedWidth: Theme.px(rawWidth, overflowMenu.dpr)
readonly property real alignedHeight: Theme.px(rawHeight, overflowMenu.dpr)
width: alignedWidth
height: alignedHeight
x: Theme.snap((() => {
if (root.isVertical) {
const edge = root.axis?.edge;
if (edge === "left") {
const targetX = overflowMenu.anchorPos.x;
return Math.min(overflowMenu.screen.width - alignedWidth - 10, targetX);
} else {
const targetX = overflowMenu.anchorPos.x - alignedWidth;
return Math.max(10, targetX);
}
} else {
const left = 10;
const right = overflowMenu.width - alignedWidth - 10;
const want = overflowMenu.anchorPos.x - alignedWidth / 2;
return Math.max(left, Math.min(right, want));
}
})(), overflowMenu.dpr)
y: Theme.snap((() => {
if (root.isVertical) {
const top = Math.max(overflowMenu.barY, 10);
const bottom = overflowMenu.height - alignedHeight - 10;
const want = overflowMenu.anchorPos.y - alignedHeight / 2;
return Math.max(top, Math.min(bottom, want));
} else {
if (root.isAtBottom) {
const targetY = overflowMenu.anchorPos.y - alignedHeight;
return Math.max(10, targetY);
} else {
const targetY = overflowMenu.anchorPos.y;
return Math.min(overflowMenu.screen.height - alignedHeight - 10, targetY);
}
}
})(), overflowMenu.dpr)
property real shadowBlurPx: 10
property real shadowSpreadPx: 0
property real shadowBaseAlpha: 0.60
readonly property real popupSurfaceAlpha: Theme.popupTransparency
readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha))
opacity: root.menuOpen ? 1 : 0
scale: root.menuOpen ? 1 : 0.85
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Item {
id: bgShadowLayer
anchors.fill: parent
layer.enabled: true
layer.smooth: true
layer.textureSize: Qt.size(Math.round(width * overflowMenu.dpr * 2), Math.round(height * overflowMenu.dpr * 2))
layer.textureMirroring: ShaderEffectSource.MirrorVertically
layer.samples: 4
readonly property int blurMax: 64
layer.effect: MultiEffect {
autoPaddingEnabled: true
shadowEnabled: true
blurEnabled: false
maskEnabled: false
shadowBlur: Math.max(0, Math.min(1, menuContainer.shadowBlurPx / bgShadowLayer.blurMax))
shadowScale: 1 + (2 * menuContainer.shadowSpreadPx) / Math.max(1, Math.min(bgShadowLayer.width, bgShadowLayer.height))
shadowColor: {
const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest;
return Theme.withAlpha(baseColor, menuContainer.effectiveShadowAlpha);
}
}
Rectangle {
anchors.fill: parent
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
antialiasing: true
smooth: true
}
}
Grid {
id: menuGrid
anchors.centerIn: parent
columns: Math.min(5, root.hiddenBarItems.length)
spacing: 2
rowSpacing: 2
Repeater {
model: root.hiddenBarItems
delegate: Rectangle {
property var trayItem: modelData
property string iconSource: {
let icon = trayItem?.icon;
if (typeof icon === 'string' || icon instanceof String) {
if (icon === "")
return "";
if (icon.includes("?path=")) {
const split = icon.split("?path=");
if (split.length !== 2)
return icon;
const name = split[0];
const path = split[1];
let fileName = name.substring(name.lastIndexOf("/") + 1);
if (fileName.startsWith("dropboxstatus")) {
fileName = `hicolor/16x16/status/${fileName}`;
}
return `file://${path}/${fileName}`;
}
if (icon.startsWith("/") && !icon.startsWith("file://")) {
return `file://${icon}`;
}
return icon;
}
return "";
}
width: 28
height: 28
radius: Theme.cornerRadius
color: itemArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.surfaceContainer, 0)
IconImage {
id: menuIconImg
anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness)
height: Theme.barIconSize(root.barThickness)
source: parent.iconSource
asynchronous: true
smooth: true
mipmap: true
visible: status === Image.Ready
}
Text {
anchors.centerIn: parent
visible: !menuIconImg.visible
text: {
const itemId = trayItem?.id || "";
if (!itemId)
return "?";
return itemId.charAt(0).toUpperCase();
}
font.pixelSize: 10
color: Theme.widgetTextColor
}
MouseArea {
id: itemArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
onClicked: mouse => {
if (!trayItem)
return;
if (mouse.button === Qt.LeftButton && !trayItem.onlyMenu) {
trayItem.activate();
root.menuOpen = false;
return;
}
if (!trayItem.hasMenu)
return;
root.showForTrayItem(trayItem, menuContainer, parentScreen, root.isAtBottom, root.isVertical, root.axis);
}
}
}
}
}
}
}
Component {
id: trayMenuComponent
Rectangle {
id: menuRoot
property var trayItem: null
property var anchorItem: null
property var parentScreen: null
property bool isAtBottom: false
property bool isVertical: false
property var axis: null
property bool showMenu: false
property var menuHandle: null
ListModel {
id: entryStack
}
function topEntry() {
return entryStack.count ? entryStack.get(entryStack.count - 1).handle : null;
}
function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj) {
trayItem = item;
anchorItem = anchor;
parentScreen = screen;
isAtBottom = atBottom;
isVertical = vertical;
axis = axisObj;
menuHandle = item?.menu;
if (parentScreen) {
for (var i = 0; i < Quickshell.screens.length; i++) {
const s = Quickshell.screens[i];
if (s === parentScreen) {
menuWindow.screen = s;
break;
}
}
}
showMenu = true;
}
function close() {
showMenu = false;
}
Connections {
target: menuWindow
function onVisibleChanged() {
if (menuWindow.visible && parentScreen) {
TrayMenuManager.registerMenu(parentScreen.name, menuRoot);
} else if (!menuWindow.visible && parentScreen) {
TrayMenuManager.unregisterMenu(parentScreen.name);
}
}
}
Component.onDestruction: {
if (parentScreen) {
TrayMenuManager.unregisterMenu(parentScreen.name);
}
}
Connections {
target: PopoutManager
function onPopoutOpening() {
menuRoot.close();
}
}
function closeWithAction() {
close();
}
function showSubMenu(entry) {
if (!entry || !entry.hasChildren)
return;
entryStack.append({
handle: entry
});
const h = entry.menu || entry;
if (h && typeof h.updateLayout === "function")
h.updateLayout();
submenuHydrator.menu = h;
submenuHydrator.open();
Qt.callLater(() => submenuHydrator.close());
}
function goBack() {
if (!entryStack.count)
return;
entryStack.remove(entryStack.count - 1);
}
width: 0
height: 0
color: "transparent"
PanelWindow {
id: menuWindow
WlrLayershell.namespace: "dms:tray-menu-window"
visible: menuRoot.showMenu && (menuRoot.trayItem?.hasMenu ?? false)
WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: {
if (!menuRoot.showMenu)
return WlrKeyboardFocus.None;
if (CompositorService.useHyprlandFocusGrab)
return WlrKeyboardFocus.OnDemand;
return WlrKeyboardFocus.Exclusive;
}
color: "transparent"
HyprlandFocusGrab {
windows: [menuWindow]
active: CompositorService.useHyprlandFocusGrab && menuRoot.showMenu
}
anchors {
top: true
left: true
right: true
bottom: true
}
readonly property real dpr: (typeof CompositorService !== "undefined" && CompositorService.getScreenScale) ? CompositorService.getScreenScale(menuWindow.screen) : (screen?.devicePixelRatio || 1)
property point anchorPos: Qt.point(screen.width / 2, screen.height / 2)
property var barBounds: {
if (!menuWindow.screen || !root.barConfig) {
return {
"x": 0,
"y": 0,
"width": 0,
"height": 0,
"wingSize": 0
};
}
const barPosition = root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1));
return SettingsData.getBarBounds(menuWindow.screen, root.barThickness + root.barSpacing, barPosition, root.barConfig);
}
property real barX: barBounds.x
property real barY: barBounds.y
property real barWidth: barBounds.width
property real barHeight: barBounds.height
readonly property int barPosition: root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1))
readonly property var adjacentBarInfo: menuRoot.parentScreen ? SettingsData.getAdjacentBarInfo(menuRoot.parentScreen, barPosition, root.barConfig) : ({
"topBar": 0,
"bottomBar": 0,
"leftBar": 0,
"rightBar": 0
})
readonly property real effectiveBarSize: root.barThickness + root.barSpacing
readonly property real maskX: {
const triggeringBarX = (barPosition === 2) ? effectiveBarSize : 0;
const adjacentLeftBar = adjacentBarInfo?.leftBar ?? 0;
return Math.max(triggeringBarX, adjacentLeftBar);
}
readonly property real maskY: {
const triggeringBarY = (barPosition === 0) ? effectiveBarSize : 0;
const adjacentTopBar = adjacentBarInfo?.topBar ?? 0;
return Math.max(triggeringBarY, adjacentTopBar);
}
readonly property real maskWidth: {
const triggeringBarRight = (barPosition === 3) ? effectiveBarSize : 0;
const adjacentRightBar = adjacentBarInfo?.rightBar ?? 0;
const rightExclusion = Math.max(triggeringBarRight, adjacentRightBar);
return Math.max(100, width - maskX - rightExclusion);
}
readonly property real maskHeight: {
const triggeringBarBottom = (barPosition === 1) ? effectiveBarSize : 0;
const adjacentBottomBar = adjacentBarInfo?.bottomBar ?? 0;
const bottomExclusion = Math.max(triggeringBarBottom, adjacentBottomBar);
return Math.max(100, height - maskY - bottomExclusion);
}
mask: Region {
item: Rectangle {
x: menuWindow.maskX
y: menuWindow.maskY
width: menuWindow.maskWidth
height: menuWindow.maskHeight
}
}
onVisibleChanged: {
if (visible) {
updatePosition();
root.menuOpen = false;
PopoutManager.closeAllPopouts();
ModalManager.closeAllModalsExcept(null);
}
}
MouseArea {
x: menuWindow.maskX
y: menuWindow.maskY
width: menuWindow.maskWidth
height: menuWindow.maskHeight
z: -1
enabled: menuRoot.showMenu
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: mouse => {
const clickX = mouse.x + menuWindow.maskX;
const clickY = mouse.y + menuWindow.maskY;
const outsideContent = clickX < menuContainer.x || clickX > menuContainer.x + menuContainer.width || clickY < menuContainer.y || clickY > menuContainer.y + menuContainer.height;
if (!outsideContent)
return;
menuRoot.close();
}
}
FocusScope {
id: menuFocusScope
anchors.fill: parent
focus: true
Keys.onEscapePressed: {
if (entryStack.count > 0) {
menuRoot.goBack();
} else {
menuRoot.close();
}
}
}
function updatePosition() {
const targetItem = (typeof menuRoot !== "undefined" && menuRoot.anchorItem) ? menuRoot.anchorItem : root;
const isFromOverflowMenu = targetItem.objectName === "overflowMenuContainer";
if (isFromOverflowMenu) {
if (menuRoot.isVertical) {
const edge = menuRoot.axis?.edge;
let targetX = edge === "left" ? root.barThickness + root.barSpacing + Theme.popupDistance : screen.width - (root.barThickness + root.barSpacing + Theme.popupDistance);
const targetY = targetItem.y + targetItem.height / 2;
anchorPos = Qt.point(targetX, targetY);
} else {
const targetX = targetItem.x + targetItem.width / 2;
let targetY = menuRoot.isAtBottom ? screen.height - (root.barThickness + root.barSpacing + (barConfig?.bottomGap ?? 0) + Theme.popupDistance) : root.barThickness + root.barSpacing + (barConfig?.bottomGap ?? 0) + Theme.popupDistance;
anchorPos = Qt.point(targetX, targetY);
}
} else {
const globalPos = targetItem.mapToGlobal(0, 0);
const screenX = screen.x || 0;
const screenY = screen.y || 0;
const relativeX = globalPos.x - screenX;
const relativeY = globalPos.y - screenY;
if (menuRoot.isVertical) {
const edge = menuRoot.axis?.edge;
let targetX = edge === "left" ? root.barThickness + root.barSpacing + Theme.popupDistance : screen.width - (root.barThickness + root.barSpacing + Theme.popupDistance);
const adjustedY = relativeY + targetItem.height / 2 + root.minTooltipY;
anchorPos = Qt.point(targetX, adjustedY);
} else {
let targetY = menuRoot.isAtBottom ? screen.height - (root.barThickness + root.barSpacing + (barConfig?.bottomGap ?? 0) + Theme.popupDistance) : root.barThickness + root.barSpacing + (barConfig?.bottomGap ?? 0) + Theme.popupDistance;
anchorPos = Qt.point(relativeX + targetItem.width / 2, targetY);
}
}
}
Item {
id: menuContainer
readonly property real rawWidth: Math.min(500, Math.max(250, menuColumn.implicitWidth + Theme.spacingS * 2))
readonly property real rawHeight: Math.max(40, menuColumn.implicitHeight + Theme.spacingS * 2)
readonly property real alignedWidth: Theme.px(rawWidth, menuWindow.dpr)
readonly property real alignedHeight: Theme.px(rawHeight, menuWindow.dpr)
width: alignedWidth
height: alignedHeight
x: Theme.snap((() => {
if (menuRoot.isVertical) {
const edge = menuRoot.axis?.edge;
if (edge === "left") {
const targetX = menuWindow.anchorPos.x;
return Math.min(menuWindow.screen.width - alignedWidth - 10, targetX);
} else {
const targetX = menuWindow.anchorPos.x - alignedWidth;
return Math.max(10, targetX);
}
} else {
const left = 10;
const right = menuWindow.width - alignedWidth - 10;
const want = menuWindow.anchorPos.x - alignedWidth / 2;
return Math.max(left, Math.min(right, want));
}
})(), menuWindow.dpr)
y: Theme.snap((() => {
if (menuRoot.isVertical) {
const top = Math.max(menuWindow.barY, 10);
const bottom = menuWindow.height - alignedHeight - 10;
const want = menuWindow.anchorPos.y - alignedHeight / 2;
return Math.max(top, Math.min(bottom, want));
} else {
if (menuRoot.isAtBottom) {
const targetY = menuWindow.anchorPos.y - alignedHeight;
return Math.max(10, targetY);
} else {
const targetY = menuWindow.anchorPos.y;
return Math.min(menuWindow.screen.height - alignedHeight - 10, targetY);
}
}
})(), menuWindow.dpr)
property real shadowBlurPx: 10
property real shadowSpreadPx: 0
property real shadowBaseAlpha: 0.60
readonly property real popupSurfaceAlpha: Theme.popupTransparency
readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha))
opacity: menuRoot.showMenu ? 1 : 0
scale: menuRoot.showMenu ? 1 : 0.85
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Item {
id: menuBgShadowLayer
anchors.fill: parent
layer.enabled: true
layer.smooth: true
layer.textureSize: Qt.size(Math.round(width * menuWindow.dpr), Math.round(height * menuWindow.dpr))
layer.textureMirroring: ShaderEffectSource.MirrorVertically
readonly property int blurMax: 64
layer.effect: MultiEffect {
autoPaddingEnabled: true
shadowEnabled: true
blurEnabled: false
maskEnabled: false
shadowBlur: Math.max(0, Math.min(1, menuContainer.shadowBlurPx / menuBgShadowLayer.blurMax))
shadowScale: 1 + (2 * menuContainer.shadowSpreadPx) / Math.max(1, Math.min(menuBgShadowLayer.width, menuBgShadowLayer.height))
shadowColor: {
const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest;
return Theme.withAlpha(baseColor, menuContainer.effectiveShadowAlpha);
}
}
Rectangle {
anchors.fill: parent
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
antialiasing: true
}
}
QsMenuAnchor {
id: submenuHydrator
anchor.window: menuWindow
}
QsMenuOpener {
id: rootOpener
menu: menuRoot.menuHandle
}
QsMenuOpener {
id: subOpener
menu: {
const e = menuRoot.topEntry();
return e ? (e.menu || e) : null;
}
}
Column {
id: menuColumn
width: parent.width - Theme.spacingS * 2
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Theme.spacingS
spacing: 1
Rectangle {
visible: entryStack.count === 0
width: parent.width
height: 28
radius: 0
color: visibilityToggleArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.surfaceContainer, 0)
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: menuRoot.trayItem?.id || "Unknown"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
elide: Text.ElideMiddle
width: parent.width - Theme.spacingS * 2 - 24
}
DankIcon {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
name: SessionData.isHiddenTrayId(menuRoot.trayItem?.id || "") ? "visibility" : "visibility_off"
size: 16
color: Theme.widgetTextColor
}
MouseArea {
id: visibilityToggleArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
const itemId = menuRoot.trayItem?.id || "";
if (!itemId)
return;
if (SessionData.isHiddenTrayId(itemId)) {
SessionData.showTrayId(itemId);
} else {
SessionData.hideTrayId(itemId);
}
menuRoot.closeWithAction();
}
}
}
Rectangle {
visible: entryStack.count === 0
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
Rectangle {
visible: entryStack.count > 0
width: parent.width
height: 28
radius: 0
color: backArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.surfaceContainer, 0)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
DankIcon {
name: "arrow_back"
size: 16
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Back")
font.pixelSize: Theme.fontSizeSmall
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: backArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: menuRoot.goBack()
}
}
Rectangle {
visible: entryStack.count > 0
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
Repeater {
model: entryStack.count ? (subOpener.children ? subOpener.children : (menuRoot.topEntry()?.children || [])) : rootOpener.children
Rectangle {
property var menuEntry: modelData
width: menuColumn.width
height: menuEntry?.isSeparator ? 1 : 28
radius: 0
color: {
if (menuEntry?.isSeparator) {
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2);
}
return itemArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.surfaceContainer, 0);
}
MouseArea {
id: itemArea
anchors.fill: parent
hoverEnabled: true
enabled: !menuEntry?.isSeparator && (menuEntry?.enabled !== false)
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!menuEntry || menuEntry.isSeparator)
return;
if (menuEntry.hasChildren) {
menuRoot.showSubMenu(menuEntry);
return;
}
if (typeof menuEntry.activate === "function") {
menuEntry.activate();
} else if (typeof menuEntry.triggered === "function") {
menuEntry.triggered();
}
Qt.createQmlObject('import QtQuick; Timer { interval: 80; running: true; repeat: false; onTriggered: menuRoot.closeWithAction() }', menuRoot);
}
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
visible: !menuEntry?.isSeparator
Rectangle {
width: 16
height: 16
anchors.verticalCenter: parent.verticalCenter
visible: menuEntry?.buttonType !== undefined && menuEntry.buttonType !== 0
radius: menuEntry?.buttonType === 2 ? 8 : 2
border.width: 1
border.color: Theme.outline
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width - 6
height: parent.height - 6
radius: parent.radius - 3
color: Theme.primary
visible: menuEntry?.checkState === 2
}
DankIcon {
anchors.centerIn: parent
name: "check"
size: 10
color: Theme.primaryText
visible: menuEntry?.buttonType === 1 && menuEntry?.checkState === 2
}
}
Item {
width: 16
height: 16
anchors.verticalCenter: parent.verticalCenter
visible: (menuEntry?.icon ?? "") !== ""
Image {
anchors.fill: parent
source: menuEntry?.icon || ""
sourceSize.width: 16
sourceSize.height: 16
fillMode: Image.PreserveAspectFit
smooth: true
}
}
StyledText {
text: menuEntry?.text || ""
font.pixelSize: Theme.fontSizeSmall
color: (menuEntry?.enabled !== false) ? Theme.surfaceText : Theme.surfaceTextMedium
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
width: Math.max(150, parent.width - 64)
wrapMode: Text.NoWrap
}
Item {
width: 16
height: 16
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: "chevron_right"
size: 14
color: Theme.widgetTextColor
visible: menuEntry?.hasChildren ?? false
}
}
}
}
}
}
}
}
}
}
function showForTrayItem(item, anchor, screen, atBottom, vertical, axisObj) {
if (!screen)
return;
if (currentTrayMenu) {
currentTrayMenu.showMenu = false;
currentTrayMenu.destroy();
currentTrayMenu = null;
}
PopoutManager.closeAllPopouts();
ModalManager.closeAllModalsExcept(null);
currentTrayMenu = trayMenuComponent.createObject(null);
if (!currentTrayMenu)
return;
currentTrayMenu.showForTrayItem(item, anchor, screen, atBottom, vertical ?? false, axisObj);
}
}