From 2bc6dfb2b42dfecb3da7bb37218849894bd99323 Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Tue, 11 Nov 2025 10:21:55 -0500 Subject: [PATCH] Tray: refactoring - back to dropdown menu while keeping the drawer for unpinned. --- Modules/Bar/Extras/TrayMenu.qml | 87 ++- Modules/Bar/Widgets/Tray.qml | 89 +-- .../MainScreen/Backgrounds/AllBackgrounds.qml | 7 - Modules/MainScreen/MainScreen.qml | 59 +- Modules/Panels/Tray/TrayDrawerPanel.qml | 91 ++- Modules/Panels/Tray/TrayMenuPanel.qml | 710 ------------------ Services/UI/BarWidgetRegistry.qml | 2 +- 7 files changed, 199 insertions(+), 846 deletions(-) delete mode 100644 Modules/Panels/Tray/TrayMenuPanel.qml diff --git a/Modules/Bar/Extras/TrayMenu.qml b/Modules/Bar/Extras/TrayMenu.qml index 5cb21226..be742ae0 100644 --- a/Modules/Bar/Extras/TrayMenu.qml +++ b/Modules/Bar/Extras/TrayMenu.qml @@ -7,17 +7,38 @@ import qs.Widgets PopupWindow { id: root - property QsMenuHandle menu + property var trayItem: null 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 + // Derive menu from trayItem (only used for non-submenus) + readonly property QsMenuHandle menu: isSubMenu ? null : (trayItem ? trayItem.menu : null) + + // Compute if current tray item is pinned + readonly property bool isPinned: { + if (!trayItem || widgetSection === "" || widgetIndex < 0) + return false + var widgets = Settings.data.bar.widgets[widgetSection] + if (!widgets || widgetIndex >= widgets.length) + return false + var widgetSettings = widgets[widgetIndex] + if (!widgetSettings || widgetSettings.id !== "Tray") + return false + var pinnedList = widgetSettings.pinned || [] + const itemName = trayItem.tooltipTitle || trayItem.name || trayItem.id || "" + for (var i = 0; i < pinnedList.length; i++) { + if (pinnedList[i] === itemName) + return true + } + return false + } + readonly property int menuWidth: 180 implicitWidth: menuWidth @@ -283,9 +304,9 @@ PopupWindow { Rectangle { Layout.preferredWidth: parent.width Layout.preferredHeight: 28 - color: addToFavoriteMouseArea.containsMouse ? Qt.alpha(Color.mPrimary, 0.2) : Qt.alpha(Color.mPrimary, 0.08) + color: pinUnpinMouseArea.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.color: Qt.alpha(Color.mPrimary, pinUnpinMouseArea.containsMouse ? 0.4 : 0.2) border.width: Style.borderS RowLayout { @@ -295,7 +316,7 @@ PopupWindow { spacing: Style.marginS NIcon { - icon: "pin" //addToFavoriteEntry.isFavorite ? "unpin" : "pin" + icon: root.isPinned ? "unpin" : "pin" pointSize: Style.fontSizeS applyUiScale: false verticalAlignment: Text.AlignVCenter @@ -305,7 +326,7 @@ PopupWindow { NText { Layout.fillWidth: true color: Color.mPrimary - text: addToFavoriteEntry.isFavorite ? I18n.tr("settings.bar.tray.unpin-application") : I18n.tr("settings.bar.tray.pin-application") + text: root.isPinned ? I18n.tr("settings.bar.tray.unpin-application") : I18n.tr("settings.bar.tray.pin-application") pointSize: Style.fontSizeS font.weight: Font.Medium verticalAlignment: Text.AlignVCenter @@ -314,87 +335,81 @@ PopupWindow { } MouseArea { - id: addToFavoriteMouseArea + id: pinUnpinMouseArea anchors.fill: parent hoverEnabled: true onClicked: { - if (addToFavoriteEntry.isFavorite) { - root.removeFromFavorites() + if (root.isPinned) { + root.removeFromPinned() } else { - root.addToFavorites() + root.addToPinned() } - root.close() } } } } } - function addToFavorites() { + function addToPinned() { if (!trayItem || widgetSection === "" || widgetIndex < 0) { - Logger.w("TrayMenu", "Cannot add as favorite: missing tray item or widget info") + Logger.w("TrayMenu", "Cannot pin: 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") + Logger.w("TrayMenu", "Cannot pin: 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") + Logger.w("TrayMenu", "Cannot pin: 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") + Logger.w("TrayMenu", "Cannot pin: widget is not a Tray widget") return } - var favorites = widgetSettings.favorites || [] - var newFavorites = favorites.slice() - newFavorites.push(itemName) + var pinnedList = widgetSettings.pinned || [] + var newPinned = pinnedList.slice() + newPinned.push(itemName) var newSettings = Object.assign({}, widgetSettings) - newSettings.favorites = newFavorites + newSettings.pinned = newPinned 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() { + function removeFromPinned() { if (!trayItem || widgetSection === "" || widgetIndex < 0) { - Logger.w("TrayMenu", "Cannot remove from favorites: missing tray item or widget info") + Logger.w("TrayMenu", "Cannot unpin: 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") + Logger.w("TrayMenu", "Cannot unpin: 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") + Logger.w("TrayMenu", "Cannot unpin: 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") + Logger.w("TrayMenu", "Cannot unpin: 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 pinnedList = widgetSettings.pinned || [] + var newPinned = [] + for (var i = 0; i < pinnedList.length; i++) { + if (pinnedList[i] !== itemName) { + newPinned.push(pinnedList[i]) } } var newSettings = Object.assign({}, widgetSettings) - newSettings.favorites = newFavorites + newSettings.pinned = newPinned 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 e81b2296..4c90e332 100644 --- a/Modules/Bar/Widgets/Tray.qml +++ b/Modules/Bar/Widgets/Tray.qml @@ -15,6 +15,18 @@ Rectangle { property ShellScreen screen + // Get shared tray menu window from MainScreen (via screen's parent MainScreen) + readonly property var trayMenuWindow: { + // Access via PanelService to get the MainScreen that contains the trayMenuWindow + if (!screen) + return null + // Get any panel from this screen to access its parent MainScreen + const drawerPanel = PanelService.getPanel("trayDrawerPanel", screen) + return drawerPanel?.trayMenuWindow || null + } + + readonly property var trayMenu: trayMenuWindow ? trayMenuWindow.trayMenuLoader : null + // Widget properties passed from Bar.qml for per-instance settings property string widgetId: "" property string section: "" @@ -38,10 +50,10 @@ Rectangle { readonly property real iconSize: Math.round(Style.capsuleHeight * 0.65) property list blacklist: widgetSettings.blacklist || widgetMetadata.blacklist || [] // Read from settings - property list favorites: widgetSettings.favorites || widgetMetadata.favorites || [] // Pinned items (shown inline) + property list pinned: widgetSettings.pinned || widgetMetadata.pinned || [] // Pinned items (shown inline) property bool drawerEnabled: widgetSettings.drawerEnabled !== undefined ? widgetSettings.drawerEnabled : (widgetMetadata.drawerEnabled !== undefined ? widgetMetadata.drawerEnabled : true) // Enable drawer panel - property var filteredItems: [] // Items to show inline (pinned/favorites) - property var dropdownItems: [] // Items to show in drawer (unpinned/non-favorites) + property var filteredItems: [] // Items to show inline (pinned) + property var dropdownItems: [] // Items to show in drawer (unpinned) // Debounce timer for updateFilteredItems to prevent excessive calls // when multiple events (e.g., SystemTray changes, settings saves) @@ -89,41 +101,41 @@ Rectangle { filteredItems = newItems dropdownItems = [] } else { - // Build inline (pinned/favorites) and drawer (unpinned/non-favorites) lists - // If favorites list is empty, all items go to drawer (none inline) - // If favorites list has items, favorites are inline, rest go to drawer - if (favorites && favorites.length > 0) { - let fav = [] + // Build inline (pinned) and drawer (unpinned) lists + // If pinned list is empty, all items go to drawer (none inline) + // If pinned list has items, pinned items are inline, rest go to drawer + if (pinned && pinned.length > 0) { + let pinnedItems = [] for (var k = 0; k < newItems.length; k++) { const item2 = newItems[k] const title2 = item2.tooltipTitle || item2.name || item2.id || "" - for (var m = 0; m < favorites.length; m++) { - const rule2 = favorites[m] + for (var m = 0; m < pinned.length; m++) { + const rule2 = pinned[m] if (wildCardMatch(title2, rule2)) { - fav.push(item2) + pinnedItems.push(item2) break } } } - filteredItems = fav + filteredItems = pinnedItems - // Non-favorites (unpinned) go to drawer - let nonFav = [] + // Unpinned items go to drawer + let unpinnedItems = [] for (var v = 0; v < newItems.length; v++) { const cand = newItems[v] - let isFavorite = false + let isPinned = false for (var f = 0; f < filteredItems.length; f++) { if (filteredItems[f] === cand) { - isFavorite = true + isPinned = true break } } - if (!isFavorite) - nonFav.push(cand) + if (!isPinned) + unpinnedItems.push(cand) } - dropdownItems = nonFav + dropdownItems = unpinnedItems } else { - // No favorites (pinned): all items go to drawer (none inline) + // No pinned items: all items go to drawer (none inline) filteredItems = [] dropdownItems = newItems } @@ -171,7 +183,7 @@ Rectangle { function onLoaded() { // When the widget is fully initialized with its props set the screen for the trayMenu - if (trayMenu.item) { + if (trayMenu && trayMenu.item) { trayMenu.item.screen = screen } } @@ -331,7 +343,6 @@ Rectangle { 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 @@ -381,38 +392,4 @@ 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" - } - } } diff --git a/Modules/MainScreen/Backgrounds/AllBackgrounds.qml b/Modules/MainScreen/Backgrounds/AllBackgrounds.qml index acb6c53a..0043bcd6 100644 --- a/Modules/MainScreen/Backgrounds/AllBackgrounds.qml +++ b/Modules/MainScreen/Backgrounds/AllBackgrounds.qml @@ -142,13 +142,6 @@ Item { backgroundColor: Color.mSurface } - // TrayMenu - PanelBackground { - panel: root.windowRoot.trayMenuPanel - shapeContainer: backgroundsShape - backgroundColor: Color.mSurface - } - // Wallpaper PanelBackground { panel: root.windowRoot.wallpaperPanel diff --git a/Modules/MainScreen/MainScreen.qml b/Modules/MainScreen/MainScreen.qml index 936246af..39786f8b 100644 --- a/Modules/MainScreen/MainScreen.qml +++ b/Modules/MainScreen/MainScreen.qml @@ -8,6 +8,7 @@ import "Backgrounds" as Backgrounds // All panels import qs.Modules.Bar +import qs.Modules.Bar.Extras import qs.Modules.Panels.Audio import qs.Modules.Panels.Battery import qs.Modules.Panels.Bluetooth @@ -41,9 +42,9 @@ PanelWindow { readonly property alias settingsPanel: settingsPanel readonly property alias setupWizardPanel: setupWizardPanel readonly property alias trayDrawerPanel: trayDrawerPanel - readonly property alias trayMenuPanel: trayMenuPanel readonly property alias wallpaperPanel: wallpaperPanel readonly property alias wifiPanel: wifiPanel + readonly property alias trayMenuWindow: trayMenuWindow Component.onCompleted: { Logger.d("MainScreen", "Initialized for screen:", screen?.name, "- Dimensions:", screen?.width, "x", screen?.height, "- Position:", screen?.x, ",", screen?.y) @@ -285,6 +286,7 @@ PanelWindow { TrayDrawerPanel { id: trayDrawerPanel screen: root.screen + trayMenuWindow: root.trayMenuWindow z: 50 Component.onCompleted: { @@ -293,17 +295,6 @@ PanelWindow { } } - TrayMenuPanel { - id: trayMenuPanel - screen: root.screen - z: 50 - - Component.onCompleted: { - objectName = "trayMenuPanel-" + (screen?.name || "unknown") - PanelService.registerPanel(trayMenuPanel) - } - } - WallpaperPanel { id: wallpaperPanel screen: root.screen @@ -326,6 +317,50 @@ PanelWindow { } } + // ---------------------------------------------- + // Shared TrayMenu window for context menus (used by both Tray widget and TrayDrawerPanel) + PanelWindow { + id: trayMenuWindow + anchors.top: true + anchors.left: true + anchors.right: true + anchors.bottom: true + visible: false + color: Color.transparent + screen: root.screen + + // Expose the trayMenu Loader directly + readonly property alias trayMenuLoader: trayMenu + + function open() { + visible = true + } + + function close() { + visible = false + if (trayMenu.item) { + trayMenu.item.hideMenu() + } + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onClicked: trayMenuWindow.close() + } + + Loader { + id: trayMenu + source: "../Bar/Extras/TrayMenu.qml" + onLoaded: { + if (item) { + item.screen = root.screen + } + } + } + } + + // ---------------------------------------------- // Bar placeholder - just for background positioning (actual bar content is in BarContentWindow) Item { id: barPlaceholder diff --git a/Modules/Panels/Tray/TrayDrawerPanel.qml b/Modules/Panels/Tray/TrayDrawerPanel.qml index 20db163a..38f0323a 100644 --- a/Modules/Panels/Tray/TrayDrawerPanel.qml +++ b/Modules/Panels/Tray/TrayDrawerPanel.qml @@ -12,6 +12,9 @@ import qs.Modules.MainScreen SmartPanel { id: root + // Shared tray menu window (set by MainScreen) + property var trayMenuWindow: null + // Widget info for menu functionality property string widgetSection: "" property int widgetIndex: -1 @@ -34,8 +37,8 @@ SmartPanel { return settings } - // Read favorites directly from settings for reactivity - readonly property var favoritesList: widgetSettings.favorites || [] + // Read pinned list directly from settings for reactivity + readonly property var pinnedList: widgetSettings.pinned || [] function wildCardMatch(str, rule) { if (!str || !rule) @@ -49,22 +52,22 @@ SmartPanel { } } - function isFavorite(item) { - if (!favoritesList || favoritesList.length === 0) + function isPinned(item) { + if (!pinnedList || pinnedList.length === 0) return false const title = item?.tooltipTitle || item?.name || item?.id || "" - for (var i = 0; i < favoritesList.length; i++) { - if (wildCardMatch(title, favoritesList[i])) + for (var i = 0; i < pinnedList.length; i++) { + if (wildCardMatch(title, pinnedList[i])) return true } return false } // Dynamic sizing based on item count - // Show items that are NOT favorites (unpinned items go to drawer) + // Show items that are NOT pinned (unpinned items go to drawer) readonly property var trayValuesAll: (SystemTray.items && SystemTray.items.values) ? SystemTray.items.values : [] readonly property var trayValues: trayValuesAll.filter(function (it) { - return !root.isFavorite(it) + return !root.isPinned(it) }) readonly property int itemCount: trayValues.length readonly property int maxColumns: 8 @@ -84,11 +87,21 @@ SmartPanel { Connections { target: Settings function onSettingsSaved() { - // Force refresh by incrementing settingsVersion, which triggers recalculation of favoritesList + // Force refresh by incrementing settingsVersion, which triggers recalculation of pinnedList root.settingsVersion++ } } + // Auto-close drawer when all items are pinned (drawer becomes empty) + onTrayValuesChanged: { + if (visible && trayValues.length === 0) { + close() + } + } + + // Get the trayMenu Loader from the shared window + readonly property var trayMenu: trayMenuWindow ? trayMenuWindow.trayMenuLoader : null + panelContent: Item { id: content @@ -144,22 +157,49 @@ SmartPanel { onClicked: mouse => { if (!modelData) return - if (mouse.button === Qt.RightButton && modelData.hasMenu && modelData.menu) { - const panel = PanelService.getPanel("trayMenuPanel", root.screen) - if (panel) { - panel.menu = modelData.menu - panel.trayItem = modelData - panel.widgetSection = root.widgetSection - panel.widgetIndex = root.widgetIndex - panel.openAt(trayIcon) + + if (mouse.button === Qt.LeftButton) { + // Left click: activate tray item + if (!modelData.onlyMenu) { + modelData.activate() } - } else if (mouse.button === Qt.LeftButton) { - modelData.activate() - // Close the drawer after activation - PanelService.getPanel("trayDrawerPanel", root.screen)?.close() } else if (mouse.button === Qt.MiddleButton) { - modelData.secondaryActivate() - PanelService.getPanel("trayDrawerPanel", root.screen)?.close() + // Middle click: activate with middle button + modelData.activate(1) + } else if (mouse.button === Qt.RightButton) { + // Right click: open context menu + TooltipService.hideImmediately() + + // Close menu if already visible + if (trayMenuWindow && trayMenuWindow.visible) { + trayMenuWindow.close() + return + } + + if (modelData.hasMenu && modelData.menu && trayMenu.item) { + trayMenuWindow.open() + + // Position menu at the tray icon + const barPosition = Settings.data.bar.position + let menuX, menuY + + if (barPosition === "left") { + menuX = trayIcon.width + Style.marginM + menuY = 0 + } else if (barPosition === "right") { + menuX = -trayMenu.item.width - Style.marginM + menuY = 0 + } else { + // Horizontal bars + menuX = (trayIcon.width / 2) - (trayMenu.item.width / 2) + menuY = trayIcon.height + Style.marginS + } + + trayMenu.item.trayItem = modelData + trayMenu.item.widgetSection = root.widgetSection + trayMenu.item.widgetIndex = root.widgetIndex + trayMenu.item.showAt(trayIcon, menuX, menuY) + } } } @@ -170,7 +210,10 @@ SmartPanel { modelData?.scrollDown() } - onEntered: TooltipService.show(Screen, trayIcon, modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item", BarService.getTooltipDirection()) + onEntered: { + trayMenuWindow.close() + TooltipService.show(Screen, trayIcon, modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item", BarService.getTooltipDirection()) + } onExited: TooltipService.hide() } } diff --git a/Modules/Panels/Tray/TrayMenuPanel.qml b/Modules/Panels/Tray/TrayMenuPanel.qml deleted file mode 100644 index 2a3f31cb..00000000 --- a/Modules/Panels/Tray/TrayMenuPanel.qml +++ /dev/null @@ -1,710 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import qs.Commons -import qs.Widgets -import qs.Modules.MainScreen -import qs.Services.UI - -SmartPanel { - id: root - - // Inputs - property QsMenuHandle menu - property var trayItem: null - property string widgetSection: "" - property int widgetIndex: -1 - - // Internal - property int menuWidth: 280 - readonly property int minMenuWidth: 280 - readonly property int maxMenuWidth: 600 - preferredWidth: menuWidth - // Height is content-driven via panelContent - - // Open positioned relative to button - function openAt(buttonItem) { - open(buttonItem) - } - - panelContent: Item { - id: content - - // Track currently open submenu - property var activeSubMenu: null - property var activeSubMenuEntry: null - // Only show submenu in place (replace main menu) - property bool inPlaceSubmenu: true - - // Let Panel size to our content - readonly property real contentPreferredWidth: root.menuWidth - readonly property real contentPreferredHeight: { - // If showing submenu in-place, size to the submenu's flickable content height - if (activeSubMenu && inPlaceSubmenu) { - const subFlickable = inPlaceSubMenuLoader.item?.children[1] // Flickable is the second child (after MouseArea) - const subHeight = subFlickable?.contentHeight || 0 - return Math.min(root.screen ? root.screen.height * 0.9 : Screen.height * 0.9, subHeight + (Style.marginS * 2)) - } - - const mainHeight = mainFlickable.contentHeight - return Math.min(root.screen ? root.screen.height * 0.9 : Screen.height * 0.9, mainHeight + (Style.marginS * 2)) - } - - QsMenuOpener { - id: opener - menu: root.menu - onChildrenChanged: Qt.callLater(() => calculateMenuWidth(opener)) - } - - // Text metrics for measuring text widths - TextMetrics { - id: textMetrics - font.pointSize: Style.fontSizeS - } - - // Watch for submenu changes - onActiveSubMenuChanged: { - if (activeSubMenu && inPlaceSubmenu) { - Qt.callLater(() => { - // Find the subMenuOpener in the loaded component - if (inPlaceSubMenuLoader.item) { - const flickable = inPlaceSubMenuLoader.item.children[1] // Flickable - if (flickable && flickable.children.length > 0) { - const columnLayout = flickable.children[0] - if (columnLayout && columnLayout.children.length > 0) { - const subOpener = columnLayout.children[0] - if (subOpener) { - calculateMenuWidth(subOpener) - } - } - } - } - }) - } else if (!activeSubMenu) { - // Reset to main menu width when submenu closes - Qt.callLater(() => calculateMenuWidth(opener)) - } - } - - // Calculate required menu width based on menu entries - function calculateMenuWidth(menuOpener) { - let maxWidth = root.minMenuWidth - - // For submenus, also measure the "Back" button - if (menuOpener !== opener && content.inPlaceSubmenu) { - textMetrics.text = I18n.tr("settings.bar.tray.back") - const backWidth = (Style.marginM * 4) + Style.fontSizeS + Style.marginS + textMetrics.width - maxWidth = Math.max(maxWidth, backWidth) - } - - // Check all menu entries - if (menuOpener && menuOpener.children) { - try { - const entries = menuOpener.children.values ? [...menuOpener.children.values] : [] - - for (var i = 0; i < entries.length; i++) { - const entry = entries[i] - if (!entry || entry.isSeparator) - continue - - const text = entry.text || "" - if (text === "") - continue - - // Measure the text - textMetrics.text = text.replace(/[\n\r]+/g, ' ') - const textWidth = textMetrics.width - - // Calculate total width: - // - Outer inset: Style.marginM * 2 (parent width reduction) - // - RowLayout margins: Style.marginM * 2 (left + right) - // - Text width - // - Spacing: Style.marginS - // - Icon: Style.marginL (if present) - // - Submenu arrow: ~20px (if present) - let requiredWidth = (Style.marginM * 4) + textWidth - - if (entry.icon && entry.icon !== "") { - requiredWidth += Style.marginS + Style.marginL - } - - if (entry.hasChildren) { - requiredWidth += Style.marginS + 20 - } - - maxWidth = Math.max(maxWidth, requiredWidth) - } - } catch (e) { - - // Silently ignore errors during width calculation - } - } - - // Check pin/unpin button (only for main menu) - if (menuOpener === opener && root.trayItem !== null && root.widgetSection !== "" && root.widgetIndex >= 0) { - textMetrics.text = I18n.tr("settings.bar.tray.pin-application") - let pinWidth = (Style.marginM * 4) + textMetrics.width + Style.marginS + 20 - maxWidth = Math.max(maxWidth, pinWidth) - - textMetrics.text = I18n.tr("settings.bar.tray.unpin-application") - let unpinWidth = (Style.marginM * 4) + textMetrics.width + Style.marginS + 20 - maxWidth = Math.max(maxWidth, unpinWidth) - } - - // Clamp to min/max and apply - const finalWidth = Math.min(root.maxMenuWidth, Math.max(root.minMenuWidth, Math.ceil(maxWidth))) - root.menuWidth = finalWidth - } - - Component.onCompleted: Qt.callLater(() => calculateMenuWidth(opener)) - - Component { - id: subMenuComponent - - Item { - id: subMenuContainer - - // MouseArea to track hover for submenu (covers entire submenu area) - MouseArea { - id: subMenuMouseArea - anchors.fill: parent - hoverEnabled: true - enabled: content.activeSubMenu !== null && !content.inPlaceSubmenu - visible: !content.inPlaceSubmenu - z: 1 - acceptedButtons: Qt.NoButton // Don't intercept clicks, just track hover - onExited: { - if (content.inPlaceSubmenu) - return - Qt.callLater(() => { - if (!subMenuMouseArea.containsMouse && content.activeSubMenuEntry) { - // Find the mouseArea in the entry (it's in the second Rectangle's MouseArea) - let entryMouseArea = null - for (var i = 0; i < content.activeSubMenuEntry.children.length; i++) { - const child = content.activeSubMenuEntry.children[i] - if (child && child.children && child.children.length > 0) { - for (var j = 0; j < child.children.length; j++) { - const grandchild = child.children[j] - if (grandchild && grandchild.hoverEnabled !== undefined) { - entryMouseArea = grandchild - break - } - } - } - if (entryMouseArea) - break - } - if (!entryMouseArea || !entryMouseArea.containsMouse) { - content.activeSubMenu = null - content.activeSubMenuEntry = null - } - } - }) - } - } - - Flickable { - id: subMenuFlickable - anchors.fill: parent - contentHeight: subMenuColumnLayout.implicitHeight - interactive: false - z: 0 - - ColumnLayout { - id: subMenuColumnLayout - width: subMenuFlickable.width - spacing: 0 - - QsMenuOpener { - id: subMenuOpener - menu: content.activeSubMenu - onChildrenChanged: Qt.callLater(() => content.calculateMenuWidth(subMenuOpener)) - } - - // Back button (only shown when submenu is in-place) - Rectangle { - id: backEntry - visible: content.inPlaceSubmenu - Layout.preferredWidth: parent.width - Layout.preferredHeight: visible ? 28 : 0 - color: Color.transparent - - Rectangle { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.topMargin: 0 - anchors.bottomMargin: 0 - anchors.horizontalCenter: parent.horizontalCenter - width: parent.width - (Style.marginM * 2) - color: backMouseArea.containsMouse ? Color.mHover : Color.transparent - radius: Style.radiusS - - RowLayout { - anchors.fill: parent - anchors.leftMargin: Style.marginM - anchors.rightMargin: Style.marginM - spacing: Style.marginS - - NIcon { - icon: "arrow-left" - pointSize: Style.fontSizeS - applyUiScale: false - verticalAlignment: Text.AlignVCenter - color: backMouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface - } - - NText { - Layout.fillWidth: true - color: backMouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface - text: I18n.tr("settings.bar.tray.back") - pointSize: Style.fontSizeS - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - } - - MouseArea { - id: backMouseArea - anchors.fill: parent - hoverEnabled: true - - onClicked: { - content.activeSubMenu = null - content.activeSubMenuEntry = null - } - } - } - } - - Rectangle { - visible: content.inPlaceSubmenu - Layout.preferredWidth: parent.width - Layout.preferredHeight: visible ? 8 : 0 - color: Color.transparent - - NDivider { - anchors.centerIn: parent - width: parent.width - (Style.marginM * 2) - visible: parent.visible - } - } - - Repeater { - model: subMenuOpener.children ? [...subMenuOpener.children.values] : [] - - delegate: Rectangle { - id: subEntry - required property var modelData - - Layout.preferredWidth: parent.width - Layout.preferredHeight: { - if (modelData?.isSeparator) { - return 8 - } else { - return 28 - } - } - - color: Color.transparent - - NDivider { - anchors.centerIn: parent - width: parent.width - (Style.marginM * 2) - visible: modelData?.isSeparator ?? false - } - - Rectangle { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.topMargin: 0 - anchors.bottomMargin: 0 - anchors.horizontalCenter: parent.horizontalCenter - width: parent.width - (Style.marginM * 2) - color: subMouseArea.containsMouse ? Color.mHover : 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 { - Layout.fillWidth: true - color: (modelData?.enabled ?? true) ? (subMouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface) : Color.mOnSurfaceVariant - text: modelData?.text !== "" ? modelData?.text.replace(/[\n\r]+/g, ' ') : "..." - pointSize: Style.fontSizeS - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - - 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 - Layout.rightMargin: Style.marginL - color: (subMouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface) - } - } - - MouseArea { - id: subMouseArea - anchors.fill: parent - hoverEnabled: true - enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) - - onClicked: { - if (!modelData || modelData.isSeparator) - return - - if (modelData.hasChildren) { - // Toggle nested submenu on click - if (content.activeSubMenu === modelData) { - // If already open, close it on second click - content.activeSubMenu = null - content.activeSubMenuEntry = null - return - } - - // Open nested submenu in place - content.activeSubMenu = modelData - content.activeSubMenuEntry = null // No entry for nested submenus - content.inPlaceSubmenu = true - } else { - modelData.triggered() - root.close() - } - } - } - } - } - } - } - } - } - } - - // Main menu and in-place submenu - RowLayout { - id: rowLayout - anchors.fill: parent - anchors.topMargin: Style.marginS - spacing: 0 - - // Submenu replacing main menu (in-place) - Loader { - id: inPlaceSubMenuLoader - Layout.preferredWidth: (content.activeSubMenu && content.inPlaceSubmenu) ? root.menuWidth : 0 - Layout.fillHeight: true - visible: content.activeSubMenu !== null && content.inPlaceSubmenu - sourceComponent: subMenuComponent - } - - // Main menu - Flickable { - id: mainFlickable - Layout.preferredWidth: root.menuWidth - Layout.fillHeight: true - contentHeight: mainColumnLayout.implicitHeight - interactive: false - visible: !(content.activeSubMenu !== null && content.inPlaceSubmenu) - - ColumnLayout { - id: mainColumnLayout - width: mainFlickable.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 { - return 28 - } - } - - color: Color.transparent - - NDivider { - anchors.centerIn: parent - width: parent.width - Style.marginL - visible: modelData?.isSeparator ?? false - } - - Rectangle { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.topMargin: 0 - anchors.bottomMargin: 0 - anchors.horizontalCenter: parent.horizontalCenter - width: parent.width - (Style.marginM * 2) - color: mouseArea.containsMouse ? Color.mHover : 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.mOnHover : Color.mOnSurface) : Color.mOnSurfaceVariant - text: modelData?.text !== "" ? modelData?.text.replace(/[\n\r]+/g, ' ') : "..." - pointSize: Style.fontSizeS - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - - 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 - Layout.rightMargin: Style.marginS - color: (mouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface) - } - } - - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) - - onClicked: { - if (!modelData || modelData.isSeparator) - return - - if (modelData.hasChildren) { - // Toggle submenu on click - if (content.activeSubMenuEntry === entry && content.inPlaceSubmenu) { - // If already open in-place, close it on second click - content.activeSubMenu = null - content.activeSubMenuEntry = null - return - } - - // Close any other open submenu - if (content.activeSubMenuEntry && content.activeSubMenuEntry !== entry) { - content.activeSubMenu = null - content.activeSubMenuEntry = null - } - - // Open submenu in place (replace main menu) - content.activeSubMenu = modelData - content.activeSubMenuEntry = entry - content.inPlaceSubmenu = true - } else { - modelData.triggered() - root.close() - } - } - - onExited: { - - } - } - } - } - } - - Rectangle { - visible: root.trayItem !== null && root.widgetSection !== "" && root.widgetIndex >= 0 - Layout.preferredWidth: parent.width - Layout.preferredHeight: visible ? 8 : 0 - color: Color.transparent - - NDivider { - anchors.centerIn: parent - width: parent.width - Style.marginL - visible: parent.visible - } - } - - Rectangle { - id: addToFavoriteEntry - visible: root.trayItem !== null && root.widgetSection !== "" && root.widgetIndex >= 0 - Layout.preferredWidth: parent.width - Layout.preferredHeight: visible ? 28 : 0 - color: Color.transparent - - readonly property bool isFavorite: { - if (!root.trayItem || root.widgetSection === "" || root.widgetIndex < 0) - return false - const itemName = root.trayItem.tooltipTitle || root.trayItem.name || root.trayItem.id || "" - if (!itemName) - return false - var widgets = Settings.data.bar.widgets[root.widgetSection] - if (!widgets || root.widgetIndex >= widgets.length) - return false - var widgetSettings = widgets[root.widgetIndex] - if (!widgetSettings || widgetSettings.id !== "Tray") - return false - var favorites = widgetSettings.favorites || [] - for (var i = 0; i < favorites.length; i++) { - if (favorites[i] === itemName) - return true - } - return false - } - - Rectangle { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.topMargin: 0 - anchors.bottomMargin: 0 - anchors.horizontalCenter: parent.horizontalCenter - width: parent.width - (Style.marginM * 2) - 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: 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() - } - } - } - } - } - } - } - - Keys.onEscapePressed: 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/Services/UI/BarWidgetRegistry.qml b/Services/UI/BarWidgetRegistry.qml index b5632bad..a2b4f470 100644 --- a/Services/UI/BarWidgetRegistry.qml +++ b/Services/UI/BarWidgetRegistry.qml @@ -165,7 +165,7 @@ Singleton { "allowUserSettings": true, "blacklist": [], "colorizeIcons": false, - "favorites": [], + "pinned": [], "drawerEnabled": true }, "WiFi": {