From 230d5de0718ce43be3eb0c48f92521528dfe94b0 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Mon, 10 Nov 2025 21:45:47 -0500 Subject: [PATCH] wip --- Modules/Bar/Extras/TrayMenu.qml | 402 ++++++++++++++++++++++++++++++++ Modules/Bar/Widgets/Tray.qml | 100 +++++--- 2 files changed, 474 insertions(+), 28 deletions(-) create mode 100644 Modules/Bar/Extras/TrayMenu.qml diff --git a/Modules/Bar/Extras/TrayMenu.qml b/Modules/Bar/Extras/TrayMenu.qml new file mode 100644 index 00000000..5cb21226 --- /dev/null +++ b/Modules/Bar/Extras/TrayMenu.qml @@ -0,0 +1,402 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Widgets + +PopupWindow { + id: root + property QsMenuHandle menu + property var anchorItem: null + property real anchorX + property real anchorY + property bool isSubMenu: false + property bool isHovered: rootMouseArea.containsMouse + property ShellScreen screen + property var trayItem: null + property string widgetSection: "" + property int widgetIndex: -1 + + readonly property int menuWidth: 180 + + implicitWidth: menuWidth + + // Use the content height of the Flickable for implicit height + implicitHeight: Math.min(screen ? screen.height * 0.9 : Screen.height * 0.9, flickable.contentHeight + (Style.marginS * 2)) + visible: false + color: Color.transparent + anchor.item: anchorItem + anchor.rect.x: anchorX + anchor.rect.y: anchorY - (isSubMenu ? 0 : 4) + + function showAt(item, x, y) { + if (!item) { + Logger.warn("TrayMenu", "anchorItem is undefined, won't show menu.") + return + } + + if (!opener.children || opener.children.values.length === 0) { + //Logger.warn("TrayMenu", "Menu not ready, delaying show") + Qt.callLater(() => showAt(item, x, y)) + return + } + + anchorItem = item + anchorX = x + anchorY = y + + visible = true + forceActiveFocus() + + // Force update after showing. + Qt.callLater(() => { + root.anchor.updateAnchor() + }) + } + + function hideMenu() { + visible = false + + // Clean up all submenus recursively + for (var i = 0; i < columnLayout.children.length; i++) { + const child = columnLayout.children[i] + if (child?.subMenu) { + child.subMenu.hideMenu() + child.subMenu.destroy() + child.subMenu = null + } + } + } + + // Full-sized, transparent MouseArea to track the mouse. + MouseArea { + id: rootMouseArea + anchors.fill: parent + hoverEnabled: true + } + + Item { + anchors.fill: parent + Keys.onEscapePressed: root.hideMenu() + } + + QsMenuOpener { + id: opener + menu: root.menu + } + + Rectangle { + anchors.fill: parent + color: Color.mSurface + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS) + radius: Style.radiusM + } + + Flickable { + id: flickable + anchors.fill: parent + anchors.margins: Style.marginS + contentHeight: columnLayout.implicitHeight + interactive: true + + // Use a ColumnLayout to handle menu item arrangement + ColumnLayout { + id: columnLayout + width: flickable.width + spacing: 0 + + Repeater { + model: opener.children ? [...opener.children.values] : [] + + delegate: Rectangle { + id: entry + required property var modelData + + Layout.preferredWidth: parent.width + Layout.preferredHeight: { + if (modelData?.isSeparator) { + return 8 + } else { + // Calculate based on text content + const textHeight = text.contentHeight || (Style.fontSizeS * 1.2) + return Math.max(28, textHeight + (Style.marginS * 2)) + } + } + + color: Color.transparent + property var subMenu: null + + NDivider { + anchors.centerIn: parent + width: parent.width - (Style.marginM * 2) + visible: modelData?.isSeparator ?? false + } + + Rectangle { + anchors.fill: parent + color: mouseArea.containsMouse ? Color.mTertiary : Color.transparent + radius: Style.radiusS + visible: !(modelData?.isSeparator ?? false) + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.marginM + anchors.rightMargin: Style.marginM + spacing: Style.marginS + + NText { + id: text + Layout.fillWidth: true + color: (modelData?.enabled ?? true) ? (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface) : Color.mOnSurfaceVariant + text: modelData?.text !== "" ? modelData?.text.replace(/[\n\r]+/g, ' ') : "..." + pointSize: Style.fontSizeS + verticalAlignment: Text.AlignVCenter + wrapMode: Text.WordWrap + } + + Image { + Layout.preferredWidth: Style.marginL + Layout.preferredHeight: Style.marginL + source: modelData?.icon ?? "" + visible: (modelData?.icon ?? "") !== "" + fillMode: Image.PreserveAspectFit + } + + NIcon { + icon: modelData?.hasChildren ? "menu" : "" + pointSize: Style.fontSizeS + applyUiScale: false + verticalAlignment: Text.AlignVCenter + visible: modelData?.hasChildren ?? false + color: (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface) + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) && root.visible + + onClicked: { + if (modelData && !modelData.isSeparator && !modelData.hasChildren) { + modelData.triggered() + root.hideMenu() + } + } + + onEntered: { + if (!root.visible) + return + + // Close all sibling submenus + for (var i = 0; i < columnLayout.children.length; i++) { + const sibling = columnLayout.children[i] + if (sibling !== entry && sibling?.subMenu) { + sibling.subMenu.hideMenu() + sibling.subMenu.destroy() + sibling.subMenu = null + } + } + + // Create submenu if needed + if (modelData?.hasChildren) { + if (entry.subMenu) { + entry.subMenu.hideMenu() + entry.subMenu.destroy() + } + + // Need a slight overlap so that menu don't close when moving the mouse to a submenu + const submenuWidth = menuWidth // Assuming a similar width as the parent + const overlap = 4 // A small overlap to bridge the mouse path + + // Determine submenu opening direction based on bar position and available space + let openLeft = false + + // Check bar position first + const barPosition = Settings.data.bar.position + const globalPos = entry.mapToItem(null, 0, 0) + + if (barPosition === "right") { + // Bar is on the right, prefer opening submenus to the left + openLeft = true + } else if (barPosition === "left") { + // Bar is on the left, prefer opening submenus to the right + openLeft = false + } else { + // Bar is horizontal (top/bottom) or undefined, use space-based logic + openLeft = (globalPos.x + entry.width + submenuWidth > screen.width) + + // Secondary check: ensure we don't open off-screen + if (openLeft && globalPos.x - submenuWidth < 0) { + // Would open off the left edge, force right opening + openLeft = false + } else if (!openLeft && globalPos.x + entry.width + submenuWidth > screen.width) { + // Would open off the right edge, force left opening + openLeft = true + } + } + + // Position with overlap + const anchorX = openLeft ? -submenuWidth + overlap : entry.width - overlap + + // Create submenu + entry.subMenu = Qt.createComponent("TrayMenu.qml").createObject(root, { + "menu": modelData, + "anchorItem": entry, + "anchorX": anchorX, + "anchorY": 0, + "isSubMenu": true, + "screen": screen + }) + + if (entry.subMenu) { + entry.subMenu.showAt(entry, anchorX, 0) + } + } + } + + onExited: { + Qt.callLater(() => { + if (entry.subMenu && !entry.subMenu.isHovered) { + entry.subMenu.hideMenu() + entry.subMenu.destroy() + entry.subMenu = null + } + }) + } + } + } + + Component.onDestruction: { + if (subMenu) { + subMenu.destroy() + subMenu = null + } + } + } + } + + // PIN / UNPIN + Rectangle { + Layout.preferredWidth: parent.width + Layout.preferredHeight: 28 + color: addToFavoriteMouseArea.containsMouse ? Qt.alpha(Color.mPrimary, 0.2) : Qt.alpha(Color.mPrimary, 0.08) + radius: Style.radiusS + border.color: Qt.alpha(Color.mPrimary, addToFavoriteMouseArea.containsMouse ? 0.4 : 0.2) + border.width: Style.borderS + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.marginM + anchors.rightMargin: Style.marginM + spacing: Style.marginS + + NIcon { + icon: "pin" //addToFavoriteEntry.isFavorite ? "unpin" : "pin" + pointSize: Style.fontSizeS + applyUiScale: false + verticalAlignment: Text.AlignVCenter + color: Color.mPrimary + } + + NText { + Layout.fillWidth: true + color: Color.mPrimary + text: addToFavoriteEntry.isFavorite ? I18n.tr("settings.bar.tray.unpin-application") : I18n.tr("settings.bar.tray.pin-application") + pointSize: Style.fontSizeS + font.weight: Font.Medium + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + } + + MouseArea { + id: addToFavoriteMouseArea + anchors.fill: parent + hoverEnabled: true + + onClicked: { + if (addToFavoriteEntry.isFavorite) { + root.removeFromFavorites() + } else { + root.addToFavorites() + } + root.close() + } + } + } + } + } + + function addToFavorites() { + if (!trayItem || widgetSection === "" || widgetIndex < 0) { + Logger.w("TrayMenu", "Cannot add as favorite: missing tray item or widget info") + return + } + const itemName = trayItem.tooltipTitle || trayItem.name || trayItem.id || "" + if (!itemName) { + Logger.w("TrayMenu", "Cannot add as favorite: tray item has no name") + return + } + var widgets = Settings.data.bar.widgets[widgetSection] + if (!widgets || widgetIndex >= widgets.length) { + Logger.w("TrayMenu", "Cannot add as favorite: invalid widget index") + return + } + var widgetSettings = widgets[widgetIndex] + if (!widgetSettings || widgetSettings.id !== "Tray") { + Logger.w("TrayMenu", "Cannot add as favorite: widget is not a Tray widget") + return + } + var favorites = widgetSettings.favorites || [] + var newFavorites = favorites.slice() + newFavorites.push(itemName) + var newSettings = Object.assign({}, widgetSettings) + newSettings.favorites = newFavorites + widgets[widgetIndex] = newSettings + Settings.data.bar.widgets[widgetSection] = widgets + Settings.saveImmediate() + if (root.screen) { + const panel = PanelService.getPanel("trayDrawerPanel", root.screen) + if (panel) + panel.close() + } + } + + function removeFromFavorites() { + if (!trayItem || widgetSection === "" || widgetIndex < 0) { + Logger.w("TrayMenu", "Cannot remove from favorites: missing tray item or widget info") + return + } + const itemName = trayItem.tooltipTitle || trayItem.name || trayItem.id || "" + if (!itemName) { + Logger.w("TrayMenu", "Cannot remove from favorites: tray item has no name") + return + } + var widgets = Settings.data.bar.widgets[widgetSection] + if (!widgets || widgetIndex >= widgets.length) { + Logger.w("TrayMenu", "Cannot remove from favorites: invalid widget index") + return + } + var widgetSettings = widgets[widgetIndex] + if (!widgetSettings || widgetSettings.id !== "Tray") { + Logger.w("TrayMenu", "Cannot remove from favorites: widget is not a Tray widget") + return + } + var favorites = widgetSettings.favorites || [] + var newFavorites = [] + for (var i = 0; i < favorites.length; i++) { + if (favorites[i] !== itemName) { + newFavorites.push(favorites[i]) + } + } + var newSettings = Object.assign({}, widgetSettings) + newSettings.favorites = newFavorites + widgets[widgetIndex] = newSettings + Settings.data.bar.widgets[widgetSection] = widgets + Settings.saveImmediate() + } +} diff --git a/Modules/Bar/Widgets/Tray.qml b/Modules/Bar/Widgets/Tray.qml index 7f1a3921..e81b2296 100644 --- a/Modules/Bar/Widgets/Tray.qml +++ b/Modules/Bar/Widgets/Tray.qml @@ -169,6 +169,13 @@ Rectangle { } } + function onLoaded() { + // When the widget is fully initialized with its props set the screen for the trayMenu + if (trayMenu.item) { + trayMenu.item.screen = screen + } + } + Connections { target: SystemTray.items function onValuesChanged() { @@ -285,54 +292,57 @@ Rectangle { if (mouse.button === Qt.LeftButton) { // Close any open menu first - PanelService.getPanel("trayMenuPanel", root.screen)?.close() + trayMenuWindow.close() if (!modelData.onlyMenu) { modelData.activate() } } else if (mouse.button === Qt.MiddleButton) { - // Close any open menu first - PanelService.getPanel("trayMenuPanel", root.screen)?.close() - modelData.secondaryActivate && modelData.secondaryActivate() + // Close any open menu first + + // TODO RESTORE LATER + // trayMenuWindow.close() + // modelData.secondaryActivate && modelData.secondaryActivate() } else if (mouse.button === Qt.RightButton) { TooltipService.hideImmediately() // Close the menu if it was visible - const menuPanel = PanelService.getPanel("trayMenuPanel", root.screen) - if (menuPanel && menuPanel.visible) { - menuPanel.close() + if (trayMenuWindow && trayMenuWindow.visible) { + trayMenuWindow.close() return } - if (modelData.hasMenu && modelData.menu) { - const panel = PanelService.getPanel("trayMenuPanel", root.screen) - if (panel) { - panel.menu = modelData.menu - panel.trayItem = modelData - panel.widgetSection = root.section - panel.widgetIndex = root.sectionWidgetIndex - panel.openAt(parent) - // Prevent onEntered from immediately closing the panel - trayIcon.menuJustOpened = true + if (modelData.hasMenu && modelData.menu && trayMenu.item) { + trayMenuWindow.open() + + // Position menu based on bar position + let menuX, menuY + if (barPosition === "left") { + // For left bar: position menu to the right of the bar + menuX = width + Style.marginM + menuY = 0 + } else if (barPosition === "right") { + // For right bar: position menu to the left of the bar + menuX = -trayMenu.item.width - Style.marginM + menuY = 0 } else { - Logger.i("Tray", "TrayMenu not available") + // For horizontal bars: center horizontally and position below + menuX = (width / 2) - (trayMenu.item.width / 2) + menuY = Style.barHeight } + trayMenu.item.menu = modelData.menu + trayMenu.item.trayItem = modelData + trayMenu.item.widgetSection = root.section + trayMenu.item.widgetIndex = root.sectionWidgetIndex + trayMenu.item.showAt(parent, menuX, menuY) } else { - Logger.i("Tray", "No menu available for", modelData.id, "or trayMenu not set") + Logger.d("Tray", "No menu available for", modelData.id, "or trayMenu not set") } } } onEntered: { - // Don't close menu immediately after opening it - if (!trayIcon.menuJustOpened) { - // Only close the menu if we're hovering over a DIFFERENT tray icon - const menuPanel = PanelService.getPanel("trayMenuPanel", root.screen) - if (menuPanel && menuPanel.trayItem !== modelData) { - menuPanel.close() - } - } - trayIcon.menuJustOpened = false + trayMenuWindow.close() TooltipService.show(Screen, trayIcon, modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item", BarService.getTooltipDirection()) } onExited: TooltipService.hide() @@ -371,4 +381,38 @@ Rectangle { onRightClicked: toggleDrawer(this) } } + + // -------------------------- + PanelWindow { + id: trayMenuWindow + anchors.top: true + anchors.left: true + anchors.right: true + anchors.bottom: true + visible: false + color: Color.transparent + screen: screen + + function open() { + visible = true + } + + function close() { + visible = false + if (trayMenu.item) { + trayMenu.item.hideMenu() + } + } + + // Clicking outside of the rectangle to close + MouseArea { + anchors.fill: parent + onClicked: trayMenuWindow.close() + } + + Loader { + id: trayMenu + source: "../Extras/TrayMenu.qml" + } + } }