Skip to content
Open
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
6020785
fix(studio): rederive linked phasors from visible table and stack vol…
masarray Oct 18, 2026
e973ea5
fix(studio): always send visible table with START
masarray Oct 18, 2026
6770eec
fix(studio): size SMV Injector identity at desktop scale
masarray Oct 18, 2026
9869295
fix(studio): use dark centered text on all light Advanced button hovers
masarray Oct 18, 2026
f881249
fix(studio): center ribbon icon and captions in both axes
masarray Oct 18, 2026
cda23b9
fix(studio): keep one constant-size checkbox indicator on hover
masarray Oct 18, 2026
766afe0
fix(studio): center Advanced navigation and maintain contrast on hover
masarray Oct 18, 2026
31d23a6
feat(studio): default PTP SOURCE AUTO on verified connect and balance…
masarray Oct 18, 2026
c5a7170
test(studio): lock latest-table injection, PTP defaults and refreshed…
masarray Oct 18, 2026
87d44e0
test(studio): initialize workflow/ribbon fixtures before visual SSOT …
masarray Oct 18, 2026
f4041eb
test(studio): assert multiline unconditional START snapshot call inst…
masarray Oct 18, 2026
1a49569
fix(studio): remove native checkbox hover overlay behind fixed custom…
masarray Oct 18, 2026
2e16a79
style(studio): fit 4I+4V stacked matrices without wasted row height
masarray Oct 18, 2026
0922e8c
test(studio): validate real stacked Voltage→Current→PTP→Quality geome…
masarray Oct 18, 2026
9a42847
fix(studio): scroll stacked 4I+4V matrices at short Windows viewports…
masarray Oct 18, 2026
5041526
test(studio): verify stacked scroll viewport and unclipped PTP contro…
masarray Oct 18, 2026
c35c73f
test(studio): require scrollable 4I+4V stacks with native PTP tray vi…
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: 6 additions & 0 deletions apps/arstack_studio/qml/CalmButton.qml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ Button {
scale: down ? 0.988 : 1.0
Behavior on scale { NumberAnimation { duration: 70; easing.type: Easing.OutCubic } }

// Consistent desktop hover affordance: light fill always pairs with dark
// caption. Applies to Advanced panels because they share CalmButton.
readonly property color activeFill: !enabled ? "#10161d" :
hovered ? "#edf4fc" :
tone === "success" ? (hovered ? "#1e5c43" : "#194d38") :
tone === "danger" ? (hovered ? "#523039" : theme.redSoft) :
tone === "accent" ? (hovered ? "#1d3c5e" : theme.accentSoft) :
Expand All @@ -32,6 +35,7 @@ Button {
tone === "accent" ? "#315f8d" :
(activeFocus ? theme.accent : hovered ? "#3a4a5b" : theme.line)
readonly property color activeText: !enabled ? theme.muted2 :
hovered ? "#142334" :
tone === "success" ? "#d6f4e6" :
tone === "danger" ? "#ffdce0" :
tone === "accent" ? "#dcebff" : theme.textSoft
Expand All @@ -53,6 +57,8 @@ Button {
text: control.text
color: control.activeText
font: control.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
anchors.verticalCenter: parent.verticalCenter
Behavior on color { ColorAnimation { duration: 90 } }
}
Expand Down
7 changes: 5 additions & 2 deletions apps/arstack_studio/qml/ConfigurationHub.qml
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,18 @@ Item {
font.family: hub.uiFont
font.pixelSize: 11
font.weight: checked ? Font.DemiBold : Font.Medium
hoverEnabled: true
contentItem: Label {
text: parent.text
color: parent.checked ? hub.theme.text : hub.theme.muted
color: parent.hovered ? "#142334" :
parent.checked ? hub.theme.text : hub.theme.muted
font: parent.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.checked ? hub.theme.raised : "transparent"
color: parent.hovered ? "#edf4fc" :
parent.checked ? hub.theme.raised : "transparent"
radius: 6
border.width: parent.checked ? 1 : 0
border.color: hub.theme.line
Expand Down
78 changes: 66 additions & 12 deletions apps/arstack_studio/qml/Main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,15 @@ ApplicationWindow {

property string uiFont: interFont.status === FontLoader.Ready ? interFont.name : "Inter"
property string monoFont: uiFont
property bool phaseLink: false
property bool phaseLink: true
property bool profileDirty: false
property bool pendingApply: true
// 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
// Operator default: PTP SOURCE ON after verified connection; OFF overrides
// this for the remainder of this Studio session.
property bool ptpDesiredForSession: true
property string ptpDesiredRole: "SOURCE"
property bool ptpRestoreAfterFirmware: false
property int ptpRestoreChecks: 0
Expand Down Expand Up @@ -198,6 +200,11 @@ ApplicationWindow {
function onDeviceVerifiedChanged() {
root.pendingApply = true
root.offlinePreviewRunning = false
if (device.deviceVerified && root.ptpDesiredForSession &&
!device.ptpRunning) {
root.ptpRestoreChecks = 0
root.ptpRestoreAfterFirmware = true
Comment on lines +203 to +206

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 Route Advanced PTP controls through the intent owner

After a fresh connection defaults ptpDesiredForSession to true, an operator can explicitly stop PTP through PtpExpertPanel.qml's header switch or Stop button, both of which call session.requestStopPtp() directly and never clear this property. On the next unplug/replug or firmware reconnect, this handler still sees true and automatically restarts the PTP source, so OFF is not authoritative for the session as promised. Advanced start/stop actions need to update the same controller-owned intent used by the main tray.

AGENTS.md reference: AGENTS.md:L46-L50

Useful? React with 👍 / 👎.

}
}
}

Expand Down Expand Up @@ -251,9 +258,9 @@ ApplicationWindow {

ListModel {
id: currentModel
ListElement { signalId: "Ia"; magnitude: 1.000; phase: 0.0; enabled: true; quality: 0; traceColor: "#ff6673" }
ListElement { signalId: "Ib"; magnitude: 1.000; phase: -120.0; enabled: true; quality: 0; traceColor: "#e6b552" }
ListElement { signalId: "Ic"; magnitude: 1.000; phase: 120.0; enabled: true; quality: 0; traceColor: "#59a7ff" }
ListElement { signalId: "Ia"; magnitude: 1.000; phase: -30.0; enabled: true; quality: 0; traceColor: "#ff6673" }
ListElement { signalId: "Ib"; magnitude: 1.000; phase: -150.0; enabled: true; quality: 0; traceColor: "#e6b552" }
ListElement { signalId: "Ic"; magnitude: 1.000; phase: 90.0; enabled: true; quality: 0; traceColor: "#59a7ff" }
ListElement { signalId: "In"; magnitude: 0.000; phase: 0.0; enabled: true; quality: 0; traceColor: "#9a88df" }
}

Expand Down Expand Up @@ -444,6 +451,30 @@ ApplicationWindow {
for (var row = 0; row < 4; ++row) sendSignal(group, row)
}

function applyPhaseLink() {
if (!phaseLink || signalFrequency === 0) return
// Preserve each group's selected phase-A setpoint, and derive B/C
// from it consistently in BOTH the model and the next wire batch.
for (var group = 0; group < 2; ++group) {
var model = groupModel(group)
var base = model.get(0).phase
model.setProperty(1, "phase", normalizedAngle(base - 120))
model.setProperty(2, "phase", normalizedAngle(base + 120))
}
refreshPreview()
markScenarioDirty()
if (workflowBar.injectionRunning && workflowBar.session &&
workflowBar.session.liveControlReady && !pendingApply) {
// Explicit link changes during RUNNING are real setpoint edits.
// Don't silently leave the publisher at its previous angles.
var ok = sendLinkedGroup(1) && sendLinkedGroup(0)
if (!ok) {
pendingApply = true
showMessage("Phase values not sent; Stop then Start to apply.", true)
}
}
}

function editSignal(group, row, field, value) {
if ((field === "magnitude" && !validMagnitude(group, value)) ||
(field === "phase" && !validPhase(value)))
Expand Down Expand Up @@ -487,6 +518,8 @@ ApplicationWindow {
return ok
}
function startScenarioSnapshot() {
// The visible 4I+4V table is the only START setpoint authority.
// Capture it immediately before the worker queues its FIFO batch.
var draft = scenarioSnapshot()
draft.currentScale = currentScale
draft.voltageScale = voltageScale
Expand Down Expand Up @@ -1040,7 +1073,8 @@ ApplicationWindow {
text: "3-phase link"
onToggled: {
root.phaseLink = checked
root.markScenarioDirty()
if (checked) root.applyPhaseLink()
else root.markScenarioDirty()
}
font.family: root.uiFont
font.pixelSize: 9
Expand All @@ -1049,17 +1083,34 @@ ApplicationWindow {
}
}

RowLayout {
// At the supported 960x620 viewport two full tables cannot
// occupy 400+ px without clipping the PTP controls.
// Keep both tables stacked, with bounded scrolling only
// when the viewport is shorter than their natural height.
ScrollView {
id: matrixScroll
objectName: "studioMatrixViewport"
Layout.fillWidth: true
Layout.preferredHeight: root.compactLayout ? 214 : 224
Layout.maximumHeight: root.compactLayout ? 214 : 224
spacing: 9
SignalMatrix {
Layout.fillHeight: true
clip: true
contentWidth: availableWidth
contentHeight: matrixStack.implicitHeight
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded

ColumnLayout {
id: matrixStack
width: matrixScroll.availableWidth
spacing: 8
SignalMatrix {
id: voltageMatrix
enabled: !workflowBar.startQueued
objectName: "studioVoltageMatrix"
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 201
Layout.preferredHeight: 201
Layout.maximumHeight: 201
theme: studioTheme
controller: root
device: device
Expand All @@ -1079,6 +1130,9 @@ ApplicationWindow {
objectName: "studioCurrentMatrix"
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 201
Layout.preferredHeight: 201
Layout.maximumHeight: 201
theme: studioTheme
controller: root
device: device
Expand All @@ -1091,8 +1145,8 @@ ApplicationWindow {
symbolText: "I"
unitText: root.signalFrequency === 0 ? "A DC" : "A RMS"
compact: root.compactLayout
}
}

}

StudioTimingTray {
Expand Down
18 changes: 9 additions & 9 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 ? 155 : 182
Layout.alignment: Qt.AlignVCenter
spacing: 8

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

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
5 changes: 3 additions & 2 deletions apps/arstack_studio/qml/RibbonAction.qml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ Button {
: tone === "danger" ? "#ffe3e6"
: tone === "accent" || checked ? "#e1f1ff" : theme.textSoft
contentItem: ColumnLayout {
spacing: 3
anchors.centerIn: parent
spacing: 4
Image {
visible: control.iconSource.toString().length > 0
source: control.hovered && control.enabled ? control.hoverPath : control.iconSource
Expand All @@ -56,7 +57,7 @@ Button {
text: control.text
color: control.textColor
font: control.font
Layout.alignment: Qt.AlignHCenter
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
Expand Down
2 changes: 1 addition & 1 deletion apps/arstack_studio/qml/SignalMatrix.qml
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ Rectangle {
}

Layout.fillWidth: true
Layout.preferredHeight: matrix.compact ? 40 : 42
Layout.preferredHeight: matrix.compact ? 34 : 35
color: signalRow.selected ? "#101c27"
: (magnitudeField.activeFocus || phaseField.activeFocus) ? "#0f1922"
: rowHover.hovered ? "#0d161e" : "transparent"
Expand Down
18 changes: 14 additions & 4 deletions apps/arstack_studio/qml/StudioDarkCheckBox.qml
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,26 @@ CheckBox {
id: control
property var theme
property string uiFont: "Inter"
implicitHeight: 30
implicitHeight: 32
hoverEnabled: true
spacing: 9
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
color: control.checked ? control.theme.accent : control.theme.surface2
border.width: 1
// Hover changes only the border colour, never checkbox size or
// creates a second overlay/nested platform-native indicator.
border.color: control.checked ? control.theme.accent
: control.activeFocus ? control.theme.textSoft : control.theme.line
: (control.hovered || control.activeFocus)
? control.theme.textSoft : control.theme.line
Text {
anchors.centerIn: parent
visible: control.checked
Expand All @@ -30,11 +36,15 @@ CheckBox {
font.weight: Font.Bold
}
}
// Disable native platform hover/ripple background: the custom indicator
// is the ONLY checkbox affordance in hover and non-hover states.
background: Item {}
contentItem: Text {
leftPadding: control.indicator.width + control.spacing
text: control.text
font: control.font
color: control.enabled ? control.theme.textSoft : control.theme.muted
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
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()
// START always transmits a fresh snapshot of the visible 4I+4V
// model. The firmware may have different setpoints after reconnect,
// earlier LIVE SETs, or restoring the operator's local scenario.
const accepted = smartSession.requestStartWithScenario(
controller.startScenarioSnapshot())
if (!accepted) {
controller.pendingApply = true
controller.showMessage("Start rejected. Check device status.", true)
Expand Down
27 changes: 20 additions & 7 deletions apps/arstack_studio/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,17 @@ struct UiBounds {
const QRectF viewport{QPointF{0, 0}, QSizeF{size}};
const auto bar = findUiBounds(window, "studioWorkflowBar");
const auto surface = findUiBounds(window, "studioInjectionSurface");
const auto matrixViewport = findUiBounds(window, "studioMatrixViewport");
const auto voltage = findUiBounds(window, "studioVoltageMatrix");
const auto current = findUiBounds(window, "studioCurrentMatrix");
const auto tray = findUiBounds(window, "studioTimingTray");
const auto quality = findUiBounds(window, "studioQualityRow");
const std::array<std::pair<const char*, UiBounds>, 6> areas{{
// At minimum-height desktop viewports the stacked matrix content scrolls.
// Assert containment of the scroll viewport, not offscreen scrollable
// rows. Offscreen tables remain real, ordered QML items.
const std::array<std::pair<const char*, UiBounds>, 5> areas{{
{"toolbar", bar}, {"injection workspace", surface},
{"voltage matrix", voltage}, {"current matrix", current},
{"matrix scroll viewport", matrixViewport},
{"PTP timing tray", tray}, {"quality editor", quality}
}};
for (const auto& [label, item] : areas) {
Expand All @@ -82,12 +86,21 @@ 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)) {
// Matrices must remain stacked with no overlap in scroll content,
// while PTP/quality remain visible OUTSIDE the scroll area. This
// accepts scrollable rows at 960x620 without tolerating a clipped
// operational PTP switch or status row.
if (!(voltage.visible && current.visible &&
voltage.area.width() >= 25 && current.area.width() >= 25 &&
voltage.area.height() >= 100 && current.area.height() >= 100 &&
voltage.area.bottom() <= current.area.top() + tolerance &&
matrixViewport.area.top() >= surface.area.top() - tolerance &&
tray.area.top() >= matrixViewport.area.bottom() - 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;
<< matrixViewport.area << voltage.area
<< current.area << tray.area << quality.area;
return false;
}
const std::array<const char*, 4> controls{{
Expand Down
Loading
Loading