Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
f1e86a4
fix(studio): synchronously commit focused and debounced phasor cells …
masarray Oct 18, 2026
d92b194
fix(studio): flush displayed phase before START and expand stacked 4I…
masarray Oct 18, 2026
5064a2b
fix(studio): reject START if an active table editor has invalid or un…
masarray Oct 18, 2026
6648260
fix(studio): readable brand, centered ribbon/buttons and stable check…
masarray Oct 18, 2026
38409ed
fix(studio): readable brand, centered ribbon/buttons and stable check…
masarray Oct 18, 2026
384dce6
fix(studio): readable brand, centered ribbon/buttons and stable check…
masarray Oct 18, 2026
af35b66
fix(studio): readable brand, centered ribbon/buttons and stable check…
masarray Oct 18, 2026
353a98b
fix(ribbon): center icon+caption vertically with symmetric stretch sp…
masarray Oct 18, 2026
f7e708e
test(studio): protect phase start snapshot, vertical matrix, branding…
masarray Oct 18, 2026
81207db
test(studio): validate flexible vertical matrix instead of obsolete f…
masarray Oct 18, 2026
2ae922e
test(studio): regress actual IA −30°/IB −150°/IC +90° wire SET encoding
masarray Oct 18, 2026
83df3ab
test(studio): require validated table snapshot before START admission
masarray Oct 18, 2026
18c11c3
test(studio): enforce one-click validated START without obsolete inli…
masarray Oct 18, 2026
0c15269
fix(qml): remove read-only Row implicit size assignments; preserve ce…
masarray Oct 18, 2026
c1135bb
fix(qml): do not assign generated Text implicitHeight in checkbox con…
masarray Oct 18, 2026
c619f72
fix(studio): keep stacked editors elastic and within 960x620 Windows …
masarray Oct 18, 2026
da0cd17
test(studio): require compressible editor min-height at 960x620
masarray Oct 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions apps/arstack_studio/qml/CalmButton.qml
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ 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
anchors.centerIn: parent
Image {
Expand Down
18 changes: 15 additions & 3 deletions apps/arstack_studio/qml/Main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -1074,9 +1080,15 @@ 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.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
enabled: !workflowBar.startQueued
Expand Down
20 changes: 11 additions & 9 deletions apps/arstack_studio/qml/ModernRibbon.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand All @@ -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
Expand Down
8 changes: 6 additions & 2 deletions apps/arstack_studio/qml/RibbonAction.qml
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ 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.
Item { Layout.fillHeight: true }
Image {
visible: control.iconSource.toString().length > 0
source: control.hovered && control.enabled ? control.hoverPath : control.iconSource
Expand All @@ -59,10 +61,12 @@ 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
}
Item { Layout.fillHeight: true }
}
background: Rectangle {
radius: 9
Expand Down
31 changes: 31 additions & 0 deletions apps/arstack_studio/qml/SignalMatrix.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
11 changes: 8 additions & 3 deletions apps/arstack_studio/qml/StudioDarkCheckBox.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,7 +32,7 @@ CheckBox {
visible: control.checked
text: "\u2713"
color: control.theme.bg
font.pixelSize: 13
font.pixelSize: 14
font.weight: Font.Bold
}
}
Expand Down
7 changes: 5 additions & 2 deletions apps/arstack_studio/qml/WorkflowBar.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions apps/arstack_studio/src/DeterministicSessionHarness.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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");
}
Expand Down
33 changes: 29 additions & 4 deletions apps/arstack_studio/tests/check_operator_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ? 180 : 210" 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,
Expand All @@ -60,6 +61,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"),
Comment on lines +71 to +73

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update all stale workspace assertions for the new flow

The new flexible-height assertion contradicts the existing check at lines 40–42, which still requires the removed Layout.preferredHeight; after correcting that, the checks at lines 155–158 and 186–188 still require the old inline controller.startScenarioSnapshot()) call. Running python3 apps/arstack_studio/tests/check_operator_workspace.py therefore exits immediately, and .github/workflows/arstack-studio-qt.yml runs this script in CI, so the commit cannot pass its Studio checks until all obsolete assertions are updated or removed.

Useful? React with 👍 / 👎.

"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")

Expand Down Expand Up @@ -132,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")
Expand Down Expand Up @@ -162,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
Expand Down
Loading