Files
noctalia-shell/Widgets/NPopupContextMenu.qml
T

310 lines
8.4 KiB
QML

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
// Automatically positions itself to respect screen boundaries
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 ShellScreen screen: null
property real calculatedWidth: 180
readonly property string barPosition: Settings.data.bar.position
signal triggered(string action, var item)
implicitWidth: calculatedWidth
implicitHeight: Math.min(600, flickable.contentHeight + (Style.marginS * 2))
visible: false
color: Color.transparent
NText {
id: textMeasure
visible: false
pointSize: Style.fontSizeS
wrapMode: Text.NoWrap
elide: Text.ElideNone
width: undefined
}
NIcon {
id: iconMeasure
visible: false
icon: "bell"
pointSize: Style.fontSizeS
applyUiScale: false
}
onModelChanged: {
Qt.callLater(calculateWidth);
}
function calculateWidth() {
let maxWidth = 0;
if (model && model.length) {
for (let i = 0; i < model.length; i++) {
const item = model[i];
if (item && item.visible !== false) {
const label = item.label || item.text || "";
textMeasure.text = label;
textMeasure.forceLayout();
let itemWidth = textMeasure.contentWidth + 8;
if (item.icon !== undefined) {
itemWidth += iconMeasure.width + Style.marginS;
}
itemWidth += Style.marginM * 2;
if (itemWidth > maxWidth) {
maxWidth = itemWidth;
}
}
}
}
calculatedWidth = Math.max(maxWidth + (Style.marginS * 2), 120);
}
anchor.item: anchorItem
anchor.rect.x: {
if (anchorItem && screen) {
const anchorGlobalPos = anchorItem.mapToItem(null, 0, 0);
// For right bar: position menu to the left of anchor
if (root.barPosition === "right") {
let baseX = -implicitWidth - Style.marginM;
return baseX;
}
// For left bar: position menu to the right of anchor
if (root.barPosition === "left") {
let baseX = anchorItem.width + Style.marginM;
return baseX;
}
// For top/bottom bar: center horizontally on anchor
const anchorCenterX = anchorItem.width / 2;
let baseX = anchorCenterX - (implicitWidth / 2);
// Calculate menu position on screen
const menuScreenX = anchorGlobalPos.x + baseX;
const menuRight = menuScreenX + implicitWidth;
// Adjust if menu would clip on the right
if (menuRight > screen.width - Style.marginM) {
const overflow = menuRight - (screen.width - Style.marginM);
return baseX - overflow;
}
// Adjust if menu would clip on the left
if (menuScreenX < Style.marginM) {
return baseX + (Style.marginM - menuScreenX);
}
return baseX;
}
return 0;
}
anchor.rect.y: {
if (anchorItem && screen) {
const anchorCenterY = anchorItem.height / 2;
// Calculate base Y position based on bar orientation
let baseY;
if (root.barPosition === "bottom") {
// For bottom bar: position menu above the bar
baseY = -(implicitHeight + Style.marginM);
} else if (root.barPosition === "top") {
// For top bar: position menu below bar
baseY = Style.barHeight + Style.marginM;
} else {
// For left/right bar: vertically center on anchor
baseY = anchorCenterY - (implicitHeight / 2);
}
// Calculate menu position on screen
const anchorGlobalPos = anchorItem.mapToItem(null, 0, 0);
const menuScreenY = anchorGlobalPos.y + baseY;
const menuBottom = menuScreenY + implicitHeight;
// Adjust if menu would clip at bottom
if (menuBottom > screen.height - Style.marginM) {
const overflow = menuBottom - (screen.height - Style.marginM);
return baseY - overflow;
}
return baseY;
}
// Fallback if no screen
if (root.barPosition === "bottom") {
return -implicitHeight - Style.marginM;
}
return Style.barHeight;
}
Component.onCompleted: {
Qt.callLater(calculateWidth);
}
Item {
anchors.fill: parent
focus: true
Keys.onEscapePressed: root.close()
}
Rectangle {
id: menuBackground
anchors.fill: parent
color: Color.mSurface
border.color: Color.mOutline
border.width: Style.borderS
radius: Style.iRadiusM
opacity: root.visible ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutQuad
}
}
}
Flickable {
id: flickable
anchors.fill: parent
anchors.margins: Style.marginS
contentHeight: columnLayout.implicitHeight
interactive: true
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.iRadiusS
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
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(), menuItem.modelData);
// Don't call root.close() here - let the parent PopupMenuWindow handle closing
}
}
}
}
}
}
}
}
// Helper function to open context menu anchored to an item
// Position is calculated automatically based on bar position and screen boundaries
function openAtItem(item, itemScreen) {
if (!item) {
Logger.w("NPopupContextMenu", "anchorItem is undefined, won't show menu.");
return;
}
calculateWidth();
anchorItem = item;
screen = itemScreen || null;
visible = true;
}
function close() {
visible = false;
}
function closeMenu() {
close();
}
}