From f1e86a4d0338f4f7c9e4d3294d0b2daa6d3472ba Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 11:00:21 +0700 Subject: [PATCH 01/17] fix(studio): synchronously commit focused and debounced phasor cells before START snapshot --- apps/arstack_studio/qml/SignalMatrix.qml | 31 ++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/apps/arstack_studio/qml/SignalMatrix.qml b/apps/arstack_studio/qml/SignalMatrix.qml index 1c84e2b43..101e9c2d3 100644 --- a/apps/arstack_studio/qml/SignalMatrix.qml +++ b/apps/arstack_studio/qml/SignalMatrix.qml @@ -27,6 +27,17 @@ Rectangle { return [theme.phaseA, theme.phaseB, theme.phaseC, theme.phaseN][index] } + // Commit any last keystroke synchronously before constructing an ordered + // START scenario. The 90ms edit debounce must never make the board + // receive old angles while the table already displays new ones. + function flushPendingEdits() { + for (var i = 0; i < rowRepeater.count; ++i) { + const row = rowRepeater.itemAt(i) + if (row && !row.flushPendingEdits()) return false + } + return true + } + function focusCell(row, column) { var rowItem = rowRepeater.itemAt(row) if (!rowItem) @@ -124,6 +135,26 @@ Rectangle { property real pendingMagnitude: mag property real pendingPhase: angle + function flushPendingEdits() { + if (magnitudeApplyTimer.running || magnitudeField.activeFocus) { + const magValue = matrix.controller.parseOperatorNumber(magnitudeField.text) + if (!matrix.controller.validMagnitude(matrix.groupIndex, magValue)) + return false + magnitudeApplyTimer.stop() + if (!matrix.controller.editSignal(matrix.groupIndex, rowIndex, "magnitude", magValue)) + return false + } + if (phaseApplyTimer.running || phaseField.activeFocus) { + const phaseValue = matrix.controller.parseOperatorNumber(phaseField.text) + if (!matrix.controller.validPhase(phaseValue)) + return false + phaseApplyTimer.stop() + if (!matrix.controller.editSignal(matrix.groupIndex, rowIndex, "phase", phaseValue)) + return false + } + return true + } + // 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. From d92b194b5c00ec7ff786ff860c637a1adce444ff Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 11:00:59 +0700 Subject: [PATCH 02/17] fix(studio): flush displayed phase before START and expand stacked 4I/4V matrices --- apps/arstack_studio/qml/Main.qml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/arstack_studio/qml/Main.qml b/apps/arstack_studio/qml/Main.qml index 72ac77a79..3d137e0ea 100644 --- a/apps/arstack_studio/qml/Main.qml +++ b/apps/arstack_studio/qml/Main.qml @@ -508,6 +508,12 @@ ApplicationWindow { return ok } function startScenarioSnapshot() { + // The table editors debounce keyboard edits for live preview. Flush + // them synchronously before creating the ordered board START payload. + if (!currentMatrix.flushPendingEdits() || !voltageMatrix.flushPendingEdits()) { + showMessage("Invalid signal input: correct highlighted field before Start.", true) + return null + } // START always serializes the same ListModels that draw the table, // including all eight current phase angles and enabled flags. var draft = scenarioSnapshot() @@ -1074,9 +1080,11 @@ ApplicationWindow { ColumnLayout { Layout.fillWidth: true - Layout.preferredHeight: root.compactLayout ? 352 : 384 - Layout.maximumHeight: root.compactLayout ? 352 : 384 - spacing: 7 + // Use all available vertical workspace rather than + // a fixed 384px island with empty space below. + Layout.fillHeight: true + Layout.minimumHeight: root.compactLayout ? 335 : 355 + spacing: 8 SignalMatrix { id: voltageMatrix enabled: !workflowBar.startQueued From 5064a2be9d93650f4c288d25594e509bbd39e698 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 11:01:15 +0700 Subject: [PATCH 03/17] fix(studio): reject START if an active table editor has invalid or unsent phase --- apps/arstack_studio/qml/WorkflowBar.qml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/arstack_studio/qml/WorkflowBar.qml b/apps/arstack_studio/qml/WorkflowBar.qml index bbc858ac4..943258148 100644 --- a/apps/arstack_studio/qml/WorkflowBar.qml +++ b/apps/arstack_studio/qml/WorkflowBar.qml @@ -284,8 +284,11 @@ SurfacePanel { // A START always transmits the current 4I+4V table snapshot. // A previous run may have cleared pendingApply; a bare START could // then repeat old board angles while Studio displays edited values. - const accepted = smartSession.requestStartWithScenario( - controller.startScenarioSnapshot()) + // Validate/flush the visible 4I+4V table before entering the + // exclusive device START transaction; no stale debounced angle. + const scenario = controller.startScenarioSnapshot() + if (!scenario) return + const accepted = smartSession.requestStartWithScenario(scenario) if (!accepted) { controller.pendingApply = true controller.showMessage("Start rejected. Check device status.", true) From 6648260623f388ba1daf5b4ec882a13428934ffe Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 11:01:53 +0700 Subject: [PATCH 04/17] fix(studio): readable brand, centered ribbon/buttons and stable checkbox hover geometry --- apps/arstack_studio/qml/ModernRibbon.qml | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/apps/arstack_studio/qml/ModernRibbon.qml b/apps/arstack_studio/qml/ModernRibbon.qml index 6776d73df..27fdd8139 100644 --- a/apps/arstack_studio/qml/ModernRibbon.qml +++ b/apps/arstack_studio/qml/ModernRibbon.qml @@ -47,24 +47,26 @@ Rectangle { spacing: 6 RowLayout { - Layout.preferredWidth: bar.compact ? 152 : 182 + Layout.preferredWidth: bar.compact ? 190 : 224 Layout.alignment: Qt.AlignVCenter spacing: 8 Rectangle { - width: 38 - height: 38 - radius: 9 + Layout.preferredWidth: 48 + Layout.preferredHeight: 48 + width: 48 + height: 48 + radius: 10 color: "#0d151d" border.width: 1 border.color: bar.theme.lineSoft Image { anchors.centerIn: parent - width: 25 - height: 25 + width: 34 + height: 34 source: Qt.resolvedUrl("../assets/material/smv_injector.svg") - sourceSize.width: 30 - sourceSize.height: 30 + sourceSize.width: 68 + sourceSize.height: 68 } } @@ -73,7 +75,7 @@ Rectangle { text: "SMV Injector" color: bar.theme.text font.family: bar.uiFont - font.pixelSize: 15 + font.pixelSize: bar.compact ? 17 : 19 font.weight: Font.Bold elide: Text.ElideRight verticalAlignment: Text.AlignVCenter From 38409edb9a95e14df67fc41cfcd126f3ad874e4c Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 11:01:56 +0700 Subject: [PATCH 05/17] fix(studio): readable brand, centered ribbon/buttons and stable checkbox hover geometry --- apps/arstack_studio/qml/RibbonAction.qml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/arstack_studio/qml/RibbonAction.qml b/apps/arstack_studio/qml/RibbonAction.qml index ae84160c4..e6f378657 100644 --- a/apps/arstack_studio/qml/RibbonAction.qml +++ b/apps/arstack_studio/qml/RibbonAction.qml @@ -39,8 +39,12 @@ Button { : tone === "danger" ? "#ffe3e6" : tone === "accent" || checked ? "#e1f1ff" : theme.textSoft contentItem: ColumnLayout { - spacing: 3 - anchors.centerIn: parent + spacing: 4 + // Controls own the content rectangle; centering is declarative + // on the layout itself, rather than anchoring Qt Controls content. + anchors.fill: parent + anchors.margins: 5 + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter Image { visible: control.iconSource.toString().length > 0 source: control.hovered && control.enabled ? control.hoverPath : control.iconSource @@ -59,6 +63,7 @@ Button { font: control.font Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter Layout.fillWidth: true + Layout.preferredHeight: 18 horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter elide: Text.ElideRight From 384dce689c524c3316875d28db3ef421bcfda5ef Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 11:02:02 +0700 Subject: [PATCH 06/17] fix(studio): readable brand, centered ribbon/buttons and stable checkbox hover geometry --- apps/arstack_studio/qml/CalmButton.qml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/arstack_studio/qml/CalmButton.qml b/apps/arstack_studio/qml/CalmButton.qml index eb418851a..aad55a8a1 100644 --- a/apps/arstack_studio/qml/CalmButton.qml +++ b/apps/arstack_studio/qml/CalmButton.qml @@ -39,9 +39,10 @@ Button { tone === "accent" ? "#dcebff" : theme.textSoft contentItem: Row { - // The same item owns both the icon and text; no native Button - // hover glyph can appear independently. + // Both caption and icon belong to the same centered Control item. spacing: control.iconSource.toString().length > 0 ? 7 : 0 + implicitWidth: childrenRect.width + implicitHeight: Math.max(18, childrenRect.height) anchors.centerIn: parent Image { visible: control.iconSource.toString().length > 0 From af35b66bc73f7db21ea6f765ab09ff899220f710 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 11:02:08 +0700 Subject: [PATCH 07/17] fix(studio): readable brand, centered ribbon/buttons and stable checkbox hover geometry --- apps/arstack_studio/qml/StudioDarkCheckBox.qml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/arstack_studio/qml/StudioDarkCheckBox.qml b/apps/arstack_studio/qml/StudioDarkCheckBox.qml index fd2794a78..710ff203a 100644 --- a/apps/arstack_studio/qml/StudioDarkCheckBox.qml +++ b/apps/arstack_studio/qml/StudioDarkCheckBox.qml @@ -8,13 +8,18 @@ CheckBox { property var theme property string uiFont: "Inter" implicitHeight: 30 + implicitWidth: Math.max(28, contentItem.implicitWidth + 30) + leftPadding: 1 + rightPadding: 2 spacing: 9 hoverEnabled: true font.family: uiFont font.pixelSize: 11 indicator: Rectangle { - implicitWidth: 18 - implicitHeight: 18 + implicitWidth: 20 + implicitHeight: 20 + width: 20 + height: 20 x: control.leftPadding y: (control.height - height) / 2 radius: 5 @@ -27,13 +32,14 @@ CheckBox { visible: control.checked text: "\u2713" color: control.theme.bg - font.pixelSize: 13 + font.pixelSize: 14 font.weight: Font.Bold } } contentItem: Text { horizontalAlignment: Text.AlignLeft leftPadding: control.indicator.width + control.spacing + implicitHeight: control.height text: control.text font: control.font color: control.enabled ? control.theme.textSoft : control.theme.muted From 353a98be065c7e2a79ab625d8bb32e798f14bd00 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 11:02:31 +0700 Subject: [PATCH 08/17] fix(ribbon): center icon+caption vertically with symmetric stretch spacers --- apps/arstack_studio/qml/RibbonAction.qml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/arstack_studio/qml/RibbonAction.qml b/apps/arstack_studio/qml/RibbonAction.qml index e6f378657..8e5bf1ff3 100644 --- a/apps/arstack_studio/qml/RibbonAction.qml +++ b/apps/arstack_studio/qml/RibbonAction.qml @@ -42,9 +42,7 @@ Button { spacing: 4 // Controls own the content rectangle; centering is declarative // on the layout itself, rather than anchoring Qt Controls content. - anchors.fill: parent - anchors.margins: 5 - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + Item { Layout.fillHeight: true } Image { visible: control.iconSource.toString().length > 0 source: control.hovered && control.enabled ? control.hoverPath : control.iconSource @@ -68,6 +66,7 @@ Button { verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } + Item { Layout.fillHeight: true } } background: Rectangle { radius: 9 From f7e708eb24e8d543acfa570604625705c3f88cfb Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 11:02:59 +0700 Subject: [PATCH 09/17] test(studio): protect phase start snapshot, vertical matrix, branding and consistent hover --- .../tests/check_operator_workspace.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index afea41801..c9d6ba077 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -60,6 +60,28 @@ def main() -> None: and "root.ptpDesiredForSession = false" in main_qml, "PTP user intent restoration must be bounded, explicit and cancellable") matrix = source("SignalMatrix.qml") + require("function flushPendingEdits()" in matrix + and "magnitudeApplyTimer.stop()" in matrix + and "phaseApplyTimer.stop()" in matrix + and "currentMatrix.flushPendingEdits()" in main_qml + and "voltageMatrix.flushPendingEdits()" in main_qml + and "if (!scenario) return" in source("WorkflowBar.qml"), + "START must commit last visible I/V angle before ordered scenario serialization") + require("Layout.fillHeight: true" in main_qml + and "Layout.maximumHeight: root.compactLayout ? 352 : 384" not in main_qml + and main_qml.index("id: voltageMatrix") < main_qml.index("id: currentMatrix"), + "Voltage and Current need a vertical, flexible-height editor workspace") + ribbon_qml = source("ModernRibbon.qml") + require("Layout.preferredWidth: bar.compact ? 190 : 224" in ribbon_qml + and "font.pixelSize: bar.compact ? 17 : 19" in ribbon_qml, + "SMV Injector logo/caption must be instrument-sized, not micro branding") + checkbox_qml = source("StudioDarkCheckBox.qml") + require("width: 20" in checkbox_qml and "height: 20" in checkbox_qml, + "checkbox geometry must remain unchanged by hover") + require("hovered ? \"#edf5ff\"" in source("CalmButton.qml") + and "hovered ? \"#142334\"" in source("CalmButton.qml"), + "light hover background must always pair with dark caption") + menu = source("StudioActionMenu.qml") cmake = (STUDIO / "CMakeLists.txt").read_text(encoding="utf-8") From 81207dbeb076e4fd4eca1255cba5f9dd244ba41b Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 11:05:27 +0700 Subject: [PATCH 10/17] test(studio): validate flexible vertical matrix instead of obsolete fixed height --- apps/arstack_studio/tests/check_operator_workspace.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index c9d6ba077..64d3b5186 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -37,9 +37,10 @@ def main() -> None: and "root.pendingApply" in main_qml and "pendingApply = true" in main_qml, "table edits must expose unsent setpoints without claiming wire readback") - require("Layout.preferredHeight: root.compactLayout ? 352 : 384" in main_qml + require("Layout.minimumHeight: root.compactLayout ? 335 : 355" in main_qml + and "Layout.fillHeight: true" in main_qml and "ColumnLayout {" in main_qml, - "voltage and current must stack vertically to use the workspace") + "voltage and current must stack vertically and fill the workspace") require('property bool ptpDefaultStartAttempted: false' in main_qml and 'root.ptpDefaultStartAttempted = true' in main_qml and '!root.startPtpRole("SOURCE")' in main_qml, From 2ae922ec5be587e455f11636381a69ed8cff53aa Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 11:06:37 +0700 Subject: [PATCH 11/17] =?UTF-8?q?test(studio):=20regress=20actual=20IA=20?= =?UTF-8?q?=E2=88=9230=C2=B0/IB=20=E2=88=92150=C2=B0/IC=20+90=C2=B0=20wire?= =?UTF-8?q?=20SET=20encoding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../arstack_studio/src/DeterministicSessionHarness.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/arstack_studio/src/DeterministicSessionHarness.cpp b/apps/arstack_studio/src/DeterministicSessionHarness.cpp index bd5fe2233..da467e2b9 100644 --- a/apps/arstack_studio/src/DeterministicSessionHarness.cpp +++ b/apps/arstack_studio/src/DeterministicSessionHarness.cpp @@ -644,6 +644,13 @@ class DeterministicSessionHarness final { QStringLiteral("Ib"), 1.0, -120.0, 0U, 1000.0, 100.0, 50.0); const QString ic = DeviceController::formatSignalCommand( QStringLiteral("Ic"), 1.0, 120.0, 0U, 1000.0, 100.0, 50.0); + // Operator table −30/−150/+90 must NEVER be serialized as 0/−120/+120. + const QString iaOperator = DeviceController::formatSignalCommand( + QStringLiteral("Ia"), 1.0, -30.0, 0U, 1000.0, 100.0, 50.0); + const QString ibOperator = DeviceController::formatSignalCommand( + QStringLiteral("Ib"), 1.0, -150.0, 0U, 1000.0, 100.0, 50.0); + const QString icOperator = DeviceController::formatSignalCommand( + QStringLiteral("Ic"), 1.0, 90.0, 0U, 1000.0, 100.0, 50.0); const QString badId = DeviceController::formatSignalCommand( QStringLiteral("IX"), 1.0, 0.0, 0U, 1000.0, 100.0, 50.0); const QString overflow = DeviceController::formatSignalCommand( @@ -653,6 +660,9 @@ class DeterministicSessionHarness final { return ia == QStringLiteral("SET IA 1000 0 0") && ib == QStringLiteral("SET IB 1000 -120000 0") && ic == QStringLiteral("SET IC 1000 120000 0") && + iaOperator == QStringLiteral("SET IA 1000 -30000 0") && + ibOperator == QStringLiteral("SET IB 1000 -150000 0") && + icOperator == QStringLiteral("SET IC 1000 90000 0") && badId.isEmpty() && overflow.isEmpty() && dcNegative == QStringLiteral("SET IA -1000 0 0"); } From 83df3ab40becb6405202c576f8a34aa37ede9149 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 11:08:26 +0700 Subject: [PATCH 12/17] test(studio): require validated table snapshot before START admission --- 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 64d3b5186..c290f2b4f 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -155,7 +155,8 @@ def main() -> None: "scenario editors and shortcuts must be frozen during a pending START snapshot") require("controller.pendingApply = true" in workflow and "smartSession.requestStartWithScenario(" in workflow - and "controller.startScenarioSnapshot())" in workflow + and "const scenario = controller.startScenarioSnapshot()" in workflow + and "if (!scenario) return" 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 18c11c3818abd36ef66099e0c8e9dc0066de0bc3 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 11:10:24 +0700 Subject: [PATCH 13/17] test(studio): enforce one-click validated START without obsolete inline expression --- 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 c290f2b4f..63cb8f4cc 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -186,7 +186,8 @@ def main() -> None: and 'ToolTip.text: "smpSynch mode"' not in timing_ui, "captioned operator controls must not duplicate labels as tooltips") require('smartSession.requestStartWithScenario(' in workflow - and 'controller.startScenarioSnapshot())' in workflow + and 'const scenario = controller.startScenarioSnapshot()' in workflow + and 'if (!scenario) return' 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 0c152695eb1b535f3e7684a821913da6e6bb43e7 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 11:14:44 +0700 Subject: [PATCH 14/17] fix(qml): remove read-only Row implicit size assignments; preserve centered content --- apps/arstack_studio/qml/CalmButton.qml | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/arstack_studio/qml/CalmButton.qml b/apps/arstack_studio/qml/CalmButton.qml index aad55a8a1..882788bfe 100644 --- a/apps/arstack_studio/qml/CalmButton.qml +++ b/apps/arstack_studio/qml/CalmButton.qml @@ -41,8 +41,6 @@ Button { contentItem: Row { // Both caption and icon belong to the same centered Control item. spacing: control.iconSource.toString().length > 0 ? 7 : 0 - implicitWidth: childrenRect.width - implicitHeight: Math.max(18, childrenRect.height) anchors.centerIn: parent Image { visible: control.iconSource.toString().length > 0 From c1135bba38c2e69175bfe11bb0d70a6b9413167c Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 11:15:00 +0700 Subject: [PATCH 15/17] fix(qml): do not assign generated Text implicitHeight in checkbox content --- apps/arstack_studio/qml/StudioDarkCheckBox.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/arstack_studio/qml/StudioDarkCheckBox.qml b/apps/arstack_studio/qml/StudioDarkCheckBox.qml index 710ff203a..a50529053 100644 --- a/apps/arstack_studio/qml/StudioDarkCheckBox.qml +++ b/apps/arstack_studio/qml/StudioDarkCheckBox.qml @@ -39,7 +39,6 @@ CheckBox { contentItem: Text { horizontalAlignment: Text.AlignLeft leftPadding: control.indicator.width + control.spacing - implicitHeight: control.height text: control.text font: control.font color: control.enabled ? control.theme.textSoft : control.theme.muted From c619f72a538a996801652f76a0860c4991bd38af Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 11:23:06 +0700 Subject: [PATCH 16/17] fix(studio): keep stacked editors elastic and within 960x620 Windows viewport --- apps/arstack_studio/qml/Main.qml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/arstack_studio/qml/Main.qml b/apps/arstack_studio/qml/Main.qml index 3d137e0ea..4ac8d2779 100644 --- a/apps/arstack_studio/qml/Main.qml +++ b/apps/arstack_studio/qml/Main.qml @@ -1083,7 +1083,11 @@ ApplicationWindow { // Use all available vertical workspace rather than // a fixed 384px island with empty space below. Layout.fillHeight: true - Layout.minimumHeight: root.compactLayout ? 335 : 355 + Layout.preferredHeight: root.compactLayout ? 350 : 380 + // Allow 960x620 minimum viewport to shrink without + // clipping timing and quality rows; on larger + // viewports this column expands into spare space. + Layout.minimumHeight: root.compactLayout ? 180 : 210 spacing: 8 SignalMatrix { id: voltageMatrix From da0cd1717388d165274543089b15e063a698d1b7 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 18 Oct 2026 11:23:19 +0700 Subject: [PATCH 17/17] test(studio): require compressible editor min-height at 960x620 --- 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 63cb8f4cc..184b54d46 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -37,7 +37,7 @@ def main() -> None: and "root.pendingApply" in main_qml and "pendingApply = true" in main_qml, "table edits must expose unsent setpoints without claiming wire readback") - require("Layout.minimumHeight: root.compactLayout ? 335 : 355" in main_qml + require("Layout.minimumHeight: root.compactLayout ? 180 : 210" in main_qml and "Layout.fillHeight: true" in main_qml and "ColumnLayout {" in main_qml, "voltage and current must stack vertically and fill the workspace")