fix: keep table handles alive across live re-syncs, plus dashboard fixes · Serial-Studio/Serial-Studio@db2debe · GitHub
Skip to content

Commit db2debe

Browse files
alex-spataruclaude
andcommitted
fix: keep table handles alive across live re-syncs, plus dashboard fixes
- Data tables: an identical layout no longer rebuilds the store, and a real rebuild under a live session reloads the frame parsers, so cached handles never go stale and table-driven boards stop going silent after a save - API: io.getLatestFrame reports mirrorAgeMs so scripts can tell GUI lag from bus silence - Checksum API: accept typed arrays in JS (fixes tst_checksum_api) - Annunciator: bell icon and count text share one animated colour - Widget menu: icons tinted with the theme text colour, resolved via the icon registry - Freeze title: new "both" mode so the title bar and face title combine; only "hidden" is exclusive - Tests: tst_data_table_layout, freeze-title integration cases Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 91e8216 commit db2debe

28 files changed

Lines changed: 396 additions & 73 deletions

‎app/qml/MainWindow/Panes/Dashboard/MasterAnnunciator.qml‎

Lines changed: 12 additions & 6 deletions

‎app/qml/MainWindow/Panes/Dashboard/WidgetDelegate.qml‎

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ Widgets.MiniWindow {
6363
readonly property bool mergeTop: (root.mergedEdges & 4) !== 0
6464

6565
//
66-
// Per-widget freeze-title mode ("bar"/"painted"/"hidden"): the project model resolves
67-
// the per-type default; the QML fallback only covers Quick Plot mode (no project)
66+
// Per-widget freeze-title mode ("bar"/"painted"/"both"/"hidden", only "hidden" exclusive):
67+
// the project model resolves the per-type default; the QML fallback only covers Quick Plot
6868
//
6969
property int entityUniqueId: -1
7070
property int entityWidgetType: -1
@@ -74,8 +74,12 @@ Widgets.MiniWindow {
7474
readonly property string effectiveFreezeTitle: freezeTitleMode !== ""
7575
? freezeTitleMode
7676
: (paintsOwnTitle ? "painted" : "bar")
77+
readonly property bool freezeShowsBar: effectiveFreezeTitle === "bar"
78+
|| effectiveFreezeTitle === "both"
79+
readonly property bool freezeShowsFace: effectiveFreezeTitle === "painted"
80+
|| effectiveFreezeTitle === "both"
7781
readonly property bool frozenHeaderVisible: root.frozen
78-
&& root.effectiveFreezeTitle === "bar"
82+
&& root.freezeShowsBar
7983
&& root.title.length > 0
8084
readonly property real frozenHeaderHeight: frozenHeaderVisible
8185
? Math.max(32, frozenHeaderMetrics.height + 14) : 0
@@ -85,6 +89,14 @@ Widgets.MiniWindow {
8589
freezeTitleMode = Cpp_JSON_ProjectModel.freezeTitleMode(entityWidgetType, entityUniqueId)
8690
}
8791

92+
//
93+
// Folds the two independent flags back into the persisted token
94+
//
95+
function setFreezeTitleFlags(bar, face) {
96+
const mode = (bar && face) ? "both" : bar ? "bar" : face ? "painted" : "hidden"
97+
Cpp_JSON_ProjectModel.setFreezeTitleMode(root.entityWidgetType, root.entityUniqueId, mode)
98+
}
99+
88100
onEntityUniqueIdChanged: refreshFreezeTitleMode()
89101

90102
Connections {
@@ -155,7 +167,8 @@ Widgets.MiniWindow {
155167
icon.height: 16
156168
text: qsTr("Rename Widget…")
157169
enabled: root.displayEditable
158-
icon.source: "qrc:/icons/buttons/rename.svg"
170+
icon.color: Cpp_ThemeManager.colors["text"]
171+
icon.source: Cpp_Misc_IconRegistry.icon("editor", "rename", 16)
159172
onTriggered: Cpp_JSON_ProjectModel.promptRenameWidget(root.entityWidgetType,
160173
root.entityUniqueId, root.title)
161174
}
@@ -166,35 +179,33 @@ Widgets.MiniWindow {
166179
icon.width: 16
167180
icon.height: 16
168181
title: qsTr("Freeze Title")
169-
icon.source: "qrc:/icons/buttons/freeze.svg"
182+
icon.color: Cpp_ThemeManager.colors["text"]
183+
icon.source: Cpp_Misc_IconRegistry.icon("commands", "freeze", 16)
170184

171185
MenuItem {
172186
checkable: true
173187
text: qsTr("Title Bar")
188+
checked: root.freezeShowsBar
174189
enabled: root.displayEditable
175-
checked: root.effectiveFreezeTitle === "bar"
176-
onTriggered: Cpp_JSON_ProjectModel.setFreezeTitleMode(root.entityWidgetType,
177-
root.entityUniqueId, "bar")
190+
onTriggered: root.setFreezeTitleFlags(!root.freezeShowsBar, root.freezeShowsFace)
178191
}
179192

180193
MenuItem {
181194
checkable: true
182195
text: qsTr("Painted Title")
183196
visible: root.paintsOwnTitle
184197
enabled: root.displayEditable
198+
checked: root.freezeShowsFace
185199
height: visible ? implicitHeight : 0
186-
checked: root.effectiveFreezeTitle === "painted"
187-
onTriggered: Cpp_JSON_ProjectModel.setFreezeTitleMode(root.entityWidgetType,
188-
root.entityUniqueId, "painted")
200+
onTriggered: root.setFreezeTitleFlags(root.freezeShowsBar, !root.freezeShowsFace)
189201
}
190202

191203
MenuItem {
192204
checkable: true
193205
text: qsTr("Hidden")
194206
enabled: root.displayEditable
195207
checked: root.effectiveFreezeTitle === "hidden"
196-
onTriggered: Cpp_JSON_ProjectModel.setFreezeTitleMode(root.entityWidgetType,
197-
root.entityUniqueId, "hidden")
208+
onTriggered: root.setFreezeTitleFlags(false, false)
198209
}
199210
}
200211

@@ -207,6 +218,7 @@ Widgets.MiniWindow {
207218
height: visible ? implicitHeight : 0
208219
visible: root.extensionConfig.length > 0
209220
onTriggered: root.showExtensionSettings()
221+
icon.color: Cpp_ThemeManager.colors["text"]
210222
icon.source: Cpp_Misc_IconRegistry.icon("commands", "settings", 16)
211223
}
212224

@@ -218,8 +230,9 @@ Widgets.MiniWindow {
218230
icon.width: 16
219231
icon.height: 16
220232
text: qsTr("Open in External Window")
221-
icon.source: "qrc:/icons/buttons/expand.svg"
222233
onTriggered: root.externalWindowClicked()
234+
icon.color: Cpp_ThemeManager.colors["text"]
235+
icon.source: Cpp_Misc_IconRegistry.icon("editor", "expand", 16)
223236
}
224237
}
225238

‎app/qml/ProjectEditor/Views/WorkspaceView.qml‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,15 +357,16 @@ Widgets.Pane {
357357
readonly property bool paintsTitle:
358358
SerialStudioHelpers.dashboardWidgetPaintsTitle(modelData.widgetType)
359359
readonly property var modeKeys:
360-
paintsTitle ? ["bar", "painted", "hidden"] : ["bar", "hidden"]
360+
paintsTitle ? ["bar", "painted", "both", "hidden"] : ["bar", "hidden"]
361361

362362
flat: true
363363
anchors.fill: parent
364364
anchors.margins: 3
365365
font: Cpp_Misc_CommonFonts.uiFont
366366
enabled: modelData.uniqueId >= 0
367367
model: paintsTitle
368-
? [qsTr("Title Bar"), qsTr("Painted Title"), qsTr("Hidden")]
368+
? [qsTr("Title Bar"), qsTr("Painted Title"),
369+
qsTr("Title Bar + Painted"), qsTr("Hidden")]
369370
: [qsTr("Title Bar"), qsTr("Hidden")]
370371
currentIndex: Math.max(0, modeKeys.indexOf(modelData.freezeTitleMode))
371372
onActivated: (index) => {

‎app/qml/Widgets/Dashboard/Bar.qml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@ Item {
4242

4343
//
4444
// Freeze-mode title gate: the delegate arbitrates the per-widget mode; painted titles
45-
// hide while frozen unless the effective mode is "painted" (name shown at most once)
45+
// hide while frozen unless the mode shows the face title ("painted" or "both")
4646
//
4747
readonly property bool titleFrozenOut: windowRoot && windowRoot.frozen === true
48-
&& windowRoot.effectiveFreezeTitle !== "painted"
48+
&& !windowRoot.freezeShowsFace
4949
readonly property string displayTitle: windowRoot && windowRoot.title ? windowRoot.title
5050
: model.title
5151

‎app/qml/Widgets/Dashboard/BarPanel.qml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Item {
4040
// Freeze-mode title gate (same contract as the other painted-title widgets)
4141
//
4242
readonly property bool titleFrozenOut: windowRoot && windowRoot.frozen === true
43-
&& windowRoot.effectiveFreezeTitle !== "painted"
43+
&& !windowRoot.freezeShowsFace
4444
readonly property string displayTitle: windowRoot && windowRoot.title ? windowRoot.title
4545
: ""
4646

‎app/qml/Widgets/Dashboard/Gauge.qml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@ InstrumentBase {
4242

4343
//
4444
// Freeze-mode title gate: the delegate arbitrates the per-widget mode; painted titles
45-
// hide while frozen unless the effective mode is "painted" (name shown at most once)
45+
// hide while frozen unless the mode shows the face title ("painted" or "both")
4646
//
4747
readonly property bool titleFrozenOut: windowRoot && windowRoot.frozen === true
48-
&& windowRoot.effectiveFreezeTitle !== "painted"
48+
&& !windowRoot.freezeShowsFace
4949
readonly property string displayTitle: windowRoot && windowRoot.title ? windowRoot.title
5050
: model.title
5151

‎app/qml/Widgets/Dashboard/Meter.qml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ InstrumentBase {
4040

4141
//
4242
// Freeze-mode title gate: the delegate arbitrates the per-widget mode; painted titles
43-
// hide while frozen unless the effective mode is "painted" (name shown at most once)
43+
// hide while frozen unless the mode shows the face title ("painted" or "both")
4444
//
4545
readonly property bool titleFrozenOut: windowRoot && windowRoot.frozen === true
46-
&& windowRoot.effectiveFreezeTitle !== "painted"
46+
&& !windowRoot.freezeShowsFace
4747
readonly property string displayTitle: windowRoot && windowRoot.title ? windowRoot.title
4848
: model.title
4949

‎app/rcc/ai/docs/control_script_js.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ nothing executes (no `apiCall`, no `tableSet` side effects).
3939
|---|---|
4040
| `apiCall(method, params)` | Call ANY API command by name; marshalled (blocking) to the GUI thread. Returns the command's response object (`{ok, result?, error?}`). |
4141
| `delay(ms)` | Sleep without tripping the watchdog. |
42-
| `newFrame(sourceId?)` | Latest received frame, returned exactly once per arrival; `null` when nothing new. Fields: `sourceId`, `sequence`, `timestampMs` (monotonic clock; never compare with `Date.now()`), `ageMs` (milliseconds since arrival; use this for staleness/watchdogs), `values` (the parser tokens, with `valueCount` and optional `base64`), `text`. |
42+
| `newFrame(sourceId?)` | Latest received frame, returned exactly once per arrival; `null` when nothing new. Fields: `sourceId`, `sequence`, `timestampMs` (monotonic clock; never compare with `Date.now()`), `ageMs` (milliseconds since arrival; use this for staleness/watchdogs), `mirrorAgeMs` (how long ago the host refreshed the copy you read; subtract it from `ageMs` so GUI lag never reads as bus silence), `values` (the parser tokens, with `valueCount` and optional `base64`), `text`. |
4343
| `refreshDashboard()` | Re-runs every dataset transform from the last received values and republishes to the dashboard (no export side effects). Call after `tableSet()` writes so they render while the device is silent. |
4444
| `ensureDashboard(spec)` | Declaratively create missing groups/datasets (matched by title / parser index). Existing items are never modified; memoized, so calling it every `loop()` is free. |
4545
| `dashboardTick()` | Wraps `dashboard.tick`: synthesizes a frame from current dataset values and fans it out to every consumer (dashboard AND CSV/MDF4/Historian/MQTT/API exports), unlike `refreshDashboard()`, which has no export side effects. |
@@ -66,7 +66,8 @@ mirrored as `raw:<alias>` / `final:<alias>`.
6666

6767
Use `ageMs` instead of bookkeeping arrival times with `Date.now()`: it is computed by the
6868
host from the same monotonic clock that stamps frames, needs no per-script state, and is
69-
immune to reconnect races:
69+
immune to reconnect races. Judge silence with `ageMs - mirrorAgeMs`: the copy a script reads
70+
refreshes on the display tick, so a GUI busy with plots adds lag that is not bus silence:
7071

7172
```js
7273
const TIMEOUT_MS = 1000;

‎app/rcc/ai/search_index.json‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎app/rcc/api/api-schema.json‎

Lines changed: 6 additions & 2 deletions

0 commit comments

Comments
 (0)