Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
8d577ba
fix(studio): always start SV from the exact current 4I+4V table snapshot
masarray Oct 18, 2026
d611ce1
feat(studio): stack voltage/current full width and reclaim unused inj…
masarray Oct 18, 2026
886363a
fix(studio): give SMV Injector brand proper icon and 15px typography
masarray Oct 18, 2026
cce5d83
fix(studio): center ribbon icon/caption consistently both axes
masarray Oct 18, 2026
e52cdf8
fix(studio): white neutral hover uses dark caption and centered content
masarray Oct 18, 2026
c203f92
fix(studio): one stable checkbox indicator geometry on hover
masarray Oct 18, 2026
fe7ad13
feat(studio): PTP SOURCE auto-start once on verified board with AUTO …
masarray Oct 18, 2026
9a3ea46
test(studio): lock direct snapshot START, visible brand, responsive s…
masarray Oct 18, 2026
a5c0f86
test(studio): match wrapped unconditional START snapshot expression
masarray Oct 18, 2026
ed1cc2f
fix(studio): eliminate native hover checkbox overlay; single custom i…
masarray Oct 18, 2026
d2eb3e3
test(studio): update second legacy START contract to unconditional mu…
masarray Oct 18, 2026
63b0c20
test(studio-windows): enforce vertical voltage/current ordering and s…
masarray Oct 18, 2026
6840959
fix(studio): stacked 4I+4V matrices scroll safely at minimum window/D…
masarray Oct 18, 2026
e18dea3
test(studio): verify clipped scrolling 4I+4V layout at 100/150/175% DPI
masarray Oct 18, 2026
c86bb64
test(studio): enforce compact-scrollable stacked signal matrices
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
6 changes: 4 additions & 2 deletions apps/arstack_studio/qml/CalmButton.qml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Button {
tone === "success" ? (hovered ? "#1e5c43" : "#194d38") :
tone === "danger" ? (hovered ? "#523039" : theme.redSoft) :
tone === "accent" ? (hovered ? "#1d3c5e" : theme.accentSoft) :
(down ? "#1a2530" : hovered ? theme.raisedHover : "#141c25")
(down ? "#d6e3f1" : hovered ? "#f0f5fa" : "#141c25")
readonly property color activeEdge: !enabled ? theme.lineSoft :
tone === "success" ? "#347a59" :
tone === "danger" ? "#86434b" :
Expand All @@ -34,11 +34,13 @@ Button {
readonly property color activeText: !enabled ? theme.muted2 :
tone === "success" ? "#d6f4e6" :
tone === "danger" ? "#ffdce0" :
tone === "accent" ? "#dcebff" : theme.textSoft
tone === "accent" ? "#dcebff" : hovered ? "#142334" : theme.textSoft

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use dark text while a neutral button is pressed

When a focused neutral button is activated with Space or Enter while the pointer is elsewhere, down selects the new light #d6e3f1 fill but hovered remains false, so this expression retains the light theme.textSoft caption and makes it nearly unreadable. Treat down like hovered when selecting the dark caption color.

Useful? React with 👍 / 👎.


contentItem: Row {
spacing: control.iconSource.toString().length > 0 ? 7 : 0
anchors.centerIn: parent
width: implicitWidth
height: implicitHeight
Image {
visible: control.iconSource.toString().length > 0
width: control.iconSize
Expand Down
32 changes: 22 additions & 10 deletions apps/arstack_studio/qml/Main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,9 @@ ApplicationWindow {
// Session-only operator intent. Never persist across application launches:
// an explicit PTP ON can be restored after a firmware reflash/reconnect
// without introducing an uncontrolled power-on publisher.
property bool ptpDesiredForSession: false
property bool ptpDesiredForSession: true
property string ptpDesiredRole: "SOURCE"
property bool ptpRestoreAfterFirmware: false
property bool ptpRestoreAfterFirmware: true
property int ptpRestoreChecks: 0
property bool restoringScenario: false
property bool offlinePreviewRunning: false
Expand Down Expand Up @@ -198,6 +198,10 @@ ApplicationWindow {
function onDeviceVerifiedChanged() {
root.pendingApply = true
root.offlinePreviewRunning = false
if (root.ptpDesiredForSession && !device.ptpRunning) {
root.ptpRestoreChecks = 0
root.ptpRestoreAfterFirmware = true
}
}
}

Expand All @@ -218,7 +222,7 @@ ApplicationWindow {
Timer {
interval: 300
repeat: true
running: root.ptpRestoreAfterFirmware
running: root.ptpRestoreAfterFirmware && device.deviceVerified
onTriggered: {
// Time-bounded one-shot restoration: never retry indefinitely.
root.ptpRestoreChecks += 1
Expand Down Expand Up @@ -1049,17 +1053,24 @@ ApplicationWindow {
}
}

RowLayout {
ScrollView {
id: signalMatrixScroll
objectName: "studioSignalMatricesScroll"
Layout.fillWidth: true
Layout.preferredHeight: root.compactLayout ? 214 : 224
Layout.maximumHeight: root.compactLayout ? 214 : 224
spacing: 9
Layout.fillHeight: true
Layout.minimumHeight: 120
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ColumnLayout {
width: signalMatrixScroll.availableWidth
spacing: 9
SignalMatrix {
id: voltageMatrix
enabled: !workflowBar.startQueued
objectName: "studioVoltageMatrix"
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredHeight: root.compactLayout ? 200 : 215
Layout.minimumHeight: 190
theme: studioTheme
controller: root
device: device
Expand All @@ -1078,7 +1089,8 @@ ApplicationWindow {
enabled: !workflowBar.startQueued
objectName: "studioCurrentMatrix"
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredHeight: root.compactLayout ? 200 : 215
Layout.minimumHeight: 190
theme: studioTheme
controller: root
device: device
Expand All @@ -1093,6 +1105,7 @@ ApplicationWindow {
compact: root.compactLayout
}

}
}

StudioTimingTray {
Expand Down Expand Up @@ -1136,7 +1149,6 @@ ApplicationWindow {
compact: root.compactLayout
}
}
Item { Layout.fillHeight: true }
}
}

Expand Down
16 changes: 8 additions & 8 deletions apps/arstack_studio/qml/ModernRibbon.qml
Original file line number Diff line number Diff line change
Expand Up @@ -47,24 +47,24 @@ Rectangle {
spacing: 6

RowLayout {
Layout.preferredWidth: bar.compact ? 105 : 124
Layout.preferredWidth: bar.compact ? 150 : 178
Layout.alignment: Qt.AlignVCenter
spacing: 8

Rectangle {
width: 28
height: 28
width: 40
height: 40
radius: 7
color: "#0d151d"
border.width: 1
border.color: bar.theme.lineSoft
Image {
anchors.centerIn: parent
width: 15
height: 15
width: 26
height: 26
source: Qt.resolvedUrl("../assets/material/smv_injector.svg")
sourceSize.width: 30
sourceSize.height: 30
sourceSize.width: 52
sourceSize.height: 52
}
}

Expand All @@ -73,7 +73,7 @@ Rectangle {
text: "SMV Injector"
color: bar.theme.text
font.family: bar.uiFont
font.pixelSize: 11
font.pixelSize: 15
font.weight: Font.DemiBold
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
Expand Down
6 changes: 5 additions & 1 deletion apps/arstack_studio/qml/RibbonAction.qml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ Button {
: tone === "accent" || checked ? "#e1f1ff" : theme.textSoft
contentItem: ColumnLayout {
spacing: 3
anchors.centerIn: parent
width: parent.width
height: implicitHeight
Image {
visible: control.iconSource.toString().length > 0
source: control.hovered && control.enabled ? control.hoverPath : control.iconSource
Expand All @@ -56,7 +59,8 @@ Button {
text: control.text
color: control.textColor
font: control.font
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
Expand Down
7 changes: 6 additions & 1 deletion apps/arstack_studio/qml/StudioDarkCheckBox.qml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ CheckBox {
property var theme
property string uiFont: "Inter"
implicitHeight: 30
hoverEnabled: true
// Suppress native Windows hover background/second checkbox glyph.
// The single custom indicator below owns both normal and hovered states.
background: Item {}
spacing: 9
font.family: uiFont
font.pixelSize: 11
Expand All @@ -20,7 +24,7 @@ CheckBox {
color: control.checked ? control.theme.accent : control.theme.surface2
border.width: 1
border.color: control.checked ? control.theme.accent
: control.activeFocus ? control.theme.textSoft : control.theme.line
: (control.activeFocus || control.hovered) ? control.theme.textSoft : control.theme.line
Text {
anchors.centerIn: parent
visible: control.checked
Expand All @@ -32,6 +36,7 @@ CheckBox {
}
contentItem: Text {
leftPadding: control.indicator.width + control.spacing
horizontalAlignment: Text.AlignLeft
text: control.text
font: control.font
color: control.enabled ? control.theme.textSoft : control.theme.muted
Expand Down
8 changes: 5 additions & 3 deletions apps/arstack_studio/qml/WorkflowBar.qml
Original file line number Diff line number Diff line change
Expand Up @@ -281,9 +281,11 @@ SurfacePanel {
controller.showMessage(ribbon.startReason(), false)
return
}
const accepted = controller.pendingApply
? smartSession.requestStartWithScenario(controller.startScenarioSnapshot())
: smartSession.requestStart()
// Always bind START to the current table snapshot. A previous live
// session may have cleared pendingApply, but STOP/START must never
// recycle stale waveform values from the device.
const accepted = smartSession.requestStartWithScenario(
controller.startScenarioSnapshot())
Comment on lines +287 to +288

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 Restore the workspace contract's START assertion

The Qt CI job directly runs check_operator_workspace.py, whose existing assertion searches for the contiguous text requestStartWithScenario(controller.startScenarioSnapshot()); splitting this call across these lines makes that test fail with “reconnect must not auto-transmit, but one-click Start must stage a bounded scenario.” Keep the call contiguous or update the older assertion to recognize the multiline form so the required regression gate passes.

AGENTS.md reference: AGENTS.md:L249-L257

Useful? React with 👍 / 👎.

if (!accepted) {
controller.pendingApply = true
controller.showMessage("Start rejected. Check device status.", true)
Expand Down
23 changes: 17 additions & 6 deletions apps/arstack_studio/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,15 @@ struct UiBounds {
const auto surface = findUiBounds(window, "studioInjectionSurface");
const auto voltage = findUiBounds(window, "studioVoltageMatrix");
const auto current = findUiBounds(window, "studioCurrentMatrix");
const auto matrixScroll = findUiBounds(window, "studioSignalMatricesScroll");
const auto tray = findUiBounds(window, "studioTimingTray");
const auto quality = findUiBounds(window, "studioQualityRow");
const std::array<std::pair<const char*, UiBounds>, 6> areas{{
// The stacked matrices live in a vertical ScrollView. On 960x620
// windows/DPI, their unscrolled content can legitimately extend below
// the viewport; only the CLIPPED scroll viewport itself must be inside.
const std::array<std::pair<const char*, UiBounds>, 5> areas{{
{"toolbar", bar}, {"injection workspace", surface},
{"voltage matrix", voltage}, {"current matrix", current},
{"matrix scroll viewport", matrixScroll},
{"PTP timing tray", tray}, {"quality editor", quality}
}};
for (const auto& [label, item] : areas) {
Expand All @@ -82,10 +86,17 @@ struct UiBounds {
return false;
}
}
if (!(voltage.area.right() <= current.area.left() + tolerance
&& tray.area.top() >= voltage.area.bottom() - tolerance
&& quality.area.top() >= tray.area.bottom() - tolerance
&& quality.area.bottom() <= surface.area.bottom() + tolerance)) {
// Stacked full-width 4I+4V layout: Voltage above Current; both
// must remain within the injection surface and above the PTP tray.
if (!(voltage.visible && current.visible &&
voltage.area.width() > 100 && current.area.width() > 100 &&
voltage.area.top() >= matrixScroll.area.top() - tolerance &&
voltage.area.bottom() <= current.area.top() + tolerance &&
current.area.left() >= matrixScroll.area.left() - tolerance &&
current.area.right() <= matrixScroll.area.right() + tolerance &&
matrixScroll.area.bottom() <= tray.area.top() + tolerance &&
quality.area.top() >= tray.area.bottom() - tolerance &&
quality.area.bottom() <= surface.area.bottom() + tolerance)) {
qCritical().noquote() << "Studio UI panel order/overlap violation" << size
<< voltage.area << current.area << tray.area << quality.area;
return false;
Expand Down
25 changes: 23 additions & 2 deletions apps/arstack_studio/tests/check_operator_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,25 @@ def main() -> None:
require("scenarioSnapshot()" in main_qml and "restoreLocalScenario()" in main_qml
and "startScenarioSnapshot()" in main_qml and "offlinePreviewRunning" in main_qml,
"offline-first scenario must persist and support one-click Start")
require('property bool ptpDesiredForSession: true' in main_qml
and 'property bool ptpRestoreAfterFirmware: true' in main_qml
and 'property bool phasorDockVisible: true' in main_qml
and 'property bool waveformDockVisible: true' in main_qml,
"operator defaults must keep PTP, phasor and waveform enabled")
require('objectName: "studioSignalMatricesScroll"' in main_qml
and 'ScrollBar.horizontal.policy: ScrollBar.AlwaysOff' in main_qml
and main_qml.index('id: voltageMatrix') < main_qml.index('id: currentMatrix'),
"full-width voltage/current tables should scroll vertically when compact")
require('const accepted = smartSession.requestStartWithScenario(' in workflow
and 'smartSession.requestStart()' not in workflow.split('function requestStart() {')[1].split('function requestStop() {')[0],
"START must always transmit the exact 4I+4V model snapshot")
brand = source("ModernRibbon.qml")
require("font.pixelSize: 15" in brand and "width: 40" in brand,
"SMV Injector brand should be readable at desktop scale")
button = source("CalmButton.qml")
require('hovered ? "#f0f5fa"' in button
and 'hovered ? "#142334"' in button,
"white hover must use a dark readable caption")
require("Apply to board" not in main_qml and "requestApplyDraft()" not in main_qml,
"operator UI must not expose a redundant Apply-to-board action")
require('saveScenarioTimer.stop()' in main_qml
Expand All @@ -116,7 +135,8 @@ def main() -> None:
and 'enabled: !workflowBar.startQueued; onActivated: root.zeroAll()' in main_qml,
"scenario editors and shortcuts must be frozen during a pending START snapshot")
require("controller.pendingApply = true" in workflow
and "requestStartWithScenario(controller.startScenarioSnapshot())" in workflow
and "smartSession.requestStartWithScenario(" in workflow
and "controller.startScenarioSnapshot())" 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 @@ -145,7 +165,8 @@ def main() -> None:
and 'toolTipText: "Advanced"' not in ribbon
and 'ToolTip.text: "smpSynch mode"' not in timing_ui,
"captioned operator controls must not duplicate labels as tooltips")
require('requestStartWithScenario(controller.startScenarioSnapshot())' in workflow
require('smartSession.requestStartWithScenario(' in workflow
and 'controller.startScenarioSnapshot())' 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