diff --git a/apps/arstack_studio/qml/CalmButton.qml b/apps/arstack_studio/qml/CalmButton.qml index ddddca61c..dab85063d 100644 --- a/apps/arstack_studio/qml/CalmButton.qml +++ b/apps/arstack_studio/qml/CalmButton.qml @@ -54,6 +54,8 @@ Button { color: control.activeText font: control.font anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter Behavior on color { ColorAnimation { duration: 90 } } } } diff --git a/apps/arstack_studio/qml/ConfigurationHub.qml b/apps/arstack_studio/qml/ConfigurationHub.qml index fecaa51a3..d45c7dd5c 100644 --- a/apps/arstack_studio/qml/ConfigurationHub.qml +++ b/apps/arstack_studio/qml/ConfigurationHub.qml @@ -34,13 +34,15 @@ Item { font.weight: checked ? Font.DemiBold : Font.Medium contentItem: Label { text: parent.text - color: parent.checked ? hub.theme.text : hub.theme.muted + color: parent.hovered ? "#142334" + : parent.checked ? hub.theme.text : hub.theme.muted font: parent.font horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } background: Rectangle { - color: parent.checked ? hub.theme.raised : "transparent" + color: parent.hovered ? "#e9f2fd" + : parent.checked ? hub.theme.raised : "transparent" radius: 6 border.width: parent.checked ? 1 : 0 border.color: hub.theme.line diff --git a/apps/arstack_studio/qml/Main.qml b/apps/arstack_studio/qml/Main.qml index 66a0daf89..d13d63a24 100644 --- a/apps/arstack_studio/qml/Main.qml +++ b/apps/arstack_studio/qml/Main.qml @@ -115,10 +115,10 @@ ApplicationWindow { property bool phaseLink: false property bool profileDirty: false property bool pendingApply: true - // Session-only operator intent. Never persist across application launches: - // an explicit PTP ON can be restored after a firmware reflash/reconnect - // without introducing an uncontrolled power-on publisher. - property bool ptpDesiredForSession: false + // The default operator workflow enables PTP SOURCE once a verified board + // becomes READY. Nothing transmits while disconnected. Explicit PTP OFF + // cancels auto-restart for this Studio session; no setting is persisted. + property bool ptpDesiredForSession: true property string ptpDesiredRole: "SOURCE" property bool ptpRestoreAfterFirmware: false property int ptpRestoreChecks: 0 @@ -1049,17 +1049,25 @@ ApplicationWindow { } } - RowLayout { + // At normal desktop heights fill the workspace + // vertically. At the minimum 960x620 viewport keep + // the previous two-column arrangement: 8 full signal + // rows cannot physically fit above the PTP tray. + GridLayout { Layout.fillWidth: true - Layout.preferredHeight: root.compactLayout ? 214 : 224 - Layout.maximumHeight: root.compactLayout ? 214 : 224 - spacing: 9 + Layout.fillHeight: root.height >= 735 + Layout.preferredHeight: root.height < 735 ? 214 : -1 + Layout.minimumHeight: root.height < 735 ? 193 : 398 + columns: root.height < 735 ? 2 : 1 + columnSpacing: 9 + rowSpacing: 8 SignalMatrix { id: voltageMatrix enabled: !workflowBar.startQueued objectName: "studioVoltageMatrix" Layout.fillWidth: true Layout.fillHeight: true + Layout.minimumHeight: 193 theme: studioTheme controller: root device: device @@ -1079,6 +1087,7 @@ ApplicationWindow { objectName: "studioCurrentMatrix" Layout.fillWidth: true Layout.fillHeight: true + Layout.minimumHeight: 193 theme: studioTheme controller: root device: device diff --git a/apps/arstack_studio/qml/ModernRibbon.qml b/apps/arstack_studio/qml/ModernRibbon.qml index aa995b10d..e78e13782 100644 --- a/apps/arstack_studio/qml/ModernRibbon.qml +++ b/apps/arstack_studio/qml/ModernRibbon.qml @@ -47,24 +47,24 @@ Rectangle { spacing: 6 RowLayout { - Layout.preferredWidth: bar.compact ? 105 : 124 + Layout.preferredWidth: bar.compact ? 150 : 184 Layout.alignment: Qt.AlignVCenter spacing: 8 Rectangle { - width: 28 - height: 28 - radius: 7 + width: 38 + height: 38 + radius: 9 color: "#0d151d" border.width: 1 border.color: bar.theme.lineSoft Image { anchors.centerIn: parent - width: 15 - height: 15 + width: 26 + height: 26 source: Qt.resolvedUrl("../assets/material/smv_injector.svg") - sourceSize.width: 30 - sourceSize.height: 30 + sourceSize.width: 52 + sourceSize.height: 52 } } @@ -73,7 +73,7 @@ Rectangle { text: "SMV Injector" color: bar.theme.text font.family: bar.uiFont - font.pixelSize: 11 + font.pixelSize: 15 font.weight: Font.DemiBold elide: Text.ElideRight verticalAlignment: Text.AlignVCenter diff --git a/apps/arstack_studio/qml/PtpExpertPanel.qml b/apps/arstack_studio/qml/PtpExpertPanel.qml index 14fb81763..35cbed9e0 100644 --- a/apps/arstack_studio/qml/PtpExpertPanel.qml +++ b/apps/arstack_studio/qml/PtpExpertPanel.qml @@ -134,7 +134,7 @@ SurfacePanel { const wantsRunning = checked checked = Qt.binding(function() { return Boolean(panel.device.ptpRunning) }) if (wantsRunning) panel.startSelectedMode() - else if (panel.session && !panel.session.requestStopPtp() && panel.controller) + else if (panel.controller && !panel.controller.requestStopPtp()) panel.controller.showMessage("PTP stop was not accepted. Copy diagnostics.", true) } } diff --git a/apps/arstack_studio/qml/RibbonAction.qml b/apps/arstack_studio/qml/RibbonAction.qml index a882bb495..e31de2cc7 100644 --- a/apps/arstack_studio/qml/RibbonAction.qml +++ b/apps/arstack_studio/qml/RibbonAction.qml @@ -39,6 +39,7 @@ Button { : tone === "danger" ? "#ffe3e6" : tone === "accent" || checked ? "#e1f1ff" : theme.textSoft contentItem: ColumnLayout { + anchors.centerIn: parent spacing: 3 Image { visible: control.iconSource.toString().length > 0 diff --git a/apps/arstack_studio/qml/StudioDarkCheckBox.qml b/apps/arstack_studio/qml/StudioDarkCheckBox.qml index eedfce586..36330996a 100644 --- a/apps/arstack_studio/qml/StudioDarkCheckBox.qml +++ b/apps/arstack_studio/qml/StudioDarkCheckBox.qml @@ -8,6 +8,10 @@ CheckBox { property var theme property string uiFont: "Inter" implicitHeight: 30 + hoverEnabled: true + // The indicator is a single custom rectangle in all pointer states. + // Disable the native style focus overlay which looks like a second box. + activeFocusOnTab: true spacing: 9 font.family: uiFont font.pixelSize: 11 @@ -20,7 +24,7 @@ CheckBox { color: control.checked ? control.theme.accent : control.theme.surface2 border.width: 1 border.color: control.checked ? control.theme.accent - : control.activeFocus ? control.theme.textSoft : control.theme.line + : control.hovered || control.activeFocus ? control.theme.textSoft : control.theme.line Text { anchors.centerIn: parent visible: control.checked diff --git a/apps/arstack_studio/qml/WorkflowBar.qml b/apps/arstack_studio/qml/WorkflowBar.qml index e49593114..efe8f44b1 100644 --- a/apps/arstack_studio/qml/WorkflowBar.qml +++ b/apps/arstack_studio/qml/WorkflowBar.qml @@ -150,8 +150,13 @@ SurfacePanel { // overwrite a board with a restored/offline local draft. controller.profileDirty = false controller.pendingApply = true - // Fast path: the next START uploads the local setpoints first. - // No auto-transmit on device discovery/reconnection. + // SV never starts automatically: the next START uploads the + // latest table snapshot. PTP is a separate operator-default + // timing SOURCE, armed only after verified board readiness. + if (controller.ptpDesiredForSession && !ribbon.device.ptpRunning) { + controller.ptpRestoreChecks = 0 + controller.ptpRestoreAfterFirmware = true + } } function onStateChanged() { if (ribbon.firmwareGateActive) @@ -281,9 +286,11 @@ SurfacePanel { controller.showMessage(ribbon.startReason(), false) return } - const accepted = controller.pendingApply - ? smartSession.requestStartWithScenario(controller.startScenarioSnapshot()) - : smartSession.requestStart() + // Always snapshot the eight visible table rows at START. Reusing + // the old device waveform when pendingApply is false makes GUI + // -30 deg look like transmitted 0 deg after a Stop/Start cycle. + const accepted = smartSession.requestStartWithScenario( + controller.startScenarioSnapshot()) if (!accepted) { controller.pendingApply = true controller.showMessage("Start rejected. Check device status.", true) diff --git a/apps/arstack_studio/src/main.cpp b/apps/arstack_studio/src/main.cpp index 13518af84..5e2ddcb8f 100644 --- a/apps/arstack_studio/src/main.cpp +++ b/apps/arstack_studio/src/main.cpp @@ -82,8 +82,14 @@ struct UiBounds { return false; } } - if (!(voltage.area.right() <= current.area.left() + tolerance - && tray.area.top() >= voltage.area.bottom() - tolerance + // At full desktop height matrices are stacked; at the minimum viewport + // they share a row. Enforce the actual responsive geometry in both modes. + const bool matricesOrdered = size.height() >= 735 + ? voltage.area.bottom() <= current.area.top() + tolerance + : voltage.area.right() <= current.area.left() + tolerance; + const qreal matricesBottom = std::max(voltage.area.bottom(), current.area.bottom()); + if (!(matricesOrdered + && tray.area.top() >= matricesBottom - tolerance && quality.area.top() >= tray.area.bottom() - tolerance && quality.area.bottom() <= surface.area.bottom() + tolerance)) { qCritical().noquote() << "Studio UI panel order/overlap violation" << size diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index ecca26e80..e860f0a74 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -26,6 +26,7 @@ def ordered(data: str, first: str, second: str, reason: str) -> None: def main() -> None: main_qml = source("Main.qml") + workflow = source("WorkflowBar.qml") preview = source("SignalPreview.qml") tray = source("StudioTimingTray.qml") expert = source("PtpExpertPanel.qml") @@ -46,14 +47,36 @@ def main() -> None: "stale PROFILE PTPROLE must not override verified PTP runtime") require("ptpRestoreAfterFirmware" in main_qml and "root.ptpRestoreChecks > 120" in main_qml - and "root.ptpDesiredForSession = false" in main_qml, - "PTP user intent restoration must be bounded, explicit and cancellable") + and "root.ptpDesiredForSession = false" in main_qml + and "property bool ptpDesiredForSession: true" in main_qml + and "controller.ptpRestoreAfterFirmware = true" in workflow + and "panel.controller.requestStopPtp()" in expert, + "default PTP SOURCE must be verified-only and OFF must cancel it in Main and Advanced") matrix = source("SignalMatrix.qml") menu = source("StudioActionMenu.qml") cmake = (STUDIO / "CMakeLists.txt").read_text(encoding="utf-8") ordered(main_qml, "id: voltageMatrix", "id: currentMatrix", "voltage must precede current in the injection table") + require("GridLayout {\n Layout.fillWidth: true" in main_qml + and "columns: root.height < 735 ? 2 : 1" in main_qml + and "Layout.minimumHeight: root.height < 735 ? 193 : 398" in main_qml, + "matrix layout must stack at normal height and fit 960x620 compact viewport") + workflow = source("WorkflowBar.qml") + require("smartSession.requestStartWithScenario(\n controller.startScenarioSnapshot())" in workflow + and "smartSession.requestStart()" not in workflow, + "every Start must send the current eight table rows, never stale board setpoints") + brand = source("ModernRibbon.qml") + require("font.pixelSize: 15" in brand and "width: 38" in brand, + "SMV Injector icon and title must be legible") + require('parent.hovered ? "#e9f2fd"' in config + and 'parent.hovered ? "#142334"' in config, + "Advanced tabs must retain dark text over light hover background") + require("horizontalAlignment: Text.AlignHCenter" in source("CalmButton.qml") + and "verticalAlignment: Text.AlignVCenter" in source("CalmButton.qml"), + "button captions must be centered") + require("control.hovered || control.activeFocus" in source("StudioDarkCheckBox.qml"), + "checkbox should keep a single indicator through hover") ordered(preview, "id: voltagePhasorCanvas", "id: currentPhasorCanvas", "voltage must precede current in the phasor view") ordered(preview, "drawWaveGroup(preview.voltageModel, voltageY, false)", @@ -116,7 +139,7 @@ def main() -> None: and 'enabled: !workflowBar.startQueued; onActivated: root.zeroAll()' in main_qml, "scenario editors and shortcuts must be frozen during a pending START snapshot") require("controller.pendingApply = true" in workflow - and "requestStartWithScenario(controller.startScenarioSnapshot())" in workflow + and "smartSession.requestStartWithScenario(\n controller.startScenarioSnapshot())" in workflow and "controller.applyAllSignals()" not in workflow and "startWatchdog" in workflow and "onRunningChanged()" in workflow, "reconnect must not auto-transmit, but one-click Start must stage a bounded scenario") @@ -145,7 +168,7 @@ def main() -> None: and 'toolTipText: "Advanced"' not in ribbon and 'ToolTip.text: "smpSynch mode"' not in timing_ui, "captioned operator controls must not duplicate labels as tooltips") - require('requestStartWithScenario(controller.startScenarioSnapshot())' in workflow + require('smartSession.requestStartWithScenario(\n controller.startScenarioSnapshot())' in workflow and "Apply to board" not in main_qml, "Edit-to-Start must not regress to a manual Apply-to-board action") require("assets/material/smv_injector.svg" in cmake_icons