mirror of
https://github.com/zoriya/noctalia-shell.git
synced 2025-12-06 06:36:15 +00:00
BarWidgets: First pass on contextual widget menu accessible from right-click.
Testing on volume widget for now.
This commit is contained in:
@@ -387,6 +387,11 @@
|
||||
"title": "Bluetooth"
|
||||
}
|
||||
},
|
||||
"context-menu": {
|
||||
"open-mixer": "Audio mixer",
|
||||
"toggle-mute": "Toggle mute",
|
||||
"widget-settings": "Widget settings"
|
||||
},
|
||||
"calendar": {
|
||||
"panel": {
|
||||
"week": "Week"
|
||||
|
||||
@@ -98,13 +98,6 @@ PopupWindow {
|
||||
}
|
||||
}
|
||||
|
||||
// Full-sized, transparent MouseArea to track the mouse.
|
||||
MouseArea {
|
||||
id: rootMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
Keys.onEscapePressed: root.hideMenu()
|
||||
@@ -186,7 +179,7 @@ PopupWindow {
|
||||
Rectangle {
|
||||
id: innerRect
|
||||
anchors.fill: parent
|
||||
color: mouseArea.containsMouse ? Color.mTertiary : Color.transparent
|
||||
color: mouseArea.containsMouse ? Color.mHover : Color.transparent
|
||||
radius: Style.radiusS
|
||||
visible: !(modelData?.isSeparator ?? false)
|
||||
|
||||
@@ -199,7 +192,7 @@ PopupWindow {
|
||||
NText {
|
||||
id: text
|
||||
Layout.fillWidth: true
|
||||
color: (modelData?.enabled ?? true) ? (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface) : Color.mOnSurfaceVariant
|
||||
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
|
||||
|
||||
@@ -16,22 +16,22 @@ Rectangle {
|
||||
property ShellScreen screen
|
||||
|
||||
// Trigger re-evaluation when window is registered
|
||||
property int trayMenuUpdateTrigger: 0
|
||||
property int popupMenuUpdateTrigger: 0
|
||||
|
||||
// Get shared tray menu window from PanelService (reactive to trigger changes)
|
||||
readonly property var trayMenuWindow: {
|
||||
// Get shared popup menu window from PanelService (reactive to trigger changes)
|
||||
readonly property var popupMenuWindow: {
|
||||
// Reference trigger to force re-evaluation
|
||||
var _ = trayMenuUpdateTrigger;
|
||||
return PanelService.getTrayMenuWindow(screen);
|
||||
var _ = popupMenuUpdateTrigger;
|
||||
return PanelService.getPopupMenuWindow(screen);
|
||||
}
|
||||
|
||||
readonly property var trayMenu: trayMenuWindow ? trayMenuWindow.trayMenuLoader : null
|
||||
readonly property var trayMenu: popupMenuWindow ? popupMenuWindow.trayMenuLoader : null
|
||||
|
||||
Connections {
|
||||
target: PanelService
|
||||
function onTrayMenuWindowRegistered(registeredScreen) {
|
||||
function onPopupMenuWindowRegistered(registeredScreen) {
|
||||
if (registeredScreen === screen) {
|
||||
root.trayMenuUpdateTrigger++;
|
||||
root.popupMenuUpdateTrigger++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,9 +183,9 @@ Rectangle {
|
||||
function toggleDrawer(button) {
|
||||
TooltipService.hideImmediately();
|
||||
|
||||
// Close the tray menu if it's open
|
||||
if (trayMenuWindow && trayMenuWindow.visible) {
|
||||
trayMenuWindow.close();
|
||||
// Close the popup menu if it's open
|
||||
if (popupMenuWindow && popupMenuWindow.visible) {
|
||||
popupMenuWindow.close();
|
||||
}
|
||||
|
||||
const panel = PanelService.getPanel("trayDrawerPanel", root.screen);
|
||||
@@ -320,8 +320,8 @@ Rectangle {
|
||||
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
// Close any open menu first
|
||||
if (trayMenuWindow) {
|
||||
trayMenuWindow.close();
|
||||
if (popupMenuWindow) {
|
||||
popupMenuWindow.close();
|
||||
}
|
||||
|
||||
if (!modelData.onlyMenu) {
|
||||
@@ -329,8 +329,8 @@ Rectangle {
|
||||
}
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
// Close the menu if it was visible
|
||||
if (trayMenuWindow && trayMenuWindow.visible) {
|
||||
trayMenuWindow.close();
|
||||
if (popupMenuWindow && popupMenuWindow.visible) {
|
||||
popupMenuWindow.close();
|
||||
return;
|
||||
}
|
||||
modelData.secondaryActivate && modelData.secondaryActivate();
|
||||
@@ -338,8 +338,8 @@ Rectangle {
|
||||
TooltipService.hideImmediately();
|
||||
|
||||
// Close the menu if it was visible
|
||||
if (trayMenuWindow && trayMenuWindow.visible) {
|
||||
trayMenuWindow.close();
|
||||
if (popupMenuWindow && popupMenuWindow.visible) {
|
||||
popupMenuWindow.close();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -348,8 +348,8 @@ Rectangle {
|
||||
PanelService.openedPanel.close();
|
||||
}
|
||||
|
||||
if (modelData.hasMenu && modelData.menu && trayMenuWindow && trayMenu && trayMenu.item) {
|
||||
trayMenuWindow.open();
|
||||
if (modelData.hasMenu && modelData.menu && popupMenuWindow && trayMenu && trayMenu.item) {
|
||||
popupMenuWindow.open();
|
||||
|
||||
// Position menu based on bar position
|
||||
let menuX, menuY;
|
||||
@@ -376,8 +376,8 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
onEntered: {
|
||||
if (trayMenuWindow) {
|
||||
trayMenuWindow.close();
|
||||
if (popupMenuWindow) {
|
||||
popupMenuWindow.close();
|
||||
}
|
||||
TooltipService.show(screen, trayIcon, modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item", BarService.getTooltipDirection());
|
||||
}
|
||||
|
||||
@@ -58,6 +58,10 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
function openExternalMixer() {
|
||||
Quickshell.execDetached(["sh", "-c", "pwvucontrol || pavucontrol"]);
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: externalHideTimer
|
||||
running: false
|
||||
@@ -67,6 +71,45 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: [
|
||||
{
|
||||
"label": I18n.tr("context-menu.toggle-mute"),
|
||||
"action": "toggle_mute",
|
||||
"icon": AudioService.muted ? "volume-off" : "volume"
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("context-menu.open-mixer"),
|
||||
"action": "open_mixer",
|
||||
"icon": "adjustments"
|
||||
}
|
||||
// ,
|
||||
// {
|
||||
// "label": I18n.tr("context-menu.widget-settings"),
|
||||
// "action": "widget_settings",
|
||||
// "icon": "settings"
|
||||
// },
|
||||
]
|
||||
|
||||
onTriggered: action => {
|
||||
if (action === "toggle_mute") {
|
||||
AudioService.setOutputMuted(!AudioService.muted);
|
||||
} else if (action === "open_mixer") {
|
||||
root.openExternalMixer();
|
||||
} else if (action === "widget_settings")
|
||||
// TODO: Open widget settings
|
||||
{}
|
||||
|
||||
// Close the popup menu window after handling the action
|
||||
var popupMenuWindow = PanelService.getPopupMenuWindow(screen);
|
||||
if (popupMenuWindow) {
|
||||
popupMenuWindow.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BarPill {
|
||||
id: pill
|
||||
|
||||
@@ -100,10 +143,17 @@ Item {
|
||||
PanelService.getPanel("audioPanel", screen)?.toggle(this);
|
||||
}
|
||||
onRightClicked: {
|
||||
AudioService.setOutputMuted(!AudioService.muted);
|
||||
}
|
||||
onMiddleClicked: {
|
||||
Quickshell.execDetached(["sh", "-c", "pwvucontrol || pavucontrol"]);
|
||||
// Get the shared popup menu window for this screen
|
||||
var popupMenuWindow = PanelService.getPopupMenuWindow(screen);
|
||||
if (popupMenuWindow) {
|
||||
// Calculate position using centralized helper (with center-based positioning)
|
||||
const pos = BarService.getContextMenuPosition(pill, contextMenu.implicitWidth, contextMenu.implicitHeight);
|
||||
|
||||
// Show the context menu inside the popup window
|
||||
contextMenu.openAtItem(pill, pos.x, pos.y);
|
||||
popupMenuWindow.showContextMenu(contextMenu);
|
||||
}
|
||||
}
|
||||
onMiddleClicked: root.openExternalMixer()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ Variants {
|
||||
}
|
||||
}
|
||||
|
||||
// TrayMenuWindow - separate window for tray context menus
|
||||
// PopupMenuWindow - reusable popup window for both tray menus and context menus
|
||||
// Disabled when bar is hidden or not configured for this screen
|
||||
Loader {
|
||||
active: {
|
||||
@@ -108,12 +108,12 @@ Variants {
|
||||
}
|
||||
asynchronous: false
|
||||
|
||||
sourceComponent: TrayMenuWindow {
|
||||
sourceComponent: PopupMenuWindow {
|
||||
screen: modelData
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
Logger.d("AllScreens", "TrayMenuWindow created for", modelData?.name);
|
||||
Logger.d("AllScreens", "PopupMenuWindow created for", modelData?.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
92
Modules/MainScreen/PopupMenuWindow.qml
Normal file
92
Modules/MainScreen/PopupMenuWindow.qml
Normal file
@@ -0,0 +1,92 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
|
||||
// Generic full-screen popup window for menus and context menus
|
||||
// This is a top-level PanelWindow (sibling to MainScreen, not nested inside it)
|
||||
// Provides click-outside-to-close functionality for any popup content
|
||||
// Loads TrayMenu by default but can show context menus via showContextMenu()
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
property string windowType: "popupmenu" // Used for namespace and registration
|
||||
|
||||
// Content item to display (set by the popup that uses this window)
|
||||
property var contentItem: null
|
||||
|
||||
// Expose the trayMenu Loader directly (for backward compatibility)
|
||||
readonly property alias trayMenuLoader: trayMenuLoader
|
||||
|
||||
anchors.top: true
|
||||
anchors.left: true
|
||||
anchors.right: true
|
||||
anchors.bottom: true
|
||||
visible: false
|
||||
color: Color.transparent
|
||||
|
||||
// Use Top layer (same as MainScreen) for proper event handling
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
WlrLayershell.namespace: "noctalia-" + windowType + "-" + (screen?.name || "unknown")
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
// Register with PanelService so widgets can find this window
|
||||
Component.onCompleted: {
|
||||
objectName = "popupMenuWindow-" + (screen?.name || "unknown");
|
||||
PanelService.registerPopupMenuWindow(screen, root);
|
||||
}
|
||||
|
||||
// Load TrayMenu as the default content
|
||||
Loader {
|
||||
id: trayMenuLoader
|
||||
source: Quickshell.shellDir + "/Modules/Bar/Extras/TrayMenu.qml"
|
||||
onLoaded: {
|
||||
if (item) {
|
||||
item.screen = root.screen;
|
||||
// Set the loaded item as default content
|
||||
root.contentItem = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function open() {
|
||||
visible = true;
|
||||
}
|
||||
|
||||
// Show a context menu (temporarily replaces TrayMenu as content)
|
||||
function showContextMenu(menu) {
|
||||
if (menu) {
|
||||
contentItem = menu;
|
||||
open();
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible = false;
|
||||
// Call close/hide method on current content
|
||||
if (contentItem) {
|
||||
if (typeof contentItem.hideMenu === "function") {
|
||||
contentItem.hideMenu();
|
||||
} else if (typeof contentItem.close === "function") {
|
||||
contentItem.close();
|
||||
}
|
||||
}
|
||||
// Restore TrayMenu as default content
|
||||
if (trayMenuLoader.item) {
|
||||
contentItem = trayMenuLoader.item;
|
||||
}
|
||||
}
|
||||
|
||||
// Full-screen click catcher - click anywhere outside content closes the window
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onClicked: root.close()
|
||||
}
|
||||
|
||||
// Content will be parented here by the popup
|
||||
// (e.g., TrayMenu, NPopupContextMenu)
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Services.UI
|
||||
|
||||
// Separate window for TrayMenu context menus
|
||||
// This is a top-level PanelWindow (sibling to MainScreen, not nested inside it)
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
|
||||
anchors.top: true
|
||||
anchors.left: true
|
||||
anchors.right: true
|
||||
anchors.bottom: true
|
||||
visible: false
|
||||
color: Color.transparent
|
||||
|
||||
// Use Top layer (same as MainScreen) for proper event handling
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
WlrLayershell.namespace: "noctalia-traymenu-" + (screen?.name || "unknown")
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
// Expose the trayMenu Loader directly
|
||||
readonly property alias trayMenuLoader: trayMenu
|
||||
|
||||
// Register with PanelService so panels can find this window
|
||||
Component.onCompleted: {
|
||||
objectName = "trayMenuWindow-" + (screen?.name || "unknown");
|
||||
PanelService.registerTrayMenuWindow(screen, root);
|
||||
}
|
||||
|
||||
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: root.close()
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: trayMenu
|
||||
source: Quickshell.shellDir + "/Modules/Bar/Extras/TrayMenu.qml"
|
||||
onLoaded: {
|
||||
if (item) {
|
||||
item.screen = root.screen;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,22 +100,22 @@ SmartPanel {
|
||||
}
|
||||
|
||||
// Trigger re-evaluation when window is registered
|
||||
property int trayMenuUpdateTrigger: 0
|
||||
property int popupMenuUpdateTrigger: 0
|
||||
|
||||
// Get the trayMenu window and loader from PanelService (reactive to trigger changes)
|
||||
readonly property var trayMenuWindow: {
|
||||
readonly property var popupMenuWindow: {
|
||||
// Reference trigger to force re-evaluation
|
||||
var _ = trayMenuUpdateTrigger;
|
||||
return PanelService.getTrayMenuWindow(screen);
|
||||
var _ = popupMenuUpdateTrigger;
|
||||
return PanelService.getPopupMenuWindow(screen);
|
||||
}
|
||||
|
||||
readonly property var trayMenu: trayMenuWindow ? trayMenuWindow.trayMenuLoader : null
|
||||
readonly property var trayMenu: popupMenuWindow ? popupMenuWindow.trayMenuLoader : null
|
||||
|
||||
Connections {
|
||||
target: PanelService
|
||||
function onTrayMenuWindowRegistered(registeredScreen) {
|
||||
function onPopupMenuWindowRegistered(registeredScreen) {
|
||||
if (registeredScreen === screen) {
|
||||
root.trayMenuUpdateTrigger++;
|
||||
root.popupMenuUpdateTrigger++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,13 +192,13 @@ SmartPanel {
|
||||
TooltipService.hideImmediately();
|
||||
|
||||
// Close menu if already visible
|
||||
if (trayMenuWindow && trayMenuWindow.visible) {
|
||||
trayMenuWindow.close();
|
||||
if (popupMenuWindow && popupMenuWindow.visible) {
|
||||
popupMenuWindow.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (modelData.hasMenu && modelData.menu && trayMenuWindow && trayMenu && trayMenu.item) {
|
||||
trayMenuWindow.open();
|
||||
if (modelData.hasMenu && modelData.menu && popupMenuWindow && trayMenu && trayMenu.item) {
|
||||
popupMenuWindow.open();
|
||||
|
||||
// Position menu at the tray icon
|
||||
const barPosition = Settings.data.bar.position;
|
||||
@@ -232,8 +232,8 @@ SmartPanel {
|
||||
}
|
||||
|
||||
onEntered: {
|
||||
if (trayMenuWindow) {
|
||||
trayMenuWindow.close();
|
||||
if (popupMenuWindow) {
|
||||
popupMenuWindow.close();
|
||||
}
|
||||
TooltipService.show(screen, trayIcon, modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item", BarService.getTooltipDirection());
|
||||
}
|
||||
|
||||
@@ -244,4 +244,54 @@ Singleton {
|
||||
return "bottom";
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate context menu position based on bar position
|
||||
// Parameters:
|
||||
// anchorItem: The widget item to anchor the menu to
|
||||
// menuWidth: Width of the context menu (optional, defaults to 180)
|
||||
// menuHeight: Height of the context menu (optional, defaults to 100)
|
||||
// Returns: { x: number, y: number }
|
||||
// Note: Anchor position is top-left corner, so we calculate from center
|
||||
function getContextMenuPosition(anchorItem, menuWidth, menuHeight) {
|
||||
if (!anchorItem) {
|
||||
Logger.w("BarService", "getContextMenuPosition: anchorItem is null");
|
||||
return {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
};
|
||||
}
|
||||
|
||||
const mWidth = menuWidth || 180;
|
||||
const mHeight = menuHeight || 100;
|
||||
const barPosition = Settings.data.bar.position;
|
||||
let menuX = 0;
|
||||
let menuY = 0;
|
||||
|
||||
// Calculate center-based positioning for consistent spacing
|
||||
const anchorCenterX = anchorItem.width / 2;
|
||||
const anchorCenterY = anchorItem.height / 2;
|
||||
|
||||
if (barPosition === "left") {
|
||||
// For left bar: position menu to the right of anchor, vertically centered
|
||||
menuX = anchorItem.width + Style.marginM;
|
||||
menuY = anchorCenterY - (mHeight / 2);
|
||||
} else if (barPosition === "right") {
|
||||
// For right bar: position menu to the left of anchor, vertically centered
|
||||
menuX = -mWidth - Style.marginM;
|
||||
menuY = anchorCenterY - (mHeight / 2);
|
||||
} else if (barPosition === "top") {
|
||||
// For top bar: position menu below bar, horizontally centered
|
||||
menuX = anchorCenterX - (mWidth / 2);
|
||||
menuY = Style.barHeight;
|
||||
} else {
|
||||
// For bottom bar: position menu above, horizontally centered
|
||||
menuX = anchorCenterX - (mWidth / 2);
|
||||
menuY = -mHeight - Style.marginM;
|
||||
}
|
||||
|
||||
return {
|
||||
"x": menuX,
|
||||
"y": menuY
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@ Singleton {
|
||||
signal willOpen
|
||||
signal didClose
|
||||
|
||||
// Tray menu windows (one per screen)
|
||||
property var trayMenuWindows: ({})
|
||||
signal trayMenuWindowRegistered(var screen)
|
||||
// Popup menu windows (one per screen) - used for both tray menus and context menus
|
||||
property var popupMenuWindows: ({})
|
||||
signal popupMenuWindowRegistered(var screen)
|
||||
|
||||
// Register this panel (called after panel is loaded)
|
||||
function registerPanel(panel) {
|
||||
@@ -26,21 +26,21 @@ Singleton {
|
||||
Logger.d("PanelService", "Registered panel:", panel.objectName);
|
||||
}
|
||||
|
||||
// Register tray menu window for a screen
|
||||
function registerTrayMenuWindow(screen, window) {
|
||||
// Register popup menu window for a screen
|
||||
function registerPopupMenuWindow(screen, window) {
|
||||
if (!screen || !window)
|
||||
return;
|
||||
var key = screen.name;
|
||||
trayMenuWindows[key] = window;
|
||||
Logger.d("PanelService", "Registered tray menu window for screen:", key);
|
||||
trayMenuWindowRegistered(screen);
|
||||
popupMenuWindows[key] = window;
|
||||
Logger.d("PanelService", "Registered popup menu window for screen:", key);
|
||||
popupMenuWindowRegistered(screen);
|
||||
}
|
||||
|
||||
// Get tray menu window for a screen
|
||||
function getTrayMenuWindow(screen) {
|
||||
// Get popup menu window for a screen
|
||||
function getPopupMenuWindow(screen) {
|
||||
if (!screen)
|
||||
return null;
|
||||
return trayMenuWindows[screen.name] || null;
|
||||
return popupMenuWindows[screen.name] || null;
|
||||
}
|
||||
|
||||
// Returns a panel (loads it on-demand if not yet loaded)
|
||||
|
||||
201
Widgets/NPopupContextMenu.qml
Normal file
201
Widgets/NPopupContextMenu.qml
Normal file
@@ -0,0 +1,201 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
|
||||
// Simple context menu PopupWindow (similar to TrayMenu)
|
||||
// Designed to be rendered inside a PopupMenuWindow for click-outside-to-close
|
||||
PopupWindow {
|
||||
id: root
|
||||
|
||||
property alias model: repeater.model
|
||||
property real itemHeight: 28 // Match TrayMenu
|
||||
property real itemPadding: Style.marginM
|
||||
property int verticalPolicy: ScrollBar.AsNeeded
|
||||
property int horizontalPolicy: ScrollBar.AsNeeded
|
||||
|
||||
property var anchorItem: null
|
||||
property real anchorX: 0
|
||||
property real anchorY: 0
|
||||
|
||||
signal triggered(string action)
|
||||
|
||||
implicitWidth: 180
|
||||
implicitHeight: Math.min(600, flickable.contentHeight + (Style.marginS * 2))
|
||||
visible: false
|
||||
color: Color.transparent
|
||||
|
||||
anchor.item: anchorItem
|
||||
anchor.rect.x: anchorX
|
||||
anchor.rect.y: anchorY
|
||||
|
||||
// Handle Escape key to close menu
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
Keys.onEscapePressed: root.close()
|
||||
}
|
||||
|
||||
// Background
|
||||
Rectangle {
|
||||
id: menuBackground
|
||||
anchors.fill: parent
|
||||
color: Color.mSurface
|
||||
border.color: Color.mOutline
|
||||
border.width: Style.borderS
|
||||
radius: Style.radiusM
|
||||
|
||||
// Fade-in animation
|
||||
opacity: root.visible ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Content - Use Flickable + ColumnLayout like TrayMenu for consistency
|
||||
Flickable {
|
||||
id: flickable
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginS
|
||||
contentHeight: columnLayout.implicitHeight
|
||||
interactive: true
|
||||
|
||||
// Fade-in animation
|
||||
opacity: root.visible ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Style.animationNormal
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: columnLayout
|
||||
width: flickable.width
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
|
||||
delegate: Rectangle {
|
||||
id: menuItem
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: modelData.visible !== false ? root.itemHeight : 0
|
||||
visible: modelData.visible !== false
|
||||
color: Color.transparent
|
||||
|
||||
Rectangle {
|
||||
id: innerRect
|
||||
anchors.fill: parent
|
||||
color: mouseArea.containsMouse ? Color.mHover : Color.transparent
|
||||
radius: Style.radiusS
|
||||
opacity: modelData.enabled !== false ? 1.0 : 0.5
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.marginM
|
||||
anchors.rightMargin: Style.marginM
|
||||
spacing: Style.marginS
|
||||
|
||||
// Optional icon
|
||||
NIcon {
|
||||
visible: modelData.icon !== undefined
|
||||
icon: modelData.icon || ""
|
||||
pointSize: Style.fontSizeS
|
||||
applyUiScale: false
|
||||
color: mouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: modelData.label || modelData.text || ""
|
||||
pointSize: Style.fontSizeS
|
||||
color: mouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: (modelData.enabled !== false) && root.visible
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
if (menuItem.modelData.enabled !== false) {
|
||||
root.triggered(menuItem.modelData.action || menuItem.modelData.key || menuItem.index.toString());
|
||||
// Don't call root.close() here - let the parent PopupMenuWindow handle closing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to open at specific position relative to anchor item
|
||||
function openAt(x, y, item) {
|
||||
if (!item) {
|
||||
Logger.w("NPopupContextMenu", "anchorItem is undefined, won't show menu.");
|
||||
return;
|
||||
}
|
||||
|
||||
anchorItem = item;
|
||||
anchorX = x;
|
||||
anchorY = y;
|
||||
|
||||
visible = true;
|
||||
|
||||
// Force update after showing
|
||||
Qt.callLater(() => {
|
||||
if (root.anchor) {
|
||||
root.anchor.updateAnchor();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to open at item (compatible with NContextMenu API)
|
||||
function openAtItem(item, mouseX, mouseY) {
|
||||
openAt(mouseX || 0, mouseY || 0, item);
|
||||
}
|
||||
|
||||
// Helper function to close menu (compatible with PopupMenuWindow)
|
||||
function close() {
|
||||
visible = false;
|
||||
}
|
||||
|
||||
// Alias for backward compatibility
|
||||
function closeMenu() {
|
||||
close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user