From ff2a94a710aa50da4143e6cd10607e34d65b0b7a Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 16:49:34 +0700 Subject: [PATCH 1/8] fix(layout): 41/59 compact split, equal phasor-waveform height and locale-safe decimal parsing --- apps/arstack_studio/qml/Main.qml | 71 ++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/apps/arstack_studio/qml/Main.qml b/apps/arstack_studio/qml/Main.qml index c8a46b6e8..d7097c5bc 100644 --- a/apps/arstack_studio/qml/Main.qml +++ b/apps/arstack_studio/qml/Main.qml @@ -431,16 +431,20 @@ ApplicationWindow { } function parseOperatorNumber(text) { + // Dot and comma are alternative DECIMAL separators, irrespective of + // Windows/keyboard locale. Never guess thousands separators or + // silently reinterpret ambiguous mixed punctuation. var normalized = String(text).trim() - .replace(/\s/g, "") + .replace(/[\s\u00a0]/g, "") .replace(/degrees?/ig, "") .replace(/deg/ig, "") .replace(/°/g, "") .replace(/Hz/ig, "") .replace(/[AV]$/i, "") - if (normalized.indexOf(",") >= 0 && normalized.indexOf(".") < 0) - normalized = normalized.replace(",", ".") - if (!normalized.length || normalized === "+" || normalized === "-" || normalized === ".") + if (normalized.indexOf(",") !== -1 && normalized.indexOf(".") !== -1) + return NaN + normalized = normalized.replace(/,/g, ".") + if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(normalized)) return NaN return Number(normalized) } @@ -1005,10 +1009,29 @@ ApplicationWindow { } ColumnLayout { + id: mainWorkspaceLayout anchors.fill: parent anchors.margins: 8 spacing: 8 + // Passive parent-level tap: clicking an unused surface also commits + // an edited channel field. A tap on that same field keeps editing. + // Child controls retain ownership of their own clicks/key handling. + TapHandler { + acceptedButtons: Qt.LeftButton + gesturePolicy: TapHandler.DragThreshold + onTapped: function(point) { + const editor = root.activeFocusItem + if (!editor || (editor.objectName !== "matrixMagnitudeEditor" && + editor.objectName !== "matrixPhaseEditor")) return + const local = editor.mapFromItem(mainWorkspaceLayout, + point.position.x, point.position.y) + if (local.x < 0 || local.y < 0 || + local.x >= editor.width || local.y >= editor.height) + editor.focus = false + } + } + WorkflowBar { id: workflowBar objectName: "studioWorkflowBar" @@ -1065,9 +1088,13 @@ ApplicationWindow { objectName: "studioInjectionSurface" SplitView.fillWidth: true SplitView.fillHeight: true - SplitView.minimumWidth: root.compactLayout ? 500 : 540 - SplitView.preferredWidth: root.compactLayout ? 600 : 690 - SplitView.maximumWidth: 750 + // Proportional initial balance. The edit table is a + // narrow control surface, not a full-width spreadsheet. + // Splitter remains operator-draggable within safe limits. + SplitView.minimumWidth: 405 + SplitView.preferredWidth: Math.max(420, + Math.min(485, Math.round(workspaceSplit.width * 0.41))) + SplitView.maximumWidth: 535 theme: studioTheme ColumnLayout { @@ -1118,10 +1145,10 @@ ApplicationWindow { anchors.fill: parent anchors.leftMargin: 11 anchors.rightMargin: 11 - spacing: 8 + spacing: root.compactLayout ? 4 : 8 Label { - text: "Frequency" + text: root.compactLayout ? "Freq" : "Frequency" color: studioTheme.textSoft font.family: root.uiFont font.pixelSize: 10 @@ -1141,7 +1168,8 @@ ApplicationWindow { theme: studioTheme monoFont: root.monoFont compact: root.compactLayout - implicitWidth: 90 + implicitWidth: root.compactLayout ? 76 : 90 + Layout.preferredWidth: root.compactLayout ? 76 : 90 text: "50.000" suffixText: "Hz" enabled: true @@ -1168,14 +1196,15 @@ ApplicationWindow { } } - CalmButton { theme: studioTheme; uiFont: root.uiFont; text: "0"; implicitWidth: 44; toolTipText: "DC"; onClicked: root.setFrequencyValue(0) } - CalmButton { theme: studioTheme; uiFont: root.uiFont; text: "50"; implicitWidth: 44; onClicked: root.setFrequencyValue(50) } - CalmButton { theme: studioTheme; uiFont: root.uiFont; text: "60"; implicitWidth: 44; onClicked: root.setFrequencyValue(60) } + CalmButton { theme: studioTheme; uiFont: root.uiFont; text: "0"; implicitWidth: root.compactLayout ? 32 : 44; toolTipText: "DC"; onClicked: root.setFrequencyValue(0) } + CalmButton { theme: studioTheme; uiFont: root.uiFont; text: "50"; implicitWidth: root.compactLayout ? 32 : 44; onClicked: root.setFrequencyValue(50) } + CalmButton { theme: studioTheme; uiFont: root.uiFont; text: "60"; implicitWidth: root.compactLayout ? 32 : 44; onClicked: root.setFrequencyValue(60) } Rectangle { width: 1; height: 24; color: studioTheme.lineSoft } StudioLinkSwitch { theme: studioTheme uiFont: root.uiFont enabled: root.signalFrequency > 0 && !workflowBar.startQueued + text: root.compactLayout ? "Link A/B/C" : "3-phase link" checked: root.phaseLink onToggled: { // Link is an editing preference, not a wire @@ -1296,8 +1325,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.preferredWidth: Math.max(490, + Math.round(workspaceSplit.width * 0.59)) + SplitView.minimumWidth: 360 SplitView.maximumWidth: 680 SplitView.fillHeight: true orientation: Qt.Vertical @@ -1317,10 +1347,9 @@ ApplicationWindow { DockFrame { visible: root.phasorDockVisible && !root.phasorDetached SplitView.fillWidth: true - SplitView.minimumHeight: 170 - SplitView.preferredHeight: 225 - SplitView.maximumHeight: 265 - SplitView.fillHeight: false + SplitView.minimumHeight: 210 + SplitView.preferredHeight: 310 + SplitView.fillHeight: true theme: studioTheme titleText: "Phasor" statusText: "" @@ -1357,8 +1386,8 @@ ApplicationWindow { DockFrame { visible: root.waveformDockVisible && !root.waveformDetached SplitView.fillWidth: true - SplitView.minimumHeight: 170 - SplitView.preferredHeight: 328 + SplitView.minimumHeight: 210 + SplitView.preferredHeight: 310 SplitView.fillHeight: true theme: studioTheme titleText: "Waveform" From f2e4332431398a34905720d2a1fa9e07439fe70a Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 16:50:24 +0700 Subject: [PATCH 2/8] fix(matrix): atomic Enter/Tab/blur/outside commit; no 90ms TX while typing; restore invalid input --- apps/arstack_studio/qml/SignalMatrix.qml | 147 +++++++++++------------ 1 file changed, 68 insertions(+), 79 deletions(-) diff --git a/apps/arstack_studio/qml/SignalMatrix.qml b/apps/arstack_studio/qml/SignalMatrix.qml index 20477ffd8..9de7bd899 100644 --- a/apps/arstack_studio/qml/SignalMatrix.qml +++ b/apps/arstack_studio/qml/SignalMatrix.qml @@ -27,9 +27,8 @@ 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. + // START flushes the same editor-commit path as Enter/Tab/blur. + // A complete value is sent once, never during partial keyboard input. function flushPendingEdits() { for (var i = 0; i < rowRepeater.count; ++i) { const row = rowRepeater.itemAt(i) @@ -130,27 +129,44 @@ Rectangle { property bool selected: matrix.controller.activeSignal === signalRow.sid property alias magnitudeEditor: magnitudeField property alias phaseEditor: phaseField - 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 + // Editing draft belongs to the field until commit. Never + // queue SET commands with a 90ms debounce while typing. + property bool magnitudeDirty: false + property bool phaseDirty: false + function commitMagnitude() { + if (!magnitudeDirty) return true + const value = matrix.controller.parseOperatorNumber(magnitudeField.text) + magnitudeDirty = false + if (!matrix.controller.validMagnitude(matrix.groupIndex, value)) { + magnitudeField.text = mag.toFixed(3) + magnitudeField.invalidInput = false + matrix.controller.showMessage( + sid + ": invalid magnitude. Previous value restored. Use 0.5 or 0,5.", true) + 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 + magnitudeField.invalidInput = false + magnitudeField.text = value.toFixed(3) + if (value === mag) return true + return matrix.controller.editSignal(matrix.groupIndex, rowIndex, "magnitude", value) + } + function commitPhase() { + if (!phaseDirty) return true + const value = matrix.controller.parseOperatorNumber(phaseField.text) + phaseDirty = false + if (!matrix.controller.validPhase(value)) { + phaseField.text = angle.toFixed(2) + phaseField.invalidInput = false + matrix.controller.showMessage( + sid + ": invalid phase. Previous angle restored. Use 0.5 or 0,5.", true) + return false } - return true + phaseField.invalidInput = false + phaseField.text = value.toFixed(2) + if (value === angle) return true + return matrix.controller.editSignal(matrix.groupIndex, rowIndex, "phase", value) + } + function flushPendingEdits() { + return commitMagnitude() && commitPhase() } // Align with the field edge, not the left side of the table. @@ -237,19 +253,8 @@ Rectangle { HoverHandler { id: rowHover } - Timer { - id: magnitudeApplyTimer - interval: 90 - repeat: false - onTriggered: matrix.controller.editSignal(matrix.groupIndex, signalRow.rowIndex, "magnitude", signalRow.pendingMagnitude) - } - Timer { - id: phaseApplyTimer - interval: 90 - repeat: false - onTriggered: matrix.controller.editSignal(matrix.groupIndex, signalRow.rowIndex, "phase", signalRow.pendingPhase) - } - + // No per-keystroke timers. Enter, blur, Tab and START each + // invoke the same idempotent editor commit functions. onMagChanged: if (!magnitudeField.activeFocus) magnitudeField.text = mag.toFixed(3) onAngleChanged: if (!phaseField.activeFocus) phaseField.text = angle.toFixed(2) @@ -286,6 +291,7 @@ Rectangle { NumericField { id: magnitudeField + objectName: "matrixMagnitudeEditor" ToolTip.visible: hovered && !activeFocus && !magnitudeMenu.visible && !phaseMenu.visible ToolTip.delay: 650 ToolTip.text: "Right-click · Shift+F10" @@ -306,31 +312,16 @@ Rectangle { if (activeFocus) { matrix.controller.selectSignal(matrix.groupIndex, signalRow.rowIndex) text = signalRow.mag.toFixed(3) + signalRow.magnitudeDirty = false selectAll() - } + } else signalRow.commitMagnitude() } onTextEdited: { - var value = matrix.controller.parseOperatorNumber(text) - if (matrix.controller.validMagnitude(matrix.groupIndex, value)) { - invalidInput = false - signalRow.pendingMagnitude = value - magnitudeApplyTimer.restart() - } else invalidInput = true - } - onEditingFinished: { - var value = matrix.controller.parseOperatorNumber(text) - if (!matrix.controller.validMagnitude(matrix.groupIndex, value)) { - magnitudeApplyTimer.stop() - text = signalRow.mag.toFixed(3) - invalidInput = false - matrix.controller.showMessage(signalRow.sid + " value is outside the supported range.", true) - } else { - signalRow.pendingMagnitude = value - magnitudeApplyTimer.stop() - matrix.controller.editSignal(matrix.groupIndex, signalRow.rowIndex, "magnitude", value) - text = value.toFixed(3) - } + signalRow.magnitudeDirty = true + const value = matrix.controller.parseOperatorNumber(text) + invalidInput = !matrix.controller.validMagnitude(matrix.groupIndex, value) } + onEditingFinished: signalRow.commitMagnitude() Keys.onPressed: function(event) { if (event.key === Qt.Key_Menu || (event.key === Qt.Key_F10 && (event.modifiers & Qt.ShiftModifier))) { @@ -340,7 +331,13 @@ Rectangle { matrix.controller.navigate(matrix.groupIndex, signalRow.rowIndex, 0, event.key) event.accepted = true } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - matrix.controller.focusCell(matrix.groupIndex, signalRow.rowIndex + 1, 0) + if (signalRow.commitMagnitude()) + matrix.controller.focusCell(matrix.groupIndex, signalRow.rowIndex + 1, 0) + event.accepted = true + } else if (event.key === Qt.Key_Escape) { + signalRow.magnitudeDirty = false + magnitudeField.invalidInput = false + magnitudeField.text = signalRow.mag.toFixed(3) event.accepted = true } } @@ -348,6 +345,7 @@ Rectangle { NumericField { id: phaseField + objectName: "matrixPhaseEditor" ToolTip.visible: hovered && !activeFocus && !magnitudeMenu.visible && !phaseMenu.visible ToolTip.delay: 650 ToolTip.text: "Right-click · Shift+F10" @@ -369,31 +367,16 @@ Rectangle { if (activeFocus) { matrix.controller.selectSignal(matrix.groupIndex, signalRow.rowIndex) text = signalRow.angle.toFixed(2) + signalRow.phaseDirty = false selectAll() - } + } else signalRow.commitPhase() } onTextEdited: { - var value = matrix.controller.parseOperatorNumber(text) - if (matrix.controller.validPhase(value)) { - invalidInput = false - signalRow.pendingPhase = value - phaseApplyTimer.restart() - } else invalidInput = true - } - onEditingFinished: { - var value = matrix.controller.parseOperatorNumber(text) - if (!matrix.controller.validPhase(value)) { - phaseApplyTimer.stop() - text = signalRow.angle.toFixed(2) - invalidInput = false - matrix.controller.showMessage(signalRow.sid + " phase is outside the supported range.", true) - } else { - signalRow.pendingPhase = value - phaseApplyTimer.stop() - matrix.controller.editSignal(matrix.groupIndex, signalRow.rowIndex, "phase", value) - text = value.toFixed(2) - } + signalRow.phaseDirty = true + const value = matrix.controller.parseOperatorNumber(text) + invalidInput = !matrix.controller.validPhase(value) } + onEditingFinished: signalRow.commitPhase() Keys.onPressed: function(event) { if (event.key === Qt.Key_Menu || (event.key === Qt.Key_F10 && (event.modifiers & Qt.ShiftModifier))) { @@ -403,7 +386,13 @@ Rectangle { matrix.controller.navigate(matrix.groupIndex, signalRow.rowIndex, 1, event.key) event.accepted = true } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - matrix.controller.focusCell(matrix.groupIndex, signalRow.rowIndex + 1, 1) + if (signalRow.commitPhase()) + matrix.controller.focusCell(matrix.groupIndex, signalRow.rowIndex + 1, 1) + event.accepted = true + } else if (event.key === Qt.Key_Escape) { + signalRow.phaseDirty = false + phaseField.invalidInput = false + phaseField.text = signalRow.angle.toFixed(2) event.accepted = true } } From 2f6c3973931aa4691ebda1dcbfd96bd65c496e39 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 16:51:22 +0700 Subject: [PATCH 3/8] test(ui): enforce compact 41/59 split, equal previews, decimal normalization and single editor commit --- .../tests/check_operator_workspace.py | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index aa459e8ae..afdb4561f 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -196,12 +196,21 @@ def main() -> None: and 'startWatchdog' not in source("WorkflowBar.qml"), "SV STOP must remain actionable while the acknowledged scenario transaction is pending") require("function flushPendingEdits()" in matrix - and "magnitudeApplyTimer.stop()" in matrix - and "phaseApplyTimer.stop()" in matrix + and "function commitMagnitude()" in matrix + and "function commitPhase()" in matrix + and "return commitMagnitude() && commitPhase()" in matrix + and "magnitudeApplyTimer" not in matrix + and "phaseApplyTimer" not in matrix + and "onEditingFinished: signalRow.commitMagnitude()" in matrix + and "onEditingFinished: signalRow.commitPhase()" in matrix + and "else signalRow.commitMagnitude()" in matrix + and "else signalRow.commitPhase()" in matrix + and "onTextEdited:" in matrix + and "onTriggered: matrix.controller.editSignal" not 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") + "Each numeric edit must commit only on Enter/Tab/blur/START, never via keyboard debounce") 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"), @@ -544,8 +553,11 @@ 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 "Math.round(workspaceSplit.width * 0.41)" in responsive_main + and responsive_main.count("SplitView.preferredHeight: 310") == 2 + and responsive_main.count("SplitView.fillHeight: true") >= 3 + and "SplitView.maximumHeight: 265" not 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 @@ -554,6 +566,19 @@ def main() -> None: (STUDIO / "CMakeLists.txt").read_text(encoding="utf-8") and "bar.device.running ? \"RUN\" : \"STOP\"" in ribbon_layout, "Studio default workbench must balance channels and charts; physical RUN/STOP uses supplied bolt") + require("function parseOperatorNumber(text)" in responsive_main + and 'normalized = normalized.replace(/,/g, ".")' in responsive_main + and "normalized.indexOf(\",\") !== -1 && normalized.indexOf(\".\") !== -1" in responsive_main + and "objectName: \"matrixMagnitudeEditor\"" in matrix + and "objectName: \"matrixPhaseEditor\"" in matrix + and "id: mainWorkspaceLayout" in responsive_main + and "TapHandler {" in responsive_main + and "editor.focus = false" in responsive_main + and "Previous value restored" in matrix + and "if (value === mag) return true" in matrix + and "if (value === angle) return true" in matrix, + "Accept comma/dot decimals, commit on outside click and restore invalid drafts without duplicate SET") + require("property bool startIntentPending: false" in start_flow and "String(device.bootId)" in start_flow and "startIntentDeadline = Date.now() + 6500" in start_flow From 1bd2003400bb4614b8a90728ea9a67f2797ca6d1 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 16:53:45 +0700 Subject: [PATCH 4/8] test(matrix): allow explicit context-menu SET, forbid obsolete keystroke debounce only --- 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 afdb4561f..f30011dc4 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -206,7 +206,7 @@ def main() -> None: and "else signalRow.commitMagnitude()" in matrix and "else signalRow.commitPhase()" in matrix and "onTextEdited:" in matrix - and "onTriggered: matrix.controller.editSignal" not in matrix + and "interval: 90" not in matrix and "currentMatrix.flushPendingEdits()" in main_qml and "voltageMatrix.flushPendingEdits()" in main_qml and "if (!scenario) return" in source("WorkflowBar.qml"), From a1f4abfdbb9a20f13982c454588c72cb321f8c29 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 16:56:02 +0700 Subject: [PATCH 5/8] test(layout): retire old 265px phasor cap; require equal-height resizable visual panels --- 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 f30011dc4..c84062b9a 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -386,9 +386,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 "SplitView.maximumHeight: 265" not in main_qml + and main_qml.count("SplitView.minimumHeight: 210") == 2, + "compact timing strip and balanced resizable 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 f1a42b4d3e51d8f52ad6b5b6ff58d83bf54ec53f Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 16:57:39 +0700 Subject: [PATCH 6/8] fix(layout): hide redundant selection readback at narrow widths to prevent Quality row overflow --- apps/arstack_studio/qml/Main.qml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/arstack_studio/qml/Main.qml b/apps/arstack_studio/qml/Main.qml index d7097c5bc..8afc08927 100644 --- a/apps/arstack_studio/qml/Main.qml +++ b/apps/arstack_studio/qml/Main.qml @@ -1085,6 +1085,7 @@ ApplicationWindow { } SurfacePanel { + id: injectionPanel objectName: "studioInjectionSurface" SplitView.fillWidth: true SplitView.fillHeight: true @@ -1297,6 +1298,9 @@ ApplicationWindow { Rectangle { width: 6; height: 6; radius: 3; color: studioTheme.accent } Label { text: root.activeSignal; color: studioTheme.textSoft; font.family: root.monoFont; font.pixelSize: studioTheme.labelSize; font.weight: Font.Bold } Label { + // Redundant selection readback yields space + // to Quality controls in the narrow table. + visible: injectionPanel.width >= 505 text: root.signalFrequency === 0 ? root.activeMagnitude.toFixed(3) + " " + root.activeUnit + " DC" : root.activeMagnitude.toFixed(3) + " " + root.activeUnit + " RMS · " + root.activePhase.toFixed(2) + "°" @@ -1304,7 +1308,12 @@ ApplicationWindow { font.family: root.uiFont font.pixelSize: 9 } - Rectangle { width: 1; height: 20; color: studioTheme.lineSoft } + Rectangle { + visible: injectionPanel.width >= 505 + width: 1 + height: 20 + color: studioTheme.lineSoft + } QualityEditor { enabled: !workflowBar.startQueued Layout.fillWidth: true From 8c1e8110367736362890d0e39da7327b68329dd3 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 16:57:54 +0700 Subject: [PATCH 7/8] test(layout): require Quality controls fit the narrow setpoint matrix without redundant label --- apps/arstack_studio/tests/check_operator_workspace.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index c84062b9a..b6145ab86 100644 --- a/apps/arstack_studio/tests/check_operator_workspace.py +++ b/apps/arstack_studio/tests/check_operator_workspace.py @@ -558,6 +558,8 @@ def main() -> None: and "Math.round(workspaceSplit.width * 0.41)" in responsive_main and responsive_main.count("SplitView.preferredHeight: 310") == 2 and responsive_main.count("SplitView.fillHeight: true") >= 3 + and "id: injectionPanel" in responsive_main + and responsive_main.count("visible: injectionPanel.width >= 505") == 2 and "SplitView.maximumHeight: 265" not in responsive_main and "electric_bolt_run.svg" in ribbon_layout and "electric_bolt_stop.svg" in ribbon_layout From 506846834bc4110594b60e7bfd45576d36f449c5 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 21 Oct 2026 17:12:54 +0700 Subject: [PATCH 8/8] ci(studio): publish exact-head direct setup EXE before long Windows installation acceptance --- .github/workflows/arstack-studio-native-preview.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/arstack-studio-native-preview.yml b/.github/workflows/arstack-studio-native-preview.yml index 5f7213f6f..d6888665f 100644 --- a/.github/workflows/arstack-studio-native-preview.yml +++ b/.github/workflows/arstack-studio-native-preview.yml @@ -263,6 +263,18 @@ jobs: } $checks | Set-Content (Join-Path $PWD 'dist\SHA256SUMS.txt') -Encoding ascii + # Publish a small single-installer artifact BEFORE long Windows upgrade + # acceptance. Operators can bench the exact source SHA without waiting + # for the full portable+firmware ZIP, while CI still validates upgrades. + - name: Upload standalone Windows setup EXE early + uses: actions/upload-artifact@v4 + with: + name: arstack-studio-setup-exe-${{ github.event.pull_request.number || github.run_number }}-${{ github.sha }} + path: dist/ARStack-Studio-Preview-win-x64-setup.exe + if-no-files-found: error + retention-days: 7 + compression-level: 0 + - name: Inno clean install, running-app upgrade and clean uninstall shell: pwsh run: |