import QtQuick import QtQuick.Controls import QtQuick.Layouts import qs.Commons /* * NContextMenu - Popup-based context menu for use inside panels and dialogs * * Use this component when you need a context menu inside: * - Settings panels * - Dialogs * - Repeater delegates * - Any nested component context * * For bar widgets and top-level window contexts, use NPopupContextMenu instead, * which provides better screen boundary handling and compositor integration. * * Usage: * NContextMenu { * id: contextMenu * parent: Overlay.overlay * model: [ * { "label": "Action 1", "action": "action1", "icon": "icon-name" }, * { "label": "Action 2", "action": "action2" } * ] * onTriggered: action => { Logger.i("MyModule", "Selected:", action) } * } * * MouseArea { * onClicked: contextMenu.openAtItem(parent, mouse.x, mouse.y) * } */ Popup { id: root property var model: [] property real itemHeight: 36 property real itemPadding: Style.marginM property int verticalPolicy: ScrollBar.AsNeeded property int horizontalPolicy: ScrollBar.AsNeeded // Optional: explicit item whose bounds the menu must stay within. // When unset, openAtItem auto-detects the nearest clipping ancestor. property Item constrainTo: null property Item _detectedConstraint: null signal triggered(string action) // Filter out hidden items to avoid spacing artifacts from zero-height items readonly property var filteredModel: { if (!model || model.length === 0) return []; var filtered = []; for (var i = 0; i < model.length; i++) { if (model[i].visible !== false) { filtered.push(model[i]); } } return filtered; } width: 180 padding: Style.marginS background: Rectangle { color: Color.mSurfaceVariant border.color: Color.mOutline border.width: Style.borderS radius: Style.iRadiusM } contentItem: NListView { id: listView implicitHeight: Math.max(contentHeight, root.itemHeight) spacing: Style.marginXXS interactive: contentHeight > root.height verticalPolicy: root.verticalPolicy horizontalPolicy: root.horizontalPolicy reserveScrollbarSpace: false model: root.filteredModel delegate: ItemDelegate { id: menuItem width: listView.availableWidth height: root.itemHeight opacity: modelData.enabled !== false ? 1.0 : 0.5 enabled: modelData.enabled !== false // Store reference to the popup property var popup: root background: Rectangle { color: menuItem.hovered && menuItem.enabled ? Color.mHover : "transparent" radius: Style.iRadiusS Behavior on color { ColorAnimation { duration: Style.animationFast } } } contentItem: RowLayout { spacing: Style.marginS // Optional icon NIcon { visible: modelData.icon !== undefined icon: modelData.icon || "" pointSize: Style.fontSizeM color: menuItem.hovered && menuItem.enabled ? Color.mOnHover : Color.mOnSurface Layout.leftMargin: root.itemPadding Behavior on color { ColorAnimation { duration: Style.animationFast } } } NText { text: modelData.label || modelData.text || "" pointSize: Style.fontSizeM color: menuItem.hovered && menuItem.enabled ? Color.mOnHover : Color.mOnSurface verticalAlignment: Text.AlignVCenter Layout.fillWidth: true Layout.leftMargin: modelData.icon === undefined ? root.itemPadding : 0 Behavior on color { ColorAnimation { duration: Style.animationFast } } } } onClicked: { if (enabled) { popup.triggered(modelData.action || modelData.key || index.toString()); popup.close(); } } } } // Helper function to open at mouse position function openAt(x, y) { if (root.parent) { var menuWidth = root.width; var itemCount = root.filteredModel.length; var menuHeight = Math.max(itemCount * root.itemHeight + Math.max(0, itemCount - 1) * listView.spacing, root.itemHeight) + root.topPadding + root.bottomPadding; var constraint = root.constrainTo || root._detectedConstraint; if (constraint) { var tl = constraint.mapToItem(root.parent, 0, 0); x = Math.max(tl.x, Math.min(x, tl.x + constraint.width - menuWidth)); y = Math.max(tl.y, Math.min(y, tl.y + constraint.height - menuHeight)); } else { x = Math.max(0, Math.min(x, root.parent.width - menuWidth)); y = Math.max(0, Math.min(y, root.parent.height - menuHeight)); } } root.x = x; root.y = y; root.open(); } // Helper function to open at item function openAtItem(item, mouseX, mouseY) { if (!root.constrainTo) { root._detectedConstraint = null; var p = item; while (p && p !== root.parent) { if (p.clip && p.width > 0 && p.height > 0) { root._detectedConstraint = p; break; } p = p.parent; } } var pos = item.mapToItem(root.parent, mouseX || 0, mouseY || 0); openAt(pos.x, pos.y); } }