diff --git a/apps/arstack_studio/qml/Main.qml b/apps/arstack_studio/qml/Main.qml index c8a46b6e8..09b38d2fa 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,11 +1065,30 @@ 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: 445 + SplitView.preferredWidth: root.width < 1100 ? 450 : 465 + SplitView.maximumWidth: 490 theme: studioTheme + // 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: injectionFocusSink.forceActiveFocus() + } + ColumnLayout { anchors.fill: parent anchors.margins: root.compactLayout ? 11 : 13 @@ -1296,9 +1315,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 +1336,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 +1377,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" 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)) { diff --git a/apps/arstack_studio/tests/check_operator_workspace.py b/apps/arstack_studio/tests/check_operator_workspace.py index aa459e8ae..dfdb82332 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"), @@ -377,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") @@ -544,8 +549,12 @@ 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: 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 + 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