Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
59 changes: 43 additions & 16 deletions apps/arstack_studio/qml/Main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -575,8 +575,8 @@ ApplicationWindow {
return ok
}
function startScenarioSnapshot() {
// The table editors debounce keyboard edits for live preview. Flush
// them synchronously before creating the ordered board START payload.
// Explicitly commit an editor still in focus BEFORE snapshotting the
// eight visible setpoints. Never START with a partially typed value.
if (!currentMatrix.flushPendingEdits() || !voltageMatrix.flushPendingEdits()) {
showMessage("Invalid signal input: correct highlighted field before Start.", true)
return null
Expand Down Expand Up @@ -1005,10 +1005,32 @@ ApplicationWindow {
}

ColumnLayout {
id: workbenchColumn
anchors.fill: parent
anchors.margins: 8
spacing: 8

// Passive tap observer: clicking outside the currently focused numeric
// cell (including blank workspace/plots) commits via focus loss.
// Do not intercept ordinary buttons or text selection.
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: function(point) {
const editor = root.activeFocusItem
if (!editor || editor.suffixText === undefined || !editor.activeFocus)
return
const at = editor.mapFromItem(
workbenchColumn, point.position.x, point.position.y)
if (at.x >= 0 && at.x <= editor.width &&
at.y >= 0 && at.y <= editor.height)
return // a click inside the editing cell must never commit
Qt.callLater(function() {
// Window is not a QQuickItem; focus the workspace Item.
if (editor.activeFocus) workbenchColumn.forceActiveFocus()
})
}
}

WorkflowBar {
id: workflowBar
objectName: "studioWorkflowBar"
Expand Down Expand Up @@ -1063,11 +1085,14 @@ ApplicationWindow {

SurfacePanel {
objectName: "studioInjectionSurface"
SplitView.fillWidth: true
// Injection setpoints are a FIXED-WIDTH editing rail.
// Old fillWidth:true made this the horizontal stretch owner
// and starved charts despite its preferredWidth.
SplitView.fillWidth: false
SplitView.fillHeight: true
SplitView.minimumWidth: root.compactLayout ? 500 : 540
SplitView.preferredWidth: root.compactLayout ? 600 : 690
SplitView.maximumWidth: 750
SplitView.minimumWidth: 425
SplitView.preferredWidth: 470
SplitView.maximumWidth: 515
theme: studioTheme

ColumnLayout {
Expand Down Expand Up @@ -1296,9 +1321,10 @@ 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
// The visual-analysis rail owns unused horizontal space.
SplitView.fillWidth: true
SplitView.preferredWidth: 640
SplitView.minimumWidth: 385
SplitView.fillHeight: true
orientation: Qt.Vertical
handle: Rectangle {
Expand All @@ -1317,10 +1343,11 @@ ApplicationWindow {
DockFrame {
visible: root.phasorDockVisible && !root.phasorDetached
SplitView.fillWidth: true
SplitView.minimumHeight: 170
SplitView.preferredHeight: 225
SplitView.maximumHeight: 265
SplitView.fillHeight: false
SplitView.minimumHeight: 190
SplitView.preferredHeight: 300
// Phasor takes leftover vertical space rather than
// being capped at 265 px regardless of window size.
SplitView.fillHeight: true
theme: studioTheme
titleText: "Phasor"
statusText: ""
Expand Down Expand Up @@ -1357,9 +1384,9 @@ ApplicationWindow {
DockFrame {
visible: root.waveformDockVisible && !root.waveformDetached
SplitView.fillWidth: true
SplitView.minimumHeight: 170
SplitView.preferredHeight: 328
SplitView.fillHeight: true
SplitView.minimumHeight: 190
SplitView.preferredHeight: 300
SplitView.fillHeight: false
theme: studioTheme
titleText: "Waveform"
statusText: root.signalFrequency.toFixed(3) + " Hz"
Expand Down
149 changes: 75 additions & 74 deletions apps/arstack_studio/qml/SignalMatrix.qml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Rectangle {
// 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.
// No 90ms live SET while typing: a partial number must never reach
// ESP32-P4 or re-render the phasor before operator confirmation.
function flushPendingEdits() {
for (var i = 0; i < rowRepeater.count; ++i) {
const row = rowRepeater.itemAt(i)
Expand All @@ -47,6 +49,16 @@ Rectangle {
editor.selectAll()
}

// Click on any empty part of this panel explicitly releases a numeric
// editor's focus. TextField.editingFinished then commits exactly once.
// Positioned UNDER actual controls, so buttons, text selection and menus
// retain their regular mouse behavior.
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
onClicked: matrix.forceActiveFocus()
}

ColumnLayout {
anchors.fill: parent
spacing: 0
Expand Down Expand Up @@ -130,27 +142,52 @@ 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
property bool magnitudeDirty: false
property bool phaseDirty: false

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
function commitMagnitude() {
if (!magnitudeDirty) return true
const value = matrix.controller.parseOperatorNumber(magnitudeField.text)
magnitudeDirty = false
if (!matrix.controller.validMagnitude(matrix.groupIndex, value)) {
magnitudeField.invalidInput = false
magnitudeField.text = mag.toFixed(3)
matrix.controller.showMessage(
sid + ": invalid magnitude. Restored " + mag.toFixed(3) +
". Use dot or comma as decimal separator.", 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
// Normalize 0,5 and 0.5 to the SAME displayed 0.500.
magnitudeField.invalidInput = false
magnitudeField.text = value.toFixed(3)
if (Math.abs(value - mag) < 0.000000001) 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.invalidInput = false
phaseField.text = angle.toFixed(2)
matrix.controller.showMessage(
sid + ": invalid angle. Restored " + angle.toFixed(2) +
"°. Use dot or comma as decimal separator.", true)
return false
}
return true
phaseField.invalidInput = false
phaseField.text = value.toFixed(2)
if (Math.abs(value - angle) < 0.000000001) return true
return matrix.controller.editSignal(
matrix.groupIndex, rowIndex, "phase", value)
}

function flushPendingEdits() {
// Also cover START while a field still owns focus.
if (!commitMagnitude()) return false
return commitPhase()
}

// Align with the field edge, not the left side of the table.
Expand Down Expand Up @@ -237,18 +274,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)
}
// Numeric drafts are held in the editor until confirmation.
// No background timer sends incomplete values to the board.

onMagChanged: if (!magnitudeField.activeFocus) magnitudeField.text = mag.toFixed(3)
onAngleChanged: if (!phaseField.activeFocus) phaseField.text = angle.toFixed(2)
Expand Down Expand Up @@ -306,31 +333,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
invalidInput = !matrix.controller.validMagnitude(
matrix.groupIndex, matrix.controller.parseOperatorNumber(text))
}
onEditingFinished: signalRow.commitMagnitude()
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Menu ||
(event.key === Qt.Key_F10 && (event.modifiers & Qt.ShiftModifier))) {
Expand All @@ -340,7 +352,9 @@ 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)
else selectAll()
event.accepted = true
}
}
Expand Down Expand Up @@ -369,31 +383,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
invalidInput = !matrix.controller.validPhase(
matrix.controller.parseOperatorNumber(text))
}
onEditingFinished: signalRow.commitPhase()
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Menu ||
(event.key === Qt.Key_F10 && (event.modifiers & Qt.ShiftModifier))) {
Expand All @@ -403,7 +402,9 @@ 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)
else selectAll()
event.accepted = true
}
}
Expand Down
Loading
Loading