From 4834a196019187720bb803185fcdcd352b3981a9 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 09:17:10 +0700 Subject: [PATCH 01/18] fix(studio): every START sends current 4I+4V table instead of reusing stale board values --- apps/arstack_studio/qml/WorkflowBar.qml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/arstack_studio/qml/WorkflowBar.qml b/apps/arstack_studio/qml/WorkflowBar.qml index e49593114..3f3a31505 100644 --- a/apps/arstack_studio/qml/WorkflowBar.qml +++ b/apps/arstack_studio/qml/WorkflowBar.qml @@ -281,9 +281,11 @@ SurfacePanel { controller.showMessage(ribbon.startReason(), false) return } - const accepted = controller.pendingApply - ? smartSession.requestStartWithScenario(controller.startScenarioSnapshot()) - : smartSession.requestStart() + // Always apply the *visible* 4I+4V table as one ordered START + // transaction. Device settings may retain the previous session; + // pendingApply is a UI hint, never an authority to omit setpoints. + const accepted = smartSession.requestStartWithScenario( + controller.startScenarioSnapshot()) if (!accepted) { controller.pendingApply = true controller.showMessage("Start rejected. Check device status.", true) From 29e53dbce2fb3516183c112455025563f74ceec2 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 09:17:30 +0700 Subject: [PATCH 02/18] style(studio): stack voltage over current for full-width control and less empty space --- apps/arstack_studio/qml/Main.qml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/arstack_studio/qml/Main.qml b/apps/arstack_studio/qml/Main.qml index 66a0daf89..d53b0f9dc 100644 --- a/apps/arstack_studio/qml/Main.qml +++ b/apps/arstack_studio/qml/Main.qml @@ -1049,11 +1049,14 @@ ApplicationWindow { } } - RowLayout { + // Two full-width channel tables read top to bottom. + // This keeps numeric columns usable and uses the space + // previously wasted below two cramped half-width panels. + ColumnLayout { Layout.fillWidth: true - Layout.preferredHeight: root.compactLayout ? 214 : 224 - Layout.maximumHeight: root.compactLayout ? 214 : 224 - spacing: 9 + Layout.preferredHeight: root.compactLayout ? 398 : 426 + Layout.maximumHeight: root.compactLayout ? 398 : 426 + spacing: 8 SignalMatrix { id: voltageMatrix enabled: !workflowBar.startQueued From 7045de2efece8726235600c9e0a3d8608f2b9efe Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 09:18:31 +0700 Subject: [PATCH 03/18] style(studio): restore normal 40px injector brand mark and legible ribbon name --- apps/arstack_studio/qml/ModernRibbon.qml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/arstack_studio/qml/ModernRibbon.qml b/apps/arstack_studio/qml/ModernRibbon.qml index aa995b10d..4b81b16eb 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 ? 148 : 172 Layout.alignment: Qt.AlignVCenter spacing: 8 Rectangle { - width: 28 - height: 28 - radius: 7 + width: 40 + height: 40 + radius: 9 color: "#0d151d" border.width: 1 border.color: bar.theme.lineSoft Image { anchors.centerIn: parent - width: 15 - height: 15 + width: 27 + height: 27 source: Qt.resolvedUrl("../assets/material/smv_injector.svg") - sourceSize.width: 30 - sourceSize.height: 30 + sourceSize.width: 54 + sourceSize.height: 54 } } @@ -73,10 +73,11 @@ Rectangle { text: "SMV Injector" color: bar.theme.text font.family: bar.uiFont - font.pixelSize: 11 + font.pixelSize: 14 font.weight: Font.DemiBold elide: Text.ElideRight verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft } } From c7270584437d7d0d307f75cf143fc3e14dca2d97 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 09:18:48 +0700 Subject: [PATCH 04/18] style(studio): readable near-white hover with dark text and matching icon --- apps/arstack_studio/qml/CalmButton.qml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/arstack_studio/qml/CalmButton.qml b/apps/arstack_studio/qml/CalmButton.qml index ddddca61c..a6169d67a 100644 --- a/apps/arstack_studio/qml/CalmButton.qml +++ b/apps/arstack_studio/qml/CalmButton.qml @@ -22,6 +22,7 @@ Button { Behavior on scale { NumberAnimation { duration: 70; easing.type: Easing.OutCubic } } readonly property color activeFill: !enabled ? "#10161d" : + hovered ? "#f0f6ff" : tone === "success" ? (hovered ? "#1e5c43" : "#194d38") : tone === "danger" ? (hovered ? "#523039" : theme.redSoft) : tone === "accent" ? (hovered ? "#1d3c5e" : theme.accentSoft) : @@ -32,6 +33,7 @@ Button { tone === "accent" ? "#315f8d" : (activeFocus ? theme.accent : hovered ? "#3a4a5b" : theme.line) readonly property color activeText: !enabled ? theme.muted2 : + hovered ? "#132233" : tone === "success" ? "#d6f4e6" : tone === "danger" ? "#ffdce0" : tone === "accent" ? "#dcebff" : theme.textSoft @@ -44,7 +46,9 @@ Button { width: control.iconSize height: control.iconSize anchors.verticalCenter: parent.verticalCenter - source: control.iconSource + source: control.hovered && control.enabled + ? control.iconSource.toString().replace("/assets/material/", "/assets/material_dark/") + : control.iconSource sourceSize.width: control.iconSize * 2 sourceSize.height: control.iconSize * 2 opacity: control.enabled ? 0.94 : 0.30 From d49127ed7006296f1eaf18a1e391174b116b1c62 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 09:19:10 +0700 Subject: [PATCH 05/18] style(studio): single consistent custom checkbox geometry across hover and checked states --- apps/arstack_studio/qml/StudioDarkCheckBox.qml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/arstack_studio/qml/StudioDarkCheckBox.qml b/apps/arstack_studio/qml/StudioDarkCheckBox.qml index eedfce586..23cd98581 100644 --- a/apps/arstack_studio/qml/StudioDarkCheckBox.qml +++ b/apps/arstack_studio/qml/StudioDarkCheckBox.qml @@ -7,8 +7,13 @@ CheckBox { id: control property var theme property string uiFont: "Inter" - implicitHeight: 30 + implicitHeight: 32 + implicitWidth: contentItem.implicitWidth + indicator.implicitWidth + spacing + leftPadding + rightPadding spacing: 9 + hoverEnabled: true + // Use one fixed-size custom indicator for all states. No native style + // hover/focus sub-checkbox may appear alongside the engineering control. + background: Item {} font.family: uiFont font.pixelSize: 11 indicator: Rectangle { @@ -20,7 +25,8 @@ 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 From 517cbe1619c9b343530ce799b07b54d72bd5b678 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 09:19:35 +0700 Subject: [PATCH 06/18] style(studio): center ribbon icons and captions both horizontally and vertically --- apps/arstack_studio/qml/RibbonAction.qml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/arstack_studio/qml/RibbonAction.qml b/apps/arstack_studio/qml/RibbonAction.qml index a882bb495..57c6e83d2 100644 --- a/apps/arstack_studio/qml/RibbonAction.qml +++ b/apps/arstack_studio/qml/RibbonAction.qml @@ -38,14 +38,15 @@ Button { : tone === "warning" ? "#f5daaa" : tone === "danger" ? "#ffe3e6" : tone === "accent" || checked ? "#e1f1ff" : theme.textSoft - contentItem: ColumnLayout { - spacing: 3 + contentItem: Column { + spacing: 4 + anchors.centerIn: parent Image { visible: control.iconSource.toString().length > 0 source: control.hovered && control.enabled ? control.hoverPath : control.iconSource - Layout.alignment: Qt.AlignHCenter - Layout.preferredWidth: 24 - Layout.preferredHeight: 24 + anchors.horizontalCenter: parent.horizontalCenter + width: 24 + height: 24 sourceSize.width: 48 sourceSize.height: 48 opacity: control.enabled ? 1 : 0.3 @@ -56,7 +57,7 @@ Button { text: control.text color: control.textColor font: control.font - Layout.alignment: Qt.AlignHCenter + anchors.horizontalCenter: parent.horizontalCenter horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter elide: Text.ElideRight From 56b2dd3395cdcc37db79d28f140a2fd53911f5ab Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 09:20:06 +0700 Subject: [PATCH 07/18] feat(studio): default verified device to PTP SOURCE ON with AUTO sync, preserving explicit OFF --- apps/arstack_studio/qml/Main.qml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/apps/arstack_studio/qml/Main.qml b/apps/arstack_studio/qml/Main.qml index d53b0f9dc..dca75c9d5 100644 --- a/apps/arstack_studio/qml/Main.qml +++ b/apps/arstack_studio/qml/Main.qml @@ -119,6 +119,7 @@ ApplicationWindow { // an explicit PTP ON can be restored after a firmware reflash/reconnect // without introducing an uncontrolled power-on publisher. property bool ptpDesiredForSession: false + property bool defaultPtpAttempted: false property string ptpDesiredRole: "SOURCE" property bool ptpRestoreAfterFirmware: false property int ptpRestoreChecks: 0 @@ -215,6 +216,25 @@ ApplicationWindow { } } + // Operator default: start PTP SOURCE automatically once the connected + // board has been verified, without starting SV or claiming clock lock. + // Never retry rejected commands indefinitely. Explicit PTP OFF wins. + Timer { + interval: 400 + repeat: true + running: !root.defaultPtpAttempted + onTriggered: { + const session = workflowBar.session + if (!session || !session.startReady || session.updatingFirmware || + !device.deviceVerified || !device.ptpAvailable || + device.ptpStarting) + return + root.defaultPtpAttempted = true + if (!device.ptpRunning) + root.startPtpRole("SOURCE") + } + } + Timer { interval: 300 repeat: true @@ -314,6 +334,7 @@ ApplicationWindow { } function startPtpRole(role) { + root.defaultPtpAttempted = true const accepted = profilePanel.startPtpRole(role) if (accepted) { root.ptpDesiredForSession = true @@ -323,6 +344,7 @@ ApplicationWindow { } function requestStopPtp() { + root.defaultPtpAttempted = true root.ptpDesiredForSession = false root.ptpRestoreAfterFirmware = false return workflowBar.session && workflowBar.session.requestStopPtp() From 63658b873c07cb13f587868ae571219bf7d8fb0d Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 09:20:22 +0700 Subject: [PATCH 08/18] style(studio): Advanced tabs readable white hover/dark captions and tighter spacing --- apps/arstack_studio/qml/ConfigurationHub.qml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/arstack_studio/qml/ConfigurationHub.qml b/apps/arstack_studio/qml/ConfigurationHub.qml index fecaa51a3..bfaf5e20f 100644 --- a/apps/arstack_studio/qml/ConfigurationHub.qml +++ b/apps/arstack_studio/qml/ConfigurationHub.qml @@ -32,15 +32,18 @@ Item { font.family: hub.uiFont font.pixelSize: 11 font.weight: checked ? Font.DemiBold : Font.Medium + hoverEnabled: true contentItem: Label { text: parent.text - color: parent.checked ? hub.theme.text : hub.theme.muted + color: parent.hovered && parent.enabled ? "#132233" + : 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 && parent.enabled ? "#f0f6ff" + : parent.checked ? hub.theme.raised : "transparent" radius: 6 border.width: parent.checked ? 1 : 0 border.color: hub.theme.line @@ -59,7 +62,7 @@ Item { ColumnLayout { anchors.fill: parent - spacing: 16 + spacing: 11 RowLayout { Layout.fillWidth: true From dd78fedb861fa13378f53fe22a7afde96e34e8d4 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 09:21:34 +0700 Subject: [PATCH 09/18] test(studio): freeze SSOT START, local-sync baseline and normalized operator layout --- .../tests/check_operator_workspace.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index ecca26e80..668ef33e4 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -48,6 +48,33 @@ def main() -> None: and "root.ptpRestoreChecks > 120" in main_qml and "root.ptpDesiredForSession = false" in main_qml, "PTP user intent restoration must be bounded, explicit and cancellable") + workflow_start = source("WorkflowBar.qml") + before_stop = workflow_start.split("function requestStart() {", 1)[1].split( + "function requestStop()", 1)[0] + require("requestStartWithScenario(" in before_stop + and "requestStart()" not in before_stop, + "each physical START must use the current GUI 4I+4V table, not stale persisted firmware setpoints") + require("Layout.preferredHeight: root.compactLayout ? 398 : 426" in main_qml + and "ColumnLayout {\n Layout.fillWidth: true" in main_qml + and main_qml.index("id: voltageMatrix") < main_qml.index("id: currentMatrix"), + "Voltage and Current must be full-width, vertical and ordered") + require('root.startPtpRole("SOURCE")' in main_qml + and 'root.defaultPtpAttempted = true' in main_qml + and 'property bool phasorDockVisible: true' in main_qml + and 'property bool waveformDockVisible: true' in main_qml, + "default verified PTP SOURCE/AUTO and visible Phasor/Wave must preserve operator OFF authority") + ribbon_component = source("ModernRibbon.qml") + ribbon_action = source("RibbonAction.qml") + calm_button = source("CalmButton.qml") + checkbox = source("StudioDarkCheckBox.qml") + require('font.pixelSize: 14' in ribbon_component + and 'width: 40' in ribbon_component + and 'anchors.centerIn: parent' in ribbon_action, + "primary brand size and action captions/icons must stay centered") + require('hovered ? "#132233"' in calm_button + and 'hovered ? "#f0f6ff"' in calm_button + and 'background: Item {}' in checkbox, + "shared buttons and checkbox must have legible, consistent hover states") matrix = source("SignalMatrix.qml") menu = source("StudioActionMenu.qml") cmake = (STUDIO / "CMakeLists.txt").read_text(encoding="utf-8") From e8fc83944ff642494f45e6e2a6ca6942c193a5e5 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 09:23:40 +0700 Subject: [PATCH 10/18] test(studio): accept multiline SSOT START snapshot command formatting --- apps/arstack_studio/tests/check_operator_workspace.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index 668ef33e4..00e8814e5 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -143,7 +143,8 @@ 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 "requestStartWithScenario(" in workflow + and "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") From 047e5a755a4bec5419acb76127c53c01f8a2b9f7 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 09:24:45 +0700 Subject: [PATCH 11/18] style(studio): Advanced selectors use high-contrast white hover and dark captions --- apps/arstack_studio/qml/StudioDarkComboBox.qml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/arstack_studio/qml/StudioDarkComboBox.qml b/apps/arstack_studio/qml/StudioDarkComboBox.qml index 60f7afb00..bdc401044 100644 --- a/apps/arstack_studio/qml/StudioDarkComboBox.qml +++ b/apps/arstack_studio/qml/StudioDarkComboBox.qml @@ -21,7 +21,7 @@ ComboBox { background: Rectangle { radius: 7 - color: combo.enabled ? (combo.hovered ? combo.theme.raisedHover : combo.theme.surface2) + color: combo.enabled ? (combo.hovered ? "#f0f6ff" : combo.theme.surface2) : combo.theme.surface2 border.width: 1 border.color: combo.activeFocus ? combo.theme.accent : combo.theme.line @@ -29,7 +29,8 @@ ComboBox { contentItem: Text { text: combo.displayText - color: combo.enabled ? combo.theme.text : combo.theme.muted + color: !combo.enabled ? combo.theme.muted + : combo.hovered ? "#132233" : combo.theme.text font: combo.font verticalAlignment: Text.AlignVCenter leftPadding: 12 @@ -39,7 +40,8 @@ ComboBox { indicator: Text { text: "\u25be" - color: combo.enabled ? combo.theme.textSoft : combo.theme.muted + color: !combo.enabled ? combo.theme.muted + : combo.hovered ? "#132233" : combo.theme.textSoft font.pixelSize: 14 anchors.right: parent.right anchors.rightMargin: 11 @@ -54,7 +56,7 @@ ComboBox { highlighted: combo.highlightedIndex === index contentItem: Text { text: combo.textAt(choice.index) - color: combo.theme.text + color: choice.highlighted ? "#132233" : combo.theme.text font: combo.font verticalAlignment: Text.AlignVCenter elide: Text.ElideRight @@ -63,7 +65,7 @@ ComboBox { } background: Rectangle { radius: 5 - color: parent.highlighted ? combo.theme.accentSoft : combo.theme.raised + color: parent.highlighted ? "#f0f6ff" : combo.theme.raised } } From 2c3a2232a8893434ed690e94c40a3fd4958bc44a Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 09:26:23 +0700 Subject: [PATCH 12/18] test(studio): recognize multiline full table START without requiring manual Apply --- apps/arstack_studio/tests/check_operator_workspace.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index 00e8814e5..18077482a 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -173,7 +173,8 @@ 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('requestStartWithScenario(' in workflow + and '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 From e0b98ee55648b8c0a20e2ae1ac9ad3886f2257d0 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 09:28:21 +0700 Subject: [PATCH 13/18] fix(studio): reserve full 4-row intrinsic height for each stacked table; prevent field clipping --- apps/arstack_studio/qml/Main.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/arstack_studio/qml/Main.qml b/apps/arstack_studio/qml/Main.qml index dca75c9d5..bef157d2e 100644 --- a/apps/arstack_studio/qml/Main.qml +++ b/apps/arstack_studio/qml/Main.qml @@ -1076,8 +1076,8 @@ ApplicationWindow { // previously wasted below two cramped half-width panels. ColumnLayout { Layout.fillWidth: true - Layout.preferredHeight: root.compactLayout ? 398 : 426 - Layout.maximumHeight: root.compactLayout ? 398 : 426 + Layout.preferredHeight: root.compactLayout ? 438 : 460 + Layout.maximumHeight: root.compactLayout ? 438 : 460 spacing: 8 SignalMatrix { id: voltageMatrix From 3ce4beac4df1918c46a40f24d0c62a8bf0cc4993 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 09:28:37 +0700 Subject: [PATCH 14/18] test(studio): assert non-clipping vertical matrix height --- apps/arstack_studio/tests/check_operator_workspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index 18077482a..dfa98933d 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -54,7 +54,7 @@ def main() -> None: require("requestStartWithScenario(" in before_stop and "requestStart()" not in before_stop, "each physical START must use the current GUI 4I+4V table, not stale persisted firmware setpoints") - require("Layout.preferredHeight: root.compactLayout ? 398 : 426" in main_qml + require("Layout.preferredHeight: root.compactLayout ? 438 : 460" in main_qml and "ColumnLayout {\n Layout.fillWidth: true" in main_qml and main_qml.index("id: voltageMatrix") < main_qml.index("id: currentMatrix"), "Voltage and Current must be full-width, vertical and ordered") From c5d886c7469329070117c03d3b4f22391ce3f979 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 09:37:09 +0700 Subject: [PATCH 15/18] fix(studio): responsive vertical Voltage/Current with compact short-window fallback --- apps/arstack_studio/qml/Main.qml | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/apps/arstack_studio/qml/Main.qml b/apps/arstack_studio/qml/Main.qml index bef157d2e..3cca99ac2 100644 --- a/apps/arstack_studio/qml/Main.qml +++ b/apps/arstack_studio/qml/Main.qml @@ -1071,14 +1071,20 @@ ApplicationWindow { } } - // Two full-width channel tables read top to bottom. - // This keeps numeric columns usable and uses the space - // previously wasted below two cramped half-width panels. - ColumnLayout { + // Use stacked full-width Voltage/Current in the + // normal operator window; keep a two-column fallback + // only in short viewports so all controls remain + // accessible at the supported 960x620 minimum. + GridLayout { + readonly property bool stacked: root.height >= 720 + columns: stacked ? 1 : 2 Layout.fillWidth: true - Layout.preferredHeight: root.compactLayout ? 438 : 460 - Layout.maximumHeight: root.compactLayout ? 438 : 460 - spacing: 8 + Layout.preferredHeight: stacked + ? (root.compactLayout ? 438 : 460) + : (root.compactLayout ? 214 : 224) + Layout.maximumHeight: Layout.preferredHeight + rowSpacing: 8 + columnSpacing: 9 SignalMatrix { id: voltageMatrix enabled: !workflowBar.startQueued From 2356e37432382195f991ad0098f7bf7fe7888e36 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 09:37:30 +0700 Subject: [PATCH 16/18] test(studio): assert stacked full-size or compact fallback geometry with no clipped PTP/quality controls --- apps/arstack_studio/src/main.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/arstack_studio/src/main.cpp b/apps/arstack_studio/src/main.cpp index 13518af84..b4067ccb5 100644 --- a/apps/arstack_studio/src/main.cpp +++ b/apps/arstack_studio/src/main.cpp @@ -82,8 +82,17 @@ struct UiBounds { return false; } } - if (!(voltage.area.right() <= current.area.left() + tolerance - && tray.area.top() >= voltage.area.bottom() - tolerance + // The full-size operator view deliberately stacks Voltage ABOVE Current. + // At the supported 960x620 minimum a two-column fallback retains all + // controls inside the viewport instead of hiding the PTP/quality rows. + const bool stacked = size.height() >= 720; + const bool matricesOrdered = stacked + ? (voltage.area.bottom() <= current.area.top() + tolerance && + std::abs(voltage.area.left() - current.area.left()) <= tolerance) + : (voltage.area.right() <= current.area.left() + tolerance); + const qreal matrixBottom = std::max(voltage.area.bottom(), current.area.bottom()); + if (!(matricesOrdered + && tray.area.top() >= matrixBottom - tolerance && quality.area.top() >= tray.area.bottom() - tolerance && quality.area.bottom() <= surface.area.bottom() + tolerance)) { qCritical().noquote() << "Studio UI panel order/overlap violation" << size From 988faf60ab45f59a1802b79650508f56e33d9328 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 09:37:51 +0700 Subject: [PATCH 17/18] test(studio): protect vertical normal-height and compact short-height layouts --- apps/arstack_studio/tests/check_operator_workspace.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index dfa98933d..b49e8fbab 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -54,10 +54,11 @@ def main() -> None: require("requestStartWithScenario(" in before_stop and "requestStart()" not in before_stop, "each physical START must use the current GUI 4I+4V table, not stale persisted firmware setpoints") - require("Layout.preferredHeight: root.compactLayout ? 438 : 460" in main_qml - and "ColumnLayout {\n Layout.fillWidth: true" in main_qml + require("readonly property bool stacked: root.height >= 720" in main_qml + and "columns: stacked ? 1 : 2" in main_qml + and "Layout.preferredHeight: stacked" in main_qml and main_qml.index("id: voltageMatrix") < main_qml.index("id: currentMatrix"), - "Voltage and Current must be full-width, vertical and ordered") + "Voltage must stack above Current at normal height, and keep a non-clipping short-window fallback") require('root.startPtpRole("SOURCE")' in main_qml and 'root.defaultPtpAttempted = true' in main_qml and 'property bool phasorDockVisible: true' in main_qml From 679da6ccfd7aecabb01432a02888a9ecc5426fe3 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 09:38:07 +0700 Subject: [PATCH 18/18] build(studio): include cmath explicitly for viewport alignment tolerance --- apps/arstack_studio/src/main.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/arstack_studio/src/main.cpp b/apps/arstack_studio/src/main.cpp index b4067ccb5..7e3d54cae 100644 --- a/apps/arstack_studio/src/main.cpp +++ b/apps/arstack_studio/src/main.cpp @@ -30,6 +30,7 @@ #include #include +#include #include #include