summaryrefslogtreecommitdiff
path: root/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml
diff options
context:
space:
mode:
authornippy <you@example.com>2026-04-18 13:49:56 +0200
committernippy <you@example.com>2026-04-18 13:49:56 +0200
commit5d94c0a7d44a2255b81815a52a7056a94a39842d (patch)
tree759bdea9645c6a62f9e1e4c001f7d81cccd120d2 /raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml
parente79cdf210b267f21a186255ce1a4d50029439d54 (diff)
downloadRaveOS-PKGBUILD-5d94c0a7d44a2255b81815a52a7056a94a39842d.tar.gz
RaveOS-PKGBUILD-5d94c0a7d44a2255b81815a52a7056a94a39842d.zip
update Raveos themes
Diffstat (limited to 'raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml')
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml1893
1 files changed, 1893 insertions, 0 deletions
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml
new file mode 100644
index 0000000..7fb9362
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml
@@ -0,0 +1,1893 @@
+import QtQuick
+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 var widgetData: null
+ property string section: "right"
+ property bool isAtBottom: false
+ property bool isAutoHideBar: false
+ property bool useOverflowPopup: !widgetData?.trayUseInlineExpansion
+ 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}`;
+ }
+
+ function trayIconSourceFor(trayItem) {
+ 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 "";
+ }
+
+ function activateInlineTrayItem(trayItem, anchorItem) {
+ if (!trayItem)
+ return;
+ if (!trayItem.onlyMenu) {
+ trayItem.activate();
+ return;
+ }
+ if (!trayItem.hasMenu)
+ return;
+ root.showForTrayItem(trayItem, anchorItem, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
+ }
+
+ function openInlineTrayContextMenu(trayItem, areaItem, mouse, anchorItem) {
+ if (!trayItem) {
+ return;
+ }
+ if (!trayItem.hasMenu) {
+ const gp = areaItem.mapToGlobal(mouse.x, mouse.y);
+ root.callContextMenuFallback(trayItem.id, Math.round(gp.x), Math.round(gp.y));
+ return;
+ }
+ root.showForTrayItem(trayItem, anchorItem, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
+ }
+
+ function toggleIconName() {
+ const edge = root.axis?.edge;
+ if (root.useOverflowPopup) {
+ switch (edge) {
+ case "left":
+ return root.menuOpen ? "keyboard_arrow_left" : "keyboard_arrow_right";
+ case "right":
+ return root.menuOpen ? "keyboard_arrow_right" : "keyboard_arrow_left";
+ case "bottom":
+ return root.menuOpen ? "keyboard_arrow_down" : "keyboard_arrow_up";
+ case "top":
+ return root.menuOpen ? "keyboard_arrow_up" : "keyboard_arrow_down";
+ }
+ }
+
+ if (edge === "left" || edge === "right") {
+ return root.menuOpen == (root.section !== "right") ? "keyboard_arrow_up" : "keyboard_arrow_down";
+ }
+
+ return root.menuOpen != (root.section === "right") ? "keyboard_arrow_left" : "keyboard_arrow_right";
+ }
+
+ // ! 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)))
+ readonly property bool reverseInlineHorizontal: !useOverflowPopup && !isVerticalOrientation && section === "right"
+ readonly property bool reverseInlineVertical: !useOverflowPopup && isVerticalOrientation && section === "right"
+ readonly property var displayedMainBarItems: reverseInlineHorizontal ? [...mainBarItems].reverse() : mainBarItems
+ readonly property var displayedInlineExpandedItems: (reverseInlineHorizontal ? [...hiddenBarItems].reverse() : hiddenBarItems).map(item => ({
+ key: getTrayItemKey(item),
+ item: 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
+ readonly property bool inlineExpanded: hasHiddenItems && !useOverflowPopup && menuOpen
+ 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;
+ }
+
+ readonly property string autoBarShadowDirection: {
+ const edge = root.axis?.edge;
+ switch (edge) {
+ case "top":
+ return "top";
+ case "bottom":
+ return "bottom";
+ case "left":
+ return "left";
+ case "right":
+ return "right";
+ default:
+ return "bottom";
+ }
+ }
+ readonly property string effectiveShadowDirection: Theme.elevationLightDirection === "autoBar" ? autoBarShadowDirection : Theme.elevationLightDirection
+
+ 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
+ layoutDirection: root.reverseInlineHorizontal ? Qt.RightToLeft : Qt.LeftToRight
+
+ Repeater {
+ model: ScriptModel {
+ values: root.displayedMainBarItems
+ objectProp: "key"
+ }
+
+ delegate: Item {
+ id: delegateRoot
+ property var trayItem: modelData.item
+ property string itemKey: modelData.key
+ property string iconSource: root.trayIconSourceFor(trayItem)
+
+ 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 ? BlurService.hoverColor(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;
+ if (root.useOverflowPopup)
+ 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 = root.reverseInlineHorizontal ? (root.mainBarItems.length - 1 - index) : index;
+ root.dropTargetIndex = root.draggedIndex;
+ }
+ }
+ 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 visualTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
+ const newTargetIndex = root.reverseInlineHorizontal ? (root.mainBarItems.length - 1 - visualTargetIndex) : visualTargetIndex;
+ 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;
+ }
+ if (root.useOverflowPopup)
+ 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 ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: root.toggleIconName()
+ 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
+ }
+ }
+ }
+
+ Repeater {
+ model: ScriptModel {
+ values: root.displayedInlineExpandedItems
+ objectProp: "key"
+ }
+
+ delegate: inlineExpandedTrayItemDelegate
+ }
+ }
+ }
+
+ Component {
+ id: inlineExpandedTrayItemDelegate
+
+ Item {
+ property var trayItem: modelData.item
+ property string itemKey: modelData.key
+ property string iconSource: root.trayIconSourceFor(trayItem)
+
+ width: root.isVerticalOrientation ? root.barThickness : (root.inlineExpanded ? root.trayItemSize : 0)
+ height: root.isVerticalOrientation ? (root.inlineExpanded ? root.trayItemSize : 0) : root.barThickness
+ visible: width > 0 || height > 0
+
+ Behavior on width {
+ enabled: !root.isVerticalOrientation
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Behavior on height {
+ enabled: root.isVerticalOrientation
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Rectangle {
+ id: inlineVisualContent
+ width: root.trayItemSize
+ height: root.trayItemSize
+ x: root.isVerticalOrientation ? Math.round((parent.width - width) / 2) : (root.reverseInlineHorizontal ? parent.width - width : 0)
+ y: root.isVerticalOrientation ? (root.reverseInlineVertical ? parent.height - height : 0) : Math.round((parent.height - height) / 2)
+ radius: Theme.cornerRadius
+ color: inlineTrayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
+ opacity: root.inlineExpanded ? 1 : 0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ IconImage {
+ id: inlineIconImg
+ 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: iconSource
+ asynchronous: true
+ smooth: true
+ mipmap: true
+ visible: status === Image.Ready
+ }
+
+ Text {
+ anchors.centerIn: parent
+ visible: !inlineIconImg.visible
+ text: {
+ const itemId = trayItem?.id || "";
+ if (!itemId)
+ return "?";
+ return itemId.charAt(0).toUpperCase();
+ }
+ font.pixelSize: 10
+ color: Theme.widgetTextColor
+ }
+
+ DankRipple {
+ id: inlineItemRipple
+ cornerRadius: Theme.cornerRadius
+ }
+ }
+
+ MouseArea {
+ id: inlineTrayItemArea
+ anchors.fill: parent
+ hoverEnabled: true
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+ cursorShape: Qt.PointingHandCursor
+ enabled: root.inlineExpanded
+
+ onPressed: mouse => {
+ const pos = mapToItem(inlineVisualContent, mouse.x, mouse.y);
+ inlineItemRipple.trigger(pos.x, pos.y);
+ }
+
+ onClicked: mouse => {
+ if (mouse.button === Qt.LeftButton) {
+ root.activateInlineTrayItem(trayItem, inlineVisualContent);
+ return;
+ }
+ if (mouse.button !== Qt.RightButton)
+ return;
+ root.openInlineTrayContextMenu(trayItem, inlineTrayItemArea, mouse, inlineVisualContent);
+ }
+ }
+ }
+ }
+
+ Component {
+ id: verticalMainTrayItemDelegate
+
+ Item {
+ property var trayItem: modelData.item
+ property string itemKey: modelData.key
+ property string iconSource: root.trayIconSourceFor(trayItem)
+
+ 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: 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 ? BlurService.hoverColor(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: 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 (!trayItem)
+ return;
+ if (!trayItem.onlyMenu) {
+ trayItem.activate();
+ return;
+ }
+ if (!trayItem.hasMenu)
+ return;
+ if (root.useOverflowPopup)
+ root.menuOpen = false;
+ root.showForTrayItem(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 = root.draggedIndex;
+ }
+ }
+ 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;
+ root.openInlineTrayContextMenu(trayItem, trayItemArea, mouse, visualContent);
+ }
+ }
+ }
+ }
+
+ Component {
+ id: columnComp
+ Column {
+ spacing: 0
+
+ // Column lacks layoutDirection, so we use four repeaters with mutually exclusive models to control whether main items or expanded items appear above/ below the toggle button.
+ // When reverseInlineVertical is true the first and third repeaters are empty and the second and fourth are active, and vice-versa.
+ // Because items are swapped between repeaters rather than reversed within a single list, vertical drag-and-drop indices don't need remapping (unlike the horizontal RightToLeft case).
+ Repeater {
+ model: ScriptModel {
+ values: root.reverseInlineVertical ? [] : root.displayedMainBarItems
+ objectProp: "key"
+ }
+ delegate: verticalMainTrayItemDelegate
+ }
+
+ Repeater {
+ model: ScriptModel {
+ values: root.reverseInlineVertical ? root.displayedInlineExpandedItems : []
+ objectProp: "key"
+ }
+ delegate: inlineExpandedTrayItemDelegate
+ }
+
+ 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 ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: root.toggleIconName()
+ 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
+ }
+ }
+ }
+
+ Repeater {
+ model: ScriptModel {
+ values: root.reverseInlineVertical ? [] : root.displayedInlineExpandedItems
+ objectProp: "key"
+ }
+ delegate: inlineExpandedTrayItemDelegate
+ }
+
+ Repeater {
+ model: ScriptModel {
+ values: root.reverseInlineVertical ? root.displayedMainBarItems : []
+ objectProp: "key"
+ }
+ delegate: verticalMainTrayItemDelegate
+ }
+ }
+ }
+
+ PanelWindow {
+ id: overflowMenu
+
+ WindowBlur {
+ targetWindow: overflowMenu
+ blurX: menuContainer.x
+ blurY: menuContainer.y
+ blurWidth: root.menuOpen ? menuContainer.width : 0
+ blurHeight: root.menuOpen ? menuContainer.height : 0
+ blurRadius: Theme.cornerRadius
+ }
+
+ visible: root.useOverflowPopup && 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.useOverflowPopup && root.menuOpen
+ }
+
+ Connections {
+ target: PopoutManager
+ function onPopoutOpening() {
+ if (root.useOverflowPopup)
+ 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)
+
+ 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: Theme.elevationLevel3
+ direction: root.effectiveShadowDirection
+ fallbackOffset: 6
+ targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ targetRadius: Theme.cornerRadius
+ sourceRect.antialiasing: true
+ sourceRect.smooth: true
+ shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled
+ 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
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ color: "transparent"
+ radius: Theme.cornerRadius
+ border.color: BlurService.borderColor
+ border.width: BlurService.borderWidth
+ z: 100
+ }
+
+ 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: root.trayIconSourceFor(trayItem)
+
+ width: root.trayItemSize + 4
+ height: root.trayItemSize + 4
+ radius: Theme.cornerRadius
+ color: itemArea.containsMouse ? BlurService.hoverColor(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
+
+ WindowBlur {
+ targetWindow: menuWindow
+ blurX: trayMenuContainer.x
+ blurY: trayMenuContainer.y
+ blurWidth: menuRoot.showMenu ? trayMenuContainer.width : 0
+ blurHeight: menuRoot.showMenu ? trayMenuContainer.height : 0
+ blurRadius: Theme.cornerRadius
+ }
+
+ 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();
+ if (root.useOverflowPopup)
+ 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 < trayMenuContainer.x || clickX > trayMenuContainer.x + trayMenuContainer.width || clickY < trayMenuContainer.y || clickY > trayMenuContainer.y + trayMenuContainer.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: trayMenuContainer
+
+ 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)
+
+ 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: Theme.elevationLevel3
+ direction: root.effectiveShadowDirection
+ fallbackOffset: 6
+ targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ targetRadius: Theme.cornerRadius
+ sourceRect.antialiasing: true
+ shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled
+ layer.smooth: true
+ layer.textureSize: Qt.size(Math.round(width * menuWindow.dpr), Math.round(height * menuWindow.dpr))
+ layer.textureMirroring: ShaderEffectSource.MirrorVertically
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ color: "transparent"
+ radius: Theme.cornerRadius
+ border.color: BlurService.borderColor
+ border.width: BlurService.borderWidth
+ z: 100
+ }
+
+ 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 ? BlurService.hoverColor(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 ? BlurService.hoverColor(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 ? BlurService.hoverColor(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);
+ }
+}