diff --git a/apps/arstack_studio/qml/SignalMatrix.qml b/apps/arstack_studio/qml/SignalMatrix.qml index 2e6d9d242..1c84e2b43 100644 --- a/apps/arstack_studio/qml/SignalMatrix.qml +++ b/apps/arstack_studio/qml/SignalMatrix.qml @@ -124,12 +124,14 @@ Rectangle { property real pendingMagnitude: mag property real pendingPhase: angle - // The same placement/visibility semantics for mouse and keyboard. - // Clamp horizontally to the row rather than allowing a 250px - // popup to extend underneath the adjacent matrix. + // Align with the field edge, not the left side of the table. + // Clamp inside the owning matrix and open below the editor; + // keyboard Menu/Shift+F10 uses exactly the same placement. function openFieldMenu(field, actionMenu) { - const at = signalRow.mapFromItem(field, 0, field.height) - actionMenu.popup(Math.max(6, Math.min(at.x, signalRow.width - actionMenu.width)), at.y) + const edge = signalRow.mapFromItem(field, field.width, field.height) + const x = Math.max(6, Math.min(edge.x - actionMenu.width, + signalRow.width - actionMenu.width - 6)) + actionMenu.popup(x, edge.y + 3) } // Action-backed menu uses a single globally styled delegate. @@ -139,7 +141,7 @@ Rectangle { id: magnitudeMenu theme: matrix.theme uiFont: matrix.uiFont - title: "Magnitude · " + signalRow.sid + contextLabel: "Magnitude · " + signalRow.sid commandRows: 4 Action { text: "Zero · 0" @@ -170,7 +172,7 @@ Rectangle { id: phaseMenu theme: matrix.theme uiFont: matrix.uiFont - title: "Angle · " + signalRow.sid + contextLabel: "Angle · " + signalRow.sid commandRows: 3 Action { text: "Zero angle · 0°" diff --git a/apps/arstack_studio/qml/StudioActionMenu.qml b/apps/arstack_studio/qml/StudioActionMenu.qml index a026db163..6395c42a1 100644 --- a/apps/arstack_studio/qml/StudioActionMenu.qml +++ b/apps/arstack_studio/qml/StudioActionMenu.qml @@ -7,11 +7,17 @@ Menu { id: actionMenu property var theme property string uiFont: "Inter" + // The native Menu.title reserves a heading area even when a dark + // custom menu delegate does not paint it. That produces the oversized + // blank top area visible in the previous Windows context popup. + // Keep context for accessibility/diagnostics without a title header. + property string contextLabel: "" + title: "" property int commandRows: 4 property int dividerRows: 1 popupType: Popup.Item - width: 212 - height: commandRows * 29 + dividerRows * 7 + 8 + width: 204 + height: commandRows * 30 + dividerRows * 7 + 8 topPadding: 4 bottomPadding: 4 leftPadding: 4 @@ -22,20 +28,21 @@ Menu { delegate: MenuItem { id: actionItem width: actionMenu.width - actionMenu.leftPadding - actionMenu.rightPadding - implicitHeight: 29 + implicitHeight: 30 leftPadding: 8 rightPadding: 8 font.family: actionMenu.uiFont font.pixelSize: 11 contentItem: RowLayout { - spacing: 7 + spacing: 9 Image { visible: actionItem.icon.source.toString().length > 0 source: actionItem.icon.source - sourceSize.width: 24 - sourceSize.height: 24 - Layout.preferredWidth: visible ? 15 : 0 - Layout.preferredHeight: 15 + sourceSize.width: 18 + sourceSize.height: 18 + Layout.preferredWidth: visible ? 16 : 0 + Layout.preferredHeight: 16 + fillMode: Image.PreserveAspectFit opacity: actionItem.enabled ? 0.95 : 0.33 } Text { @@ -48,7 +55,7 @@ Menu { } } background: Rectangle { - radius: 4 + radius: 5 color: actionItem.highlighted ? actionMenu.theme.menuHover : "transparent" border.width: actionItem.activeFocus ? 1 : 0 border.color: actionMenu.theme.accent diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index c9fa6afd1..af471e7e1 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -127,7 +127,15 @@ def main() -> None: "context actions must stay compact, descriptive and grouped") assert "implicitHeight: 65" in source("RibbonAction.qml") assert 'hovered ? "#142334"' in source("RibbonAction.qml") - assert "width: 212" in menu and "commandRows * 29" in menu + require('title: ""' in menu and 'property string contextLabel: ""' in menu + and 'contextLabel: "Magnitude · "' in matrix + and 'contextLabel: "Angle · "' in matrix + and 'title: "Magnitude · "' not in matrix and 'title: "Angle · "' not in matrix, + "context menu must have no phantom native title/header blank space") + require("width: 204" in menu and "commandRows * 30" in menu + and "actionMenu.popup(x, edge.y + 3)" in matrix + and "signalRow.mapFromItem(field, field.width, field.height)" in matrix, + "context menu must align to the editor with compact deterministic geometry") assert "text: \"Generated setpoint · not independent on-wire proof\"" not in preview assert "No ESP32 board detected · Preview mode only" in main_qml require("Layout.preferredHeight: 57" in main_qml