refactor: relocate live stream controls to ReadoutPanel and update chart axis rendering

This commit is contained in:
jack
2026-07-07 10:38:42 -07:00
parent 799155f249
commit e2d05d2c33
4 changed files with 149 additions and 121 deletions
@@ -75,6 +75,115 @@ ColumnLayout {
}
}
// Live-mode stream stats + stop — moved here from the footer transport bar
// so that bar collapses to zero height in live mode, freeing vertical
// space for the trend chart.
Rectangle {
Layout.fillWidth: true
visible: streamController.mode === "live"
implicitHeight: visible ? liveStatsCard.implicitHeight + 24 : 0
color: Theme.sidePanelBackground
radius: Theme.sidePanelRadius
border.color: Theme.sideBorder
border.width: 1
ColumnLayout {
id: liveStatsCard
anchors {
left: parent.left
right: parent.right
top: parent.top
margins: 12
}
spacing: 8
Label {
text: "LIVE STREAM"
color: Theme.sideMutedText
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontXs
font.letterSpacing: 1.5
font.bold: true
Layout.bottomMargin: 4
}
RowLayout {
Layout.fillWidth: true
Label {
text: "Received"
color: Theme.sideMutedText
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontXs
font.bold: true
}
Item { Layout.fillWidth: true }
Label {
text: streamController.receivedCount
color: Theme.headingColor
font.family: Theme.codeFontFamily
font.pixelSize: Theme.fontMd
font.bold: true
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: Theme.cardBorder
}
RowLayout {
Layout.fillWidth: true
Label {
text: "Errors"
color: Theme.sideMutedText
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontXs
font.bold: true
}
Item { Layout.fillWidth: true }
Label {
text: streamController.errorCount
color: streamController.errorCount > 0 ? Theme.statusWarningColor : Theme.headingColor
font.family: Theme.codeFontFamily
font.pixelSize: Theme.fontMd
font.bold: true
}
}
Button {
id: stopStreamBtn
Layout.fillWidth: true
Layout.topMargin: 4
hoverEnabled: true
text: "STOP"
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontSm
font.weight: Font.Medium
font.letterSpacing: 1.0
background: Rectangle {
radius: 8
color: stopStreamBtn.down ? Theme.transportButtonHover
: (stopStreamBtn.hovered ? Theme.transportButtonBg : "transparent")
border.width: Theme.borderThin
border.color: Theme.sideBorder
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
}
contentItem: Text {
text: stopStreamBtn.text
color: Theme.headingColor
font: stopStreamBtn.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: streamController.stopStream()
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: readoutCard.implicitHeight + 24
@@ -200,16 +200,13 @@ ColumnLayout {
return streamController.mode === "review" && streamController.loadedFile !== "" && modelData.fileName === streamController.loadedFile;
}
// "live_<serial>_<timestamp>.csv" is SessionController's recording
// naming convention (see FileBrowser._refresh_files) — grey these out
// unconditionally rather than only while actively being written, since
// a stale directory listing can't tell "mid-write" from "just finished".
readonly property bool isLiveFile: (modelData.baseName || "").toLowerCase().indexOf("live") >= 0
readonly property bool liveFileBlocked: root.selectedTabIndex === 1 && isLiveFile
visible: matchesFilter
height: matchesFilter ? implicitHeight : 0
enabled: !isLiveFile
opacity: isLiveFile ? 0.45 : 1.0
enabled: !liveFileBlocked
opacity: liveFileBlocked ? 0.45 : 1.0
Behavior on opacity {
NumberAnimation { duration: Theme.durationFast }
}
+6 -111
View File
@@ -5,8 +5,10 @@ import QtQuick.Layouts
import ISC
// ===== Control Bar =====
// Mode-aware footer: cross-fades between Live and Review layouts.
// Both variants render as a single centered pill — no full-width bar.
// Review-mode transport footer (frame counter, play/pause/step, speed).
// Live-mode's Received/Errors/Stop moved to ReadoutPanel's "LIVE STREAM"
// card so this bar can collapse to zero height and free vertical space for
// the trend chart while streaming.
Item {
id: bar
Layout.fillWidth: true
@@ -16,120 +18,13 @@ Item {
}
clip: true
// Hide entirely when in review mode with no file loaded
readonly property bool hasContent: {
if (streamController.mode === "live") return true;
return streamController.loadedFile !== "";
}
readonly property bool isLive: streamController.mode === "live"
// ── LIVE MODE ─────────────────────────────────────────────────
Item {
id: liveContent
anchors.fill: parent
opacity: bar.isLive ? 1.0 : 0.0
visible: opacity > 0
Behavior on opacity {
NumberAnimation { duration: Theme.durationBase; easing.type: Theme.easeStandard }
}
// Single centered pill for Received + Errors + Stop
Rectangle {
anchors.centerIn: parent
implicitWidth: liveRow.implicitWidth + 48
implicitHeight: 68
radius: Theme.radiusMd
color: Theme.sidePanelBackground
border.color: Theme.sideBorder
border.width: 1
Row {
id: liveRow
anchors.centerIn: parent
spacing: 24
// Received counter
Text {
anchors.verticalCenter: parent.verticalCenter
text: "Received: " + streamController.receivedCount
color: Theme.sideMutedText
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontMd
font.weight: Font.Medium
}
// Separator
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 1
height: 36
color: Theme.sideBorder
}
// Errors counter
Text {
anchors.verticalCenter: parent.verticalCenter
text: "Errors: " + streamController.errorCount
color: streamController.errorCount > 0
? Theme.statusWarningColor : Theme.sideMutedText
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontMd
font.weight: Font.Medium
}
// Separator
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 1
height: 36
color: Theme.sideBorder
}
// STOP button
Button {
id: stopBtn
anchors.verticalCenter: parent.verticalCenter
implicitWidth: 112
implicitHeight: 48
hoverEnabled: true
text: "STOP"
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontSm
font.weight: Font.Medium
font.letterSpacing: 1.0
background: Rectangle {
radius: 8
color: stopBtn.pressed ? Theme.transportButtonHover
: Theme.transportButtonBg
border.color: Theme.sideBorder
border.width: 1
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
}
contentItem: Text {
text: parent.text
color: Theme.headingColor
font: parent.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: streamController.stopStream()
}
}
}
}
// Only shown in review mode, and only once a file is loaded
readonly property bool hasContent: streamController.mode !== "live" && streamController.loadedFile !== ""
// ── REVIEW MODE ──────────────────────────────────────────────
Item {
id: reviewContent
anchors.fill: parent
opacity: !bar.isLive ? 1.0 : 0.0
visible: opacity > 0
Behavior on opacity {
NumberAnimation { duration: Theme.durationBase; easing.type: Theme.easeStandard }
}
// Single centered pill for frame counter + transport + speed
Rectangle {
@@ -44,6 +44,11 @@ class TrendChartItem(QQuickPaintedItem):
self._data: list[float] = []
self._padding: int = 8
# Separate from `_padding` (breathing room around the plot area): these
# reserve space for the axis labels themselves, which are wider than
# `_padding` alone — that mismatch is what clipped y-axis text before.
self._margin_left: int = 34
self._margin_bottom: int = 16
self._line_color = QColor("#5B9DF5")
self._grid_color = QColor("#2A3441")
self._text_color = QColor("#CBD5E1")
@@ -153,9 +158,11 @@ class TrendChartItem(QQuickPaintedItem):
if w <= 0 or h <= 0:
return
plot_rect = QRectF(self._padding, self._padding,
max(1, w - 2 * self._padding),
max(1, h - 2 * self._padding))
left = self._padding + self._margin_left
bottom_inset = self._padding + self._margin_bottom
plot_rect = QRectF(left, self._padding,
max(1, w - left - self._padding),
max(1, h - self._padding - bottom_inset))
self._draw_grid(painter, plot_rect)
@@ -220,16 +227,36 @@ class TrendChartItem(QQuickPaintedItem):
font = QFont(painter.font())
font.setPointSizeF(max(7.0, font.pointSizeF() * 0.85))
painter.setFont(font)
rows = 4
label_w = self._margin_left - 4 # 4px gutter before the plot area
for i in range(rows + 1):
t = i / rows
y_val = y_max - t * (y_max - y_min)
y_px = plot_rect.top() + t * plot_rect.height()
label = f"{y_val:.1f}"
painter.drawText(QRectF(0, y_px - 7, self._padding, 14),
painter.drawText(QRectF(0, y_px - 7, label_w, 14),
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter,
label)
# X-axis: sample index. `trendData` carries plain averages with no
# timestamps attached, so index is the only x-coordinate available —
# it reads correctly for review mode (index == frame number) and as a
# relative recency order for the live rolling window. paint() already
# guarantees len(self._data) >= 2 before calling this.
n = len(self._data)
cols = min(4, n - 1)
y_px = plot_rect.bottom() + 4
for i in range(cols + 1):
idx = round((i / cols) * (n - 1))
x_px = self._x_to_px(idx, n, plot_rect)
align = (Qt.AlignmentFlag.AlignLeft if i == 0
else Qt.AlignmentFlag.AlignRight if i == cols
else Qt.AlignmentFlag.AlignHCenter)
painter.drawText(QRectF(x_px - 20, y_px, 40, 14),
align | Qt.AlignmentFlag.AlignTop,
str(idx))
def _draw_line(
self,
painter: QPainter,