From 43f6a3359c5b2d0e11e7d3ff6aef0441d74e3765 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 18:42:51 +0700 Subject: [PATCH 1/9] fix(layout): cap matrix at 535px and give phasor/waveform equal resizable 1:1 height --- apps/arstack_studio/qml/Main.qml | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/apps/arstack_studio/qml/Main.qml b/apps/arstack_studio/qml/Main.qml index c8a46b6e8..8fb2c692c 100644 --- a/apps/arstack_studio/qml/Main.qml +++ b/apps/arstack_studio/qml/Main.qml @@ -8,7 +8,7 @@ import ARStack.Studio 1.0 ApplicationWindow { id: root width: 1150 - height: 790 + height: 800 minimumWidth: 960 minimumHeight: 620 visible: true @@ -1065,9 +1065,12 @@ ApplicationWindow { objectName: "studioInjectionSurface" SplitView.fillWidth: true SplitView.fillHeight: true - SplitView.minimumWidth: root.compactLayout ? 500 : 540 - SplitView.preferredWidth: root.compactLayout ? 600 : 690 - SplitView.maximumWidth: 750 + // Hard width budget prevents input columns from stretching + // at desktop sizes. Visualization owns spare horizontal + // space; operator may still resize within useful bounds. + SplitView.minimumWidth: 450 + SplitView.preferredWidth: root.width < 1100 ? 470 : 500 + SplitView.maximumWidth: 535 theme: studioTheme ColumnLayout { @@ -1296,9 +1299,9 @@ ApplicationWindow { SplitView { visible: (root.phasorDockVisible && !root.phasorDetached) || (root.waveformDockVisible && !root.waveformDetached) - SplitView.preferredWidth: root.compactLayout ? 450 : 470 - SplitView.minimumWidth: root.compactLayout ? 300 : 340 - SplitView.maximumWidth: 680 + SplitView.preferredWidth: Math.max(440, root.width - 540) + SplitView.minimumWidth: 410 + SplitView.fillWidth: true SplitView.fillHeight: true orientation: Qt.Vertical handle: Rectangle { @@ -1317,10 +1320,11 @@ ApplicationWindow { DockFrame { visible: root.phasorDockVisible && !root.phasorDetached SplitView.fillWidth: true - SplitView.minimumHeight: 170 - SplitView.preferredHeight: 225 - SplitView.maximumHeight: 265 - SplitView.fillHeight: false + // Phasor and waveform each own half of available + // height; no hidden 265px cap on the phasor plot. + SplitView.minimumHeight: 245 + SplitView.preferredHeight: 310 + SplitView.fillHeight: true theme: studioTheme titleText: "Phasor" statusText: "" @@ -1357,8 +1361,8 @@ ApplicationWindow { DockFrame { visible: root.waveformDockVisible && !root.waveformDetached SplitView.fillWidth: true - SplitView.minimumHeight: 170 - SplitView.preferredHeight: 328 + SplitView.minimumHeight: 245 + SplitView.preferredHeight: 310 SplitView.fillHeight: true theme: studioTheme titleText: "Waveform" From 448dfab9e64ff178e562bade3afb002cac4ad28d Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 18:43:16 +0700 Subject: [PATCH 2/9] fix(matrix): commit only at Enter/blur, validate as typed; never send partial edits on debounce --- apps/arstack_studio/qml/SignalMatrix.qml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/arstack_studio/qml/SignalMatrix.qml b/apps/arstack_studio/qml/SignalMatrix.qml index 20477ffd8..27db340f3 100644 --- a/apps/arstack_studio/qml/SignalMatrix.qml +++ b/apps/arstack_studio/qml/SignalMatrix.qml @@ -313,10 +313,15 @@ Rectangle { var value = matrix.controller.parseOperatorNumber(text) if (matrix.controller.validMagnitude(matrix.groupIndex, value)) { invalidInput = false + // Validate immediately, commit atomically only + // at Enter, Tab or focus-out. No 90ms race + // sends half-entered numeric values to board. signalRow.pendingMagnitude = value - magnitudeApplyTimer.restart() } else invalidInput = true } + // Qt Quick editingFinished fires on Return AND loss of + // active focus (Tab, other field, empty workbench click). + // Both decimal separators are parsed by controller. onEditingFinished: { var value = matrix.controller.parseOperatorNumber(text) if (!matrix.controller.validMagnitude(matrix.groupIndex, value)) { @@ -377,9 +382,11 @@ Rectangle { if (matrix.controller.validPhase(value)) { invalidInput = false signalRow.pendingPhase = value - phaseApplyTimer.restart() } else invalidInput = true } + // Qt Quick editingFinished fires on Return AND loss of + // active focus (Tab, other field, empty workbench click). + // Both decimal separators are parsed by controller. onEditingFinished: { var value = matrix.controller.parseOperatorNumber(text) if (!matrix.controller.validPhase(value)) { From 02050420efc499f66029272481d12bf1ce3109dd Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 18:43:46 +0700 Subject: [PATCH 3/9] fix(editor): commit edited cell when operator clicks empty injection workspace --- apps/arstack_studio/qml/Main.qml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/arstack_studio/qml/Main.qml b/apps/arstack_studio/qml/Main.qml index 8fb2c692c..d1fcf3ea5 100644 --- a/apps/arstack_studio/qml/Main.qml +++ b/apps/arstack_studio/qml/Main.qml @@ -1073,6 +1073,16 @@ ApplicationWindow { SplitView.maximumWidth: 535 theme: studioTheme + // Click on unoccupied injection panel space commits the + // currently focused table editor through editingFinished. + // Child input controls remain above this passive surface. + MouseArea { + anchors.fill: parent + z: 0 + acceptedButtons: Qt.LeftButton + onClicked: root.forceActiveFocus() + } + ColumnLayout { anchors.fill: parent anchors.margins: root.compactLayout ? 11 : 13 From e7c2fb705f7a58558474f98afcba5e28df1cc0d0 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 18:44:08 +0700 Subject: [PATCH 4/9] test(ui): enforce compact workbench, 1:1 chart space, and blur/Enter-only model commit --- apps/arstack_studio/tests/check_operator_workspace.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index aa459e8ae..72cbe76bd 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -198,6 +198,10 @@ def main() -> None: require("function flushPendingEdits()" in matrix and "magnitudeApplyTimer.stop()" in matrix and "phaseApplyTimer.stop()" in matrix + and "magnitudeApplyTimer.restart()" not in matrix + and "phaseApplyTimer.restart()" not in matrix + and matrix.count("onEditingFinished:") >= 2 + and "parseOperatorNumber(text)" in matrix and "currentMatrix.flushPendingEdits()" in main_qml and "voltageMatrix.flushPendingEdits()" in main_qml and "if (!scenario) return" in source("WorkflowBar.qml"), @@ -544,8 +548,10 @@ def main() -> None: ribbon_layout = source("ModernRibbon.qml") start_flow = source("WorkflowBar.qml") require("width: 1150" in responsive_main - and "SplitView.maximumWidth: 750" in responsive_main - and "SplitView.preferredWidth: root.compactLayout ? 450 : 470" in responsive_main + and "SplitView.maximumWidth: 535" in responsive_main + and "SplitView.preferredHeight: 310" in responsive_main + and "SplitView.maximumHeight: 265" not in responsive_main + and "onClicked: root.forceActiveFocus()" in responsive_main and "electric_bolt_run.svg" in ribbon_layout and "electric_bolt_stop.svg" in ribbon_layout and "assets/material/electric_bolt_run.svg" in From 2c3f568a776a9a0e8ba6681e4acdf5bf378ac5aa Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 18:46:07 +0700 Subject: [PATCH 5/9] test(layout): replace obsolete 265px phasor cap contract with equal plot height invariant --- 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 72cbe76bd..7554514a5 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -381,9 +381,10 @@ def main() -> None: 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 - and "SplitView.maximumHeight: 265" in main_qml - and "SplitView.fillHeight: false" in main_qml, - "compact timing strip and bounded phasor panel must avoid dead space") + and main_qml.count("SplitView.preferredHeight: 310") >= 2 + and main_qml.count("SplitView.minimumHeight: 245") >= 2 + and "SplitView.maximumHeight: 265" not in main_qml, + "compact timing strip and equal-height phasor/waveform must avoid dead space") require("TIME SYNC / SV SIMULATION" not in tray and "StateBadge {" not in tray, "device/PTP status must not be duplicated in the control tray") From 99b541a7c3dfd01d5204113978b8a3960462c5c1 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 18:50:59 +0700 Subject: [PATCH 6/9] fix(qml): use real Item focus sink for empty-space edit commit, not Window.forceActiveFocus --- apps/arstack_studio/qml/Main.qml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/arstack_studio/qml/Main.qml b/apps/arstack_studio/qml/Main.qml index d1fcf3ea5..d072ed833 100644 --- a/apps/arstack_studio/qml/Main.qml +++ b/apps/arstack_studio/qml/Main.qml @@ -1076,11 +1076,17 @@ ApplicationWindow { // Click on unoccupied injection panel space commits the // currently focused table editor through editingFinished. // Child input controls remain above this passive surface. + Item { + id: injectionFocusSink + width: 0 + height: 0 + focus: false + } MouseArea { anchors.fill: parent z: 0 acceptedButtons: Qt.LeftButton - onClicked: root.forceActiveFocus() + onClicked: injectionFocusSink.forceActiveFocus() } ColumnLayout { From 1647f3e59e9eca9eb78272bca5aab186da15dd2d Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 18:51:11 +0700 Subject: [PATCH 7/9] test(qml): require actual focusable Item target for click-outside commit --- 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 7554514a5..2ad40caaa 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -552,7 +552,8 @@ def main() -> None: and "SplitView.maximumWidth: 535" in responsive_main and "SplitView.preferredHeight: 310" in responsive_main and "SplitView.maximumHeight: 265" not in responsive_main - and "onClicked: root.forceActiveFocus()" in responsive_main + and "id: injectionFocusSink" in responsive_main + and "onClicked: injectionFocusSink.forceActiveFocus()" in responsive_main and "electric_bolt_run.svg" in ribbon_layout and "electric_bolt_stop.svg" in ribbon_layout and "assets/material/electric_bolt_run.svg" in From 8fffe89b86d7bb7a30a6d2fd663eba62d3b6d021 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 18:52:24 +0700 Subject: [PATCH 8/9] refine(layout): narrow matrix below screenshot-2 width (465px target, 490px cap) --- apps/arstack_studio/qml/Main.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/arstack_studio/qml/Main.qml b/apps/arstack_studio/qml/Main.qml index d072ed833..09b38d2fa 100644 --- a/apps/arstack_studio/qml/Main.qml +++ b/apps/arstack_studio/qml/Main.qml @@ -1068,9 +1068,9 @@ ApplicationWindow { // Hard width budget prevents input columns from stretching // at desktop sizes. Visualization owns spare horizontal // space; operator may still resize within useful bounds. - SplitView.minimumWidth: 450 - SplitView.preferredWidth: root.width < 1100 ? 470 : 500 - SplitView.maximumWidth: 535 + SplitView.minimumWidth: 445 + SplitView.preferredWidth: root.width < 1100 ? 450 : 465 + SplitView.maximumWidth: 490 theme: studioTheme // Click on unoccupied injection panel space commits the From bc999d3d01014f0eeb6df2b30be7071cd7070dc1 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 18:52:46 +0700 Subject: [PATCH 9/9] test(layout): enforce truly short 465px input panel and strict 490px cap --- 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 2ad40caaa..dfdb82332 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -549,7 +549,8 @@ def main() -> None: ribbon_layout = source("ModernRibbon.qml") start_flow = source("WorkflowBar.qml") require("width: 1150" in responsive_main - and "SplitView.maximumWidth: 535" in responsive_main + and "SplitView.maximumWidth: 490" in responsive_main + and "SplitView.preferredWidth: root.width < 1100 ? 450 : 465" in responsive_main and "SplitView.preferredHeight: 310" in responsive_main and "SplitView.maximumHeight: 265" not in responsive_main and "id: injectionFocusSink" in responsive_main