fedora
This commit is contained in:
@@ -0,0 +1,372 @@
|
||||
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 minWidth: 120
|
||||
property real calculatedWidth: 180
|
||||
|
||||
// Explicit offset for centering on target item (computed from targetItem in openAtItem)
|
||||
property real targetOffsetX: 0
|
||||
property real targetOffsetY: 0
|
||||
property real targetWidth: 0
|
||||
property real targetHeight: 0
|
||||
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screen?.name)
|
||||
readonly property real barHeight: Style.getBarHeightForScreen(screen?.name)
|
||||
|
||||
signal triggered(string action, var item)
|
||||
|
||||
implicitWidth: calculatedWidth
|
||||
implicitHeight: Math.min(600, flickable.contentHeight + Style.margin2S)
|
||||
visible: false
|
||||
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.margin2M;
|
||||
|
||||
if (itemWidth > maxWidth) {
|
||||
maxWidth = itemWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
calculatedWidth = Math.max(maxWidth + Style.margin2S, minWidth);
|
||||
}
|
||||
|
||||
anchor.item: anchorItem
|
||||
|
||||
anchor.rect.x: {
|
||||
if (anchorItem && screen) {
|
||||
const anchorGlobalPos = anchorItem.mapToItem(null, 0, 0);
|
||||
|
||||
// Use stored targetOffsetX and targetWidth for positioning
|
||||
const effectiveWidth = targetWidth > 0 ? targetWidth : anchorItem.width;
|
||||
const targetGlobalX = anchorGlobalPos.x + targetOffsetX;
|
||||
|
||||
// For right bar: position menu to the left of target
|
||||
if (root.barPosition === "right") {
|
||||
let baseX = targetOffsetX - implicitWidth - Style.marginM;
|
||||
return baseX;
|
||||
}
|
||||
|
||||
// For left bar: position menu to the right of target
|
||||
if (root.barPosition === "left") {
|
||||
let baseX = targetOffsetX + effectiveWidth + Style.marginM;
|
||||
return baseX;
|
||||
}
|
||||
|
||||
// For top/bottom bar: center horizontally on target
|
||||
const targetCenterScreenX = targetGlobalX + (effectiveWidth / 2);
|
||||
const menuScreenX = targetCenterScreenX - (implicitWidth / 2);
|
||||
let baseX = menuScreenX - anchorGlobalPos.x;
|
||||
|
||||
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) {
|
||||
// Check if using absolute positioning (small anchor point item)
|
||||
const isAbsolutePosition = anchorItem.width <= 1 && anchorItem.height <= 1;
|
||||
|
||||
if (isAbsolutePosition) {
|
||||
// For absolute positioning, show menu directly at anchor Y
|
||||
// Only adjust if menu would clip at bottom
|
||||
const anchorGlobalPos = anchorItem.mapToItem(null, 0, 0);
|
||||
const menuBottom = anchorGlobalPos.y + implicitHeight;
|
||||
|
||||
if (menuBottom > screen.height - Style.marginM) {
|
||||
// Position above the click point instead
|
||||
return -implicitHeight;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const anchorGlobalPos = anchorItem.mapToItem(null, 0, 0);
|
||||
|
||||
// Use target offset/height for vertical bars, anchor dimensions otherwise
|
||||
const effectiveHeight = targetHeight > 0 ? targetHeight : anchorItem.height;
|
||||
const effectiveOffsetY = targetOffsetY;
|
||||
|
||||
// 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.marginS);
|
||||
} else if (root.barPosition === "top") {
|
||||
// For top bar: position menu below bar at consistent height
|
||||
// Compensate for anchor's Y position to ensure menu top is always at (barHeight + margin)
|
||||
baseY = barHeight + Style.marginS - anchorGlobalPos.y;
|
||||
} else {
|
||||
// For left/right bar: vertically center on target item
|
||||
const targetCenterY = effectiveOffsetY + (effectiveHeight / 2);
|
||||
baseY = targetCenterY - (implicitHeight / 2);
|
||||
}
|
||||
|
||||
const menuScreenY = anchorGlobalPos.y + baseY;
|
||||
const menuBottom = menuScreenY + implicitHeight;
|
||||
|
||||
// Define clipping boundaries based on bar position
|
||||
const topLimit = Style.marginM;
|
||||
const bottomLimit = root.barPosition === "bottom" ? screen.height - barHeight - Style.marginS : screen.height - Style.marginM;
|
||||
|
||||
// Adjust if menu would clip at top (skip for bottom bar - don't push menu down over bar)
|
||||
if (menuScreenY < topLimit && root.barPosition !== "bottom") {
|
||||
const adjustment = topLimit - menuScreenY;
|
||||
return baseY + adjustment;
|
||||
}
|
||||
|
||||
// Adjust if menu would clip at bottom (or overlap bar for bottom bar)
|
||||
if (menuBottom > bottomLimit) {
|
||||
const overflow = menuBottom - bottomLimit;
|
||||
return baseY - overflow;
|
||||
}
|
||||
|
||||
return baseY;
|
||||
}
|
||||
|
||||
// Fallback if no screen
|
||||
if (root.barPosition === "bottom") {
|
||||
return -implicitHeight - Style.marginS;
|
||||
}
|
||||
return 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.radiusM
|
||||
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: "transparent"
|
||||
|
||||
Rectangle {
|
||||
id: innerRect
|
||||
anchors.fill: parent
|
||||
color: mouseArea.containsMouse ? Color.mHover : "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
|
||||
|
||||
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
|
||||
// Optional centerOnItem: if provided, menu will be horizontally centered on this item instead of anchorItem
|
||||
function openAtItem(item, itemScreen, centerOnItem) {
|
||||
if (!item) {
|
||||
Logger.w("NPopupContextMenu", "anchorItem is undefined, won't show menu.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Set anchor and screen first
|
||||
anchorItem = item;
|
||||
screen = itemScreen || null;
|
||||
|
||||
// Compute target offset and dimensions from centerOnItem
|
||||
if (centerOnItem && centerOnItem !== item) {
|
||||
const relPos = centerOnItem.mapToItem(item, 0, 0);
|
||||
targetOffsetX = relPos.x;
|
||||
targetOffsetY = relPos.y;
|
||||
targetWidth = centerOnItem.width;
|
||||
targetHeight = centerOnItem.height;
|
||||
} else {
|
||||
targetOffsetX = 0;
|
||||
targetOffsetY = 0;
|
||||
targetWidth = 0;
|
||||
targetHeight = 0;
|
||||
}
|
||||
|
||||
// Calculate menu width after anchor is set
|
||||
calculateWidth();
|
||||
|
||||
visible = true;
|
||||
|
||||
// Force anchor recalculation after showing
|
||||
Qt.callLater(() => {
|
||||
anchor.updateAnchor();
|
||||
});
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible = false;
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user