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.Modules.Plugins import qs.Services import qs.Widgets BasePill { id: root enableBackgroundHover: false enableCursor: false property var parentWindow: null property bool isAtBottom: false property bool isAutoHideBar: false 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()); }); } function getTrayItemKey(item) { const id = item?.id || ""; const tooltipTitle = item?.tooltipTitle || ""; if (!tooltipTitle || tooltipTitle === id) { return id; } return `${id}::${tooltipTitle}`; } // ! TODO - replace with either native dbus client (like plugins use) or just a DMS cli or something function callContextMenuFallback(trayItemId, globalX, globalY) { const script = ['ITEMS=$(dbus-send --session --print-reply --dest=org.kde.StatusNotifierWatcher /StatusNotifierWatcher org.freedesktop.DBus.Properties.Get string:org.kde.StatusNotifierWatcher string:RegisteredStatusNotifierItems 2>/dev/null)', 'while IFS= read -r line; do', ' line="${line#*\\\"}"', ' line="${line%\\\"*}"', ' [ -z "$line" ] && continue', ' BUS="${line%%/*}"', ' OBJ="/${line#*/}"', ' ID=$(dbus-send --session --print-reply --dest="$BUS" "$OBJ" org.freedesktop.DBus.Properties.Get string:org.kde.StatusNotifierItem string:Id 2>/dev/null | grep -oP "(?<=\\\")(.*?)(?=\\\")" | tail -1)', ' if [ "$ID" = "$1" ]; then', ' dbus-send --session --type=method_call --dest="$BUS" "$OBJ" org.kde.StatusNotifierItem.ContextMenu int32:"$2" int32:"$3"', ' exit 0', ' fi', 'done <<< "$ITEMS"',].join("\n"); Quickshell.execDetached(["bash", "-c", script, "_", trayItemId, String(globalX), String(globalY)]); } property int _trayOrderTrigger: 0 Connections { target: SessionData function onTrayItemOrderChanged() { root._trayOrderTrigger++; } } function sortByPreferredOrder(items, trigger) { void trigger; const savedOrder = SessionData.trayItemOrder || []; const orderMap = new Map(); savedOrder.forEach((key, idx) => orderMap.set(key, idx)); return [...items].sort((a, b) => { const keyA = getTrayItemKey(a); const keyB = getTrayItemKey(b); const orderA = orderMap.has(keyA) ? orderMap.get(keyA) : 10000 + items.indexOf(a); const orderB = orderMap.has(keyB) ? orderMap.get(keyB) : 10000 + items.indexOf(b); return orderA - orderB; }); } readonly property var allSortedTrayItems: sortByPreferredOrder(allTrayItems, _trayOrderTrigger) readonly property var allSortedTrayItemKeys: allSortedTrayItems.map(item => getTrayItemKey(item)) readonly property var mainBarItemsRaw: allSortedTrayItems.filter(item => !SessionData.isHiddenTrayId(root.getTrayItemKey(item))) readonly property var mainBarItems: mainBarItemsRaw.map((item, idx) => ({ key: getTrayItemKey(item), item: item })) readonly property var hiddenBarItems: allSortedTrayItems.filter(item => SessionData.isHiddenTrayId(root.getTrayItemKey(item))) function moveTrayItemInFullOrder(visibleFromIndex, visibleToIndex) { if (visibleFromIndex === visibleToIndex || visibleFromIndex < 0 || visibleToIndex < 0) return; const fromKey = mainBarItems[visibleFromIndex]?.key ?? null; const toKey = mainBarItems[visibleToIndex]?.key ?? null; if (!fromKey || !toKey) return; const fullOrder = [...allSortedTrayItemKeys]; const fullFromIndex = fullOrder.indexOf(fromKey); const fullToIndex = fullOrder.indexOf(toKey); if (fullFromIndex < 0 || fullToIndex < 0) return; const movedKey = fullOrder.splice(fullFromIndex, 1)[0]; fullOrder.splice(fullToIndex, 0, movedKey); SessionData.setTrayItemOrder(fullOrder); } property int draggedIndex: -1 property int dropTargetIndex: -1 property bool suppressShiftAnimation: false readonly property bool hasHiddenItems: allTrayItems.length > mainBarItems.length visible: allTrayItems.length > 0 opacity: allTrayItems.length > 0 ? 1 : 0 states: [ State { name: "hidden_horizontal" when: allTrayItems.length === 0 && !isVerticalOrientation PropertyChanges { target: root width: 0 } }, State { name: "hidden_vertical" when: allTrayItems.length === 0 && isVerticalOrientation PropertyChanges { target: root height: 0 } } ] transitions: [ Transition { NumberAnimation { properties: "width,height" duration: Theme.shortDuration easing.type: Theme.standardEasing } } ] Behavior on opacity { NumberAnimation { duration: Theme.shortDuration easing.type: Theme.standardEasing } } readonly property real trayItemSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + 6 readonly property real minTooltipY: { if (!parentScreen || !isVerticalOrientation) { 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 content: Component { Item { implicitWidth: layoutLoader.item ? layoutLoader.item.implicitWidth : 0 implicitHeight: layoutLoader.item ? layoutLoader.item.implicitHeight : 0 Loader { id: layoutLoader anchors.centerIn: parent sourceComponent: root.isVerticalOrientation ? columnComp : rowComp } } } Component { id: rowComp Row { spacing: 0 Repeater { model: ScriptModel { values: root.mainBarItems objectProp: "key" } delegate: Item { id: delegateRoot property var trayItem: modelData.item property string itemKey: modelData.key 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.trayItemSize height: root.barThickness z: dragHandler.dragging ? 100 : 0 property real shiftOffset: { if (root.draggedIndex < 0) return 0; if (index === root.draggedIndex) return 0; const dragIdx = root.draggedIndex; const dropIdx = root.dropTargetIndex; const shiftAmount = root.trayItemSize; if (dropIdx < 0) return 0; if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx) return -shiftAmount; if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx) return shiftAmount; return 0; } transform: Translate { x: delegateRoot.shiftOffset Behavior on x { enabled: !root.suppressShiftAnimation NumberAnimation { duration: 150 easing.type: Easing.OutCubic } } } Item { id: dragHandler anchors.fill: parent property bool dragging: false property point dragStartPos: Qt.point(0, 0) property real dragAxisOffset: 0 property bool longPressing: false Timer { id: longPressTimer interval: 400 repeat: false onTriggered: dragHandler.longPressing = true } } Rectangle { id: visualContent width: root.trayItemSize height: root.trayItemSize anchors.centerIn: parent radius: Theme.cornerRadius color: trayItemArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" border.width: dragHandler.dragging ? 2 : 0 border.color: Theme.primary opacity: dragHandler.dragging ? 0.8 : 1.0 transform: Translate { x: dragHandler.dragging ? dragHandler.dragAxisOffset : 0 } IconImage { id: iconImg anchors.centerIn: parent width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) 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 } DankRipple { id: itemRipple cornerRadius: Theme.cornerRadius } } MouseArea { id: trayItemArea anchors.fill: parent hoverEnabled: true acceptedButtons: Qt.LeftButton | Qt.RightButton cursorShape: dragHandler.longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor onPressed: mouse => { const pos = mapToItem(visualContent, mouse.x, mouse.y); itemRipple.trigger(pos.x, pos.y); if (mouse.button === Qt.LeftButton) { dragHandler.dragStartPos = Qt.point(mouse.x, mouse.y); longPressTimer.start(); } } onReleased: mouse => { longPressTimer.stop(); const wasDragging = dragHandler.dragging; const didReorder = wasDragging && root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex; if (didReorder) { root.suppressShiftAnimation = true; root.moveTrayItemInFullOrder(root.draggedIndex, root.dropTargetIndex); Qt.callLater(() => root.suppressShiftAnimation = false); } dragHandler.longPressing = false; dragHandler.dragging = false; dragHandler.dragAxisOffset = 0; root.draggedIndex = -1; root.dropTargetIndex = -1; if (wasDragging || mouse.button !== Qt.LeftButton) return; if (!delegateRoot.trayItem) return; if (!delegateRoot.trayItem.onlyMenu) { delegateRoot.trayItem.activate(); return; } if (!delegateRoot.trayItem.hasMenu) return; root.menuOpen = false; root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis); } onPositionChanged: mouse => { if (dragHandler.longPressing && !dragHandler.dragging) { const distance = Math.abs(mouse.x - dragHandler.dragStartPos.x); if (distance > 5) { dragHandler.dragging = true; root.draggedIndex = index; root.dropTargetIndex = index; } } if (!dragHandler.dragging) return; const axisOffset = mouse.x - dragHandler.dragStartPos.x; dragHandler.dragAxisOffset = axisOffset; const itemSize = root.trayItemSize; const slotOffset = Math.round(axisOffset / itemSize); const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset)); if (newTargetIndex !== root.dropTargetIndex) { root.dropTargetIndex = newTargetIndex; } } onClicked: mouse => { if (dragHandler.dragging) return; if (mouse.button !== Qt.RightButton) return; if (!delegateRoot.trayItem?.hasMenu) { const gp = trayItemArea.mapToGlobal(mouse.x, mouse.y); root.callContextMenuFallback(delegateRoot.trayItem.id, Math.round(gp.x), Math.round(gp.y)); return; } root.menuOpen = false; root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis); } } } } Item { width: root.trayItemSize height: root.barThickness visible: root.hasHiddenItems Rectangle { id: caretButton width: root.trayItemSize height: root.trayItemSize anchors.centerIn: parent radius: Theme.cornerRadius color: caretArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" DankIcon { anchors.centerIn: parent name: root.menuOpen ? "expand_less" : "expand_more" size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) color: Theme.widgetTextColor } DankRipple { id: caretRipple cornerRadius: Theme.cornerRadius } MouseArea { id: caretArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onPressed: mouse => { caretRipple.trigger(mouse.x, mouse.y); } onClicked: root.menuOpen = !root.menuOpen } } } } } Component { id: columnComp Column { spacing: 0 Repeater { model: ScriptModel { values: root.mainBarItems objectProp: "key" } delegate: Item { id: delegateRoot property var trayItem: modelData.item property string itemKey: modelData.key 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: root.trayItemSize z: dragHandler.dragging ? 100 : 0 property real shiftOffset: { if (root.draggedIndex < 0) return 0; if (index === root.draggedIndex) return 0; const dragIdx = root.draggedIndex; const dropIdx = root.dropTargetIndex; const shiftAmount = root.trayItemSize; if (dropIdx < 0) return 0; if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx) return -shiftAmount; if (dragIdx > dropIdx && index >= dropIdx && index < dragIdx) return shiftAmount; return 0; } transform: Translate { y: delegateRoot.shiftOffset Behavior on y { enabled: !root.suppressShiftAnimation NumberAnimation { duration: 150 easing.type: Easing.OutCubic } } } Item { id: dragHandler anchors.fill: parent property bool dragging: false property point dragStartPos: Qt.point(0, 0) property real dragAxisOffset: 0 property bool longPressing: false Timer { id: longPressTimer interval: 400 repeat: false onTriggered: dragHandler.longPressing = true } } Rectangle { id: visualContent width: root.trayItemSize height: root.trayItemSize anchors.centerIn: parent radius: Theme.cornerRadius color: trayItemArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" border.width: dragHandler.dragging ? 2 : 0 border.color: Theme.primary opacity: dragHandler.dragging ? 0.8 : 1.0 transform: Translate { y: dragHandler.dragging ? dragHandler.dragAxisOffset : 0 } IconImage { id: iconImg anchors.centerIn: parent width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) 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 } DankRipple { id: itemRipple cornerRadius: Theme.cornerRadius } } MouseArea { id: trayItemArea anchors.fill: parent hoverEnabled: true acceptedButtons: Qt.LeftButton | Qt.RightButton cursorShape: dragHandler.longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor onPressed: mouse => { const pos = mapToItem(visualContent, mouse.x, mouse.y); itemRipple.trigger(pos.x, pos.y); if (mouse.button === Qt.LeftButton) { dragHandler.dragStartPos = Qt.point(mouse.x, mouse.y); longPressTimer.start(); } } onReleased: mouse => { longPressTimer.stop(); const wasDragging = dragHandler.dragging; const didReorder = wasDragging && root.dropTargetIndex >= 0 && root.dropTargetIndex !== root.draggedIndex; if (didReorder) { root.suppressShiftAnimation = true; root.moveTrayItemInFullOrder(root.draggedIndex, root.dropTargetIndex); Qt.callLater(() => root.suppressShiftAnimation = false); } dragHandler.longPressing = false; dragHandler.dragging = false; dragHandler.dragAxisOffset = 0; root.draggedIndex = -1; root.dropTargetIndex = -1; if (wasDragging || mouse.button !== Qt.LeftButton) return; if (!delegateRoot.trayItem) return; if (!delegateRoot.trayItem.onlyMenu) { delegateRoot.trayItem.activate(); return; } if (!delegateRoot.trayItem.hasMenu) return; root.menuOpen = false; root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis); } onPositionChanged: mouse => { if (dragHandler.longPressing && !dragHandler.dragging) { const distance = Math.abs(mouse.y - dragHandler.dragStartPos.y); if (distance > 5) { dragHandler.dragging = true; root.draggedIndex = index; root.dropTargetIndex = index; } } if (!dragHandler.dragging) return; const axisOffset = mouse.y - dragHandler.dragStartPos.y; dragHandler.dragAxisOffset = axisOffset; const itemSize = root.trayItemSize; const slotOffset = Math.round(axisOffset / itemSize); const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset)); if (newTargetIndex !== root.dropTargetIndex) { root.dropTargetIndex = newTargetIndex; } } onClicked: mouse => { if (dragHandler.dragging) return; if (mouse.button !== Qt.RightButton) return; if (!delegateRoot.trayItem?.hasMenu) { const gp = trayItemArea.mapToGlobal(mouse.x, mouse.y); root.callContextMenuFallback(delegateRoot.trayItem.id, Math.round(gp.x), Math.round(gp.y)); return; } root.menuOpen = false; root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis); } } } } Item { width: root.barThickness height: root.trayItemSize visible: root.hasHiddenItems Rectangle { id: caretButtonVert width: root.trayItemSize height: root.trayItemSize anchors.centerIn: parent radius: Theme.cornerRadius color: caretAreaVert.containsMouse ? Theme.widgetBaseHoverColor : "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, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) color: Theme.widgetTextColor } DankRipple { id: caretRippleVert cornerRadius: Theme.cornerRadius } MouseArea { id: caretAreaVert anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onPressed: mouse => { caretRippleVert.trigger(mouse.x, mouse.y); } 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.isVerticalOrientation) { 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 = root.trayItemSize + 4; 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 = root.trayItemSize + 4; 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.isVerticalOrientation) { 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.isVerticalOrientation) { 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) readonly property var elev: Theme.elevationLevel2 property real shadowBlurPx: elev && elev.blurPx !== undefined ? elev.blurPx : 8 property real shadowSpreadPx: elev && elev.spreadPx !== undefined ? elev.spreadPx : 0 property real shadowBaseAlpha: elev && elev.alpha !== undefined ? elev.alpha : 0.25 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 } } ElevationShadow { id: bgShadowLayer anchors.fill: parent level: menuContainer.elev fallbackOffset: 4 shadowBlurPx: menuContainer.shadowBlurPx shadowSpreadPx: menuContainer.shadowSpreadPx shadowColor: { const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest; return Theme.withAlpha(baseColor, menuContainer.effectiveShadowAlpha); } targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) targetRadius: Theme.cornerRadius sourceRect.antialiasing: true sourceRect.smooth: true shadowEnabled: Theme.elevationEnabled 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 } 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: root.trayItemSize + 4 height: root.trayItemSize + 4 radius: Theme.cornerRadius color: itemArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.withAlpha(Theme.surfaceContainer, 0) IconImage { id: menuIconImg anchors.centerIn: parent width: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) height: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) 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) { const gp = itemArea.mapToGlobal(mouse.x, mouse.y); root.callContextMenuFallback(trayItem.id, Math.round(gp.x), Math.round(gp.y)); return; } root.showForTrayItem(trayItem, menuContainer, parentScreen, root.isAtBottom, root.isVerticalOrientation, 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) readonly property var elev: Theme.elevationLevel2 property real shadowBlurPx: elev && elev.blurPx !== undefined ? elev.blurPx : 8 property real shadowSpreadPx: elev && elev.spreadPx !== undefined ? elev.spreadPx : 0 property real shadowBaseAlpha: elev && elev.alpha !== undefined ? elev.alpha : 0.25 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 } } ElevationShadow { id: menuBgShadowLayer anchors.fill: parent level: menuContainer.elev fallbackOffset: 4 shadowBlurPx: menuContainer.shadowBlurPx shadowSpreadPx: menuContainer.shadowSpreadPx shadowColor: { const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest; return Theme.withAlpha(baseColor, menuContainer.effectiveShadowAlpha); } targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) targetRadius: Theme.cornerRadius sourceRect.antialiasing: true shadowEnabled: Theme.elevationEnabled layer.smooth: true layer.textureSize: Qt.size(Math.round(width * menuWindow.dpr), Math.round(height * menuWindow.dpr)) layer.textureMirroring: ShaderEffectSource.MirrorVertically } 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: Theme.cornerRadius color: visibilityToggleArea.containsMouse ? Theme.widgetBaseHoverColor : 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(root.getTrayItemKey(menuRoot.trayItem)) ? "visibility" : "visibility_off" size: 16 color: Theme.widgetTextColor } MouseArea { id: visibilityToggleArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { const itemKey = root.getTrayItemKey(menuRoot.trayItem); if (!itemKey) return; if (SessionData.isHiddenTrayId(itemKey)) { SessionData.showTrayId(itemKey); } else { SessionData.hideTrayId(itemKey); } 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: Theme.cornerRadius color: backArea.containsMouse ? Theme.widgetBaseHoverColor : 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: menuEntry?.isSeparator ? 0 : Theme.cornerRadius color: { if (menuEntry?.isSeparator) return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2); return itemArea.containsMouse ? Theme.widgetBaseHoverColor : 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); } }