diff options
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.qml | 1893 |
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); + } +} |