fedora
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user