style: increase ReadoutPanel font sizes and add CSV test fixture for data logging
This commit is contained in:
+221
-64
@@ -22,11 +22,17 @@ Item {
|
||||
property string compareFileB: ""
|
||||
property real warpingDistance: -1
|
||||
property int frameOffset: 0
|
||||
property real frameOffsetSeconds: 0
|
||||
property real maxSensorDeviation: -1
|
||||
property bool comparing: false
|
||||
property var trendDataA: []
|
||||
property var trendDataB: []
|
||||
property var timeDataA: []
|
||||
property var timeDataB: []
|
||||
property int frameCountA: 0
|
||||
property int frameCountB: 0
|
||||
property string errorMessage: ""
|
||||
property string mismatchMessage: ""
|
||||
|
||||
// Wafer overlap properties
|
||||
property var compareSensorLayout: []
|
||||
@@ -45,15 +51,28 @@ Item {
|
||||
return path ? path.split("/").pop() : ""
|
||||
}
|
||||
|
||||
function scrubTimeLabel(idx) {
|
||||
var t = idx < root.timeDataA.length ? root.timeDataA[idx]
|
||||
: (idx < root.timeDataB.length ? root.timeDataB[idx] : null)
|
||||
return t !== null ? " (" + t.toFixed(1) + "s)" : ""
|
||||
}
|
||||
|
||||
function clearResults() {
|
||||
root.warpingDistance = -1
|
||||
root.frameOffset = 0
|
||||
root.frameOffsetSeconds = 0
|
||||
root.maxSensorDeviation = -1
|
||||
root.trendDataA = []
|
||||
root.trendDataB = []
|
||||
root.timeDataA = []
|
||||
root.timeDataB = []
|
||||
root.frameCountA = 0
|
||||
root.frameCountB = 0
|
||||
root.compareSensorLayout = []
|
||||
root.compareSensorDiff = []
|
||||
root.errorMessage = ""
|
||||
root.mismatchMessage = ""
|
||||
mismatchHideTimer.stop()
|
||||
}
|
||||
|
||||
function resetComparison() {
|
||||
@@ -66,6 +85,12 @@ Item {
|
||||
|
||||
Component.onCompleted: file_browser.refreshFiles()
|
||||
|
||||
Timer {
|
||||
id: mismatchHideTimer
|
||||
interval: 6000
|
||||
onTriggered: root.mismatchMessage = ""
|
||||
}
|
||||
|
||||
// Clicking a file in the left rail's browser populates the armed run slot
|
||||
Connections {
|
||||
target: streamController
|
||||
@@ -85,15 +110,31 @@ Item {
|
||||
if (result && result.success) {
|
||||
root.warpingDistance = result.distance
|
||||
root.frameOffset = result.frame_offset !== undefined ? result.frame_offset : 0
|
||||
root.frameOffsetSeconds = result.frame_offset_seconds !== undefined ? result.frame_offset_seconds : 0
|
||||
root.maxSensorDeviation = result.max_sensor_deviation !== undefined ? result.max_sensor_deviation : -1
|
||||
root.trendDataA = result.series_a || []
|
||||
root.trendDataB = result.series_b || []
|
||||
root.timeDataA = result.time_a || []
|
||||
root.timeDataB = result.time_b || []
|
||||
root.frameCountA = result.frame_count_a || 0
|
||||
root.frameCountB = result.frame_count_b || 0
|
||||
root.compareSensorLayout = result.sensor_layout || []
|
||||
root.compareSensorDiff = result.sensor_diff || []
|
||||
root.compareWaferShape = result.wafer_shape || "round"
|
||||
root.compareWaferSize = result.wafer_size || 300.0
|
||||
root.errorMessage = ""
|
||||
scrubber.value = Math.floor(root.seriesLen / 2)
|
||||
if (root.frameCountA !== root.frameCountB) {
|
||||
var timeA = root.timeDataA.length > 0 ? root.timeDataA[root.timeDataA.length - 1] : 0
|
||||
var timeB = root.timeDataB.length > 0 ? root.timeDataB[root.timeDataB.length - 1] : 0
|
||||
root.mismatchMessage = "Run A: " + root.frameCountA + " frames (" + timeA.toFixed(1) + "s) · "
|
||||
+ "Run B: " + root.frameCountB + " frames (" + timeB.toFixed(1) + "s). "
|
||||
+ "Deviation beyond the shorter run shows as \"none\"."
|
||||
mismatchHideTimer.restart()
|
||||
} else {
|
||||
root.mismatchMessage = ""
|
||||
mismatchHideTimer.stop()
|
||||
}
|
||||
} else {
|
||||
root.clearResults()
|
||||
root.errorMessage = (result && result.error) ? result.error : "Comparison failed"
|
||||
@@ -103,17 +144,73 @@ Item {
|
||||
|
||||
// ── Reusable pieces ────────────────────────────────────────────
|
||||
component SectionTitle: Label {
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.family: Theme.uiFontFamily
|
||||
font.pixelSize: Theme.fontSm
|
||||
font.letterSpacing: 1.5
|
||||
font.bold: true
|
||||
font.letterSpacing: 1.2
|
||||
color: Theme.sideMutedText
|
||||
}
|
||||
|
||||
component PanelBox: Rectangle {
|
||||
color: Theme.cardBackground
|
||||
border.color: Theme.cardBorder
|
||||
color: Theme.sidePanelBackground
|
||||
border.color: Theme.sideBorder
|
||||
border.width: 1
|
||||
radius: Theme.radiusMd
|
||||
radius: Theme.sidePanelRadius
|
||||
}
|
||||
|
||||
// Checkbox styled to match MapTab's right-rail ReadoutPanel (square accent
|
||||
// indicator + check icon) instead of the platform-default CheckBox look.
|
||||
component PanelCheckBox: CheckBox {
|
||||
id: toggle
|
||||
indicator: Rectangle {
|
||||
implicitWidth: 18
|
||||
implicitHeight: 18
|
||||
x: toggle.leftPadding
|
||||
y: parent.height / 2 - height / 2
|
||||
radius: Theme.radiusXs
|
||||
color: toggle.checked ? Theme.primaryAccent : "transparent"
|
||||
border.width: Theme.borderThin
|
||||
border.color: toggle.checked ? Theme.primaryAccent : Theme.fieldBorder
|
||||
|
||||
Behavior on color { ColorAnimation { duration: Theme.durationFast } }
|
||||
Behavior on border.color { ColorAnimation { duration: Theme.durationFast } }
|
||||
|
||||
IconImage {
|
||||
anchors.centerIn: parent
|
||||
source: "icons/check.svg"
|
||||
width: 12; height: 12
|
||||
sourceSize.width: 12
|
||||
sourceSize.height: 12
|
||||
color: Theme.panelBackground
|
||||
opacity: toggle.checked ? 1.0 : 0.0
|
||||
Behavior on opacity { NumberAnimation { duration: Theme.durationFast } }
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: toggle.text
|
||||
color: Theme.headingColor
|
||||
font.family: Theme.uiFontFamily
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: toggle.indicator.width + toggle.spacing
|
||||
font.pixelSize: toggle.font.pixelSize || Theme.fontXs
|
||||
}
|
||||
}
|
||||
|
||||
// Slider styled to match MapTab's right-rail heatmap slider (circular
|
||||
// accent handle that grows slightly on press).
|
||||
component PanelSlider: Slider {
|
||||
id: panelSlider
|
||||
handle: Rectangle {
|
||||
x: panelSlider.leftPadding + panelSlider.visualPosition * (panelSlider.availableWidth - width)
|
||||
y: panelSlider.topPadding + panelSlider.availableHeight / 2 - height / 2
|
||||
implicitWidth: 18
|
||||
implicitHeight: 18
|
||||
radius: 9
|
||||
color: Theme.primaryAccent
|
||||
scale: panelSlider.pressed ? 1.15 : 1.0
|
||||
Behavior on scale { NumberAnimation { duration: Theme.durationFast; easing.type: Theme.easeStandard } }
|
||||
}
|
||||
}
|
||||
|
||||
// Mini stat box under the alignment scrubber (mockup's subtle-box trio)
|
||||
@@ -170,7 +267,7 @@ Item {
|
||||
spacing: 2
|
||||
Label {
|
||||
text: readoutCard.label
|
||||
font.pixelSize: 9
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.bold: true
|
||||
font.letterSpacing: 0.2
|
||||
color: Theme.sideMutedText
|
||||
@@ -225,7 +322,7 @@ Item {
|
||||
spacing: 2
|
||||
Label {
|
||||
text: slotBox.title
|
||||
font.pixelSize: 9
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.bold: true
|
||||
font.letterSpacing: 1.0
|
||||
color: root.activeBox === slotBox.boxIndex ? slotBox.accent : Theme.sideMutedText
|
||||
@@ -234,7 +331,7 @@ Item {
|
||||
text: slotBox.filePath !== ""
|
||||
? root.shortName(slotBox.filePath)
|
||||
: (root.activeBox === slotBox.boxIndex ? "<- Select file from left panel..." : "Click to select")
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.pixelSize: Theme.fontSm
|
||||
color: slotBox.filePath !== "" ? Theme.headingColor : Theme.bodyColor
|
||||
opacity: slotBox.filePath !== "" ? 1.0 : 0.6
|
||||
elide: Text.ElideMiddle
|
||||
@@ -371,7 +468,7 @@ Item {
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
SectionTitle {
|
||||
text: "ALIGNED TEMP CURVE OVERLAY (DTW)"
|
||||
text: "CURVE OVERLAY"
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Row {
|
||||
@@ -406,8 +503,8 @@ Item {
|
||||
|
||||
readonly property real padLeft: 50
|
||||
readonly property real padRight: 16
|
||||
readonly property real padTop: 16
|
||||
readonly property real padBottom: 28
|
||||
readonly property real padTop: 24
|
||||
readonly property real padBottom: 36
|
||||
|
||||
function drawGrid(ctx, minV, maxV, dataLen) {
|
||||
var plotW = width - padLeft - padRight
|
||||
@@ -417,7 +514,7 @@ Item {
|
||||
ctx.strokeStyle = Theme.chartGridLine
|
||||
ctx.lineWidth = 1
|
||||
ctx.fillStyle = Theme.chartAxisText
|
||||
ctx.font = "10px sans-serif"
|
||||
ctx.font = "10px " + Theme.uiFontFamily
|
||||
ctx.textAlign = "right"
|
||||
|
||||
var ySteps = 5
|
||||
@@ -440,21 +537,27 @@ Item {
|
||||
var xFrac = xi / xSteps
|
||||
var xPx = padLeft + plotW * xFrac
|
||||
var xIdx = Math.round(xFrac * (dataLen - 1))
|
||||
ctx.fillText(xIdx.toString(), xPx, height - 6)
|
||||
ctx.fillText(xIdx.toString(), xPx, height - 20)
|
||||
}
|
||||
|
||||
ctx.save()
|
||||
ctx.translate(12, padTop + plotH / 2)
|
||||
ctx.translate(14, padTop + plotH / 2)
|
||||
ctx.rotate(-Math.PI / 2)
|
||||
ctx.textAlign = "center"
|
||||
ctx.fillText("°C", 0, 0)
|
||||
ctx.font = "12px " + Theme.uiFontFamily
|
||||
ctx.fillText("Celcius (°C)", 0, 0)
|
||||
ctx.restore()
|
||||
|
||||
ctx.textAlign = "center"
|
||||
ctx.fillText("Frame", padLeft + plotW / 2, height - 2)
|
||||
ctx.font = "12px " + Theme.uiFontFamily
|
||||
ctx.fillText("Frame", padLeft + plotW / 2, height - 4)
|
||||
}
|
||||
|
||||
function drawSeries(ctx, series, minV, range, color) {
|
||||
function drawSeries(ctx, series, minV, range, color, dataLen) {
|
||||
// x is mapped against the shared frame domain (dataLen), not the
|
||||
// series' own length — otherwise a shorter run's line stretches to
|
||||
// fill the whole plot instead of stopping where its data ends.
|
||||
if (series.length < 2) return
|
||||
var plotW = width - padLeft - padRight
|
||||
var plotH = height - padTop - padBottom
|
||||
|
||||
@@ -462,7 +565,7 @@ Item {
|
||||
ctx.lineWidth = 2
|
||||
ctx.beginPath()
|
||||
for (var i = 0; i < series.length; i++) {
|
||||
var x = padLeft + (i / (series.length - 1)) * plotW
|
||||
var x = padLeft + (i / (dataLen - 1)) * plotW
|
||||
var y = padTop + plotH - ((series[i] - minV) / range) * plotH
|
||||
if (i === 0) ctx.moveTo(x, y)
|
||||
else ctx.lineTo(x, y)
|
||||
@@ -497,8 +600,8 @@ Item {
|
||||
var range = (maxV - minV) || 1
|
||||
|
||||
drawGrid(ctx, minV, maxV, root.seriesLen)
|
||||
drawSeries(ctx, root.trendDataA, minV, range, Theme.primaryAccent)
|
||||
drawSeries(ctx, root.trendDataB, minV, range, Theme.themeSkill)
|
||||
drawSeries(ctx, root.trendDataA, minV, range, Theme.primaryAccent, root.seriesLen)
|
||||
drawSeries(ctx, root.trendDataB, minV, range, Theme.themeSkill, root.seriesLen)
|
||||
drawScrubMarker(ctx)
|
||||
}
|
||||
|
||||
@@ -556,19 +659,10 @@ Item {
|
||||
color: Theme.cardBorder
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
SectionTitle {
|
||||
text: "TIMELINE SCRUBBER"
|
||||
Layout.fillWidth: true
|
||||
SectionTitle {
|
||||
text: "ALIGNMENT TIMELINE SCRUBBER"
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Label {
|
||||
text: "Frame " + Math.round(scrubber.value) + " / " + Math.max(0, root.seriesLen - 1)
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.bold: true
|
||||
font.family: Theme.codeFontFamily
|
||||
color: Theme.headingColor
|
||||
}
|
||||
Layout.topMargin: 8
|
||||
}
|
||||
|
||||
Slider {
|
||||
@@ -586,24 +680,30 @@ Item {
|
||||
spacing: 8
|
||||
|
||||
readonly property int scrubIdx: Math.round(scrubber.value)
|
||||
readonly property real tempA: root.trendDataA.length > 0
|
||||
? root.trendDataA[Math.min(scrubIdx, root.trendDataA.length - 1)] : 0
|
||||
readonly property real tempB: root.trendDataB.length > 0
|
||||
? root.trendDataB[Math.min(scrubIdx, root.trendDataB.length - 1)] : 0
|
||||
readonly property bool hasA: scrubIdx < root.trendDataA.length
|
||||
readonly property bool hasB: scrubIdx < root.trendDataB.length
|
||||
readonly property real tempA: hasA ? root.trendDataA[scrubIdx] : 0
|
||||
readonly property real tempB: hasB ? root.trendDataB[scrubIdx] : 0
|
||||
|
||||
ScrubStat {
|
||||
label: "Position"
|
||||
value: scrubReadout.scrubIdx + " / " + Math.max(0, root.seriesLen - 1)
|
||||
+ root.scrubTimeLabel(scrubReadout.scrubIdx)
|
||||
}
|
||||
ScrubStat {
|
||||
label: "Temp (Run A)"
|
||||
value: scrubReadout.tempA.toFixed(1) + "°C"
|
||||
value: scrubReadout.hasA ? scrubReadout.tempA.toFixed(1) + "°C" : "none"
|
||||
valueColor: Theme.primaryAccent
|
||||
}
|
||||
ScrubStat {
|
||||
label: "Temp (Run B)"
|
||||
value: scrubReadout.tempB.toFixed(1) + "°C"
|
||||
value: scrubReadout.hasB ? scrubReadout.tempB.toFixed(1) + "°C" : "none"
|
||||
valueColor: Theme.themeSkill
|
||||
}
|
||||
ScrubStat {
|
||||
label: "Difference"
|
||||
value: Math.abs(scrubReadout.tempA - scrubReadout.tempB).toFixed(1) + "°C"
|
||||
value: (scrubReadout.hasA && scrubReadout.hasB)
|
||||
? Math.abs(scrubReadout.tempA - scrubReadout.tempB).toFixed(1) + "°C" : "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -624,7 +724,7 @@ Item {
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
SectionTitle {
|
||||
text: "WAFER MAP OVERLAP VIEW"
|
||||
text: "WAFER OVERLAP"
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Label {
|
||||
@@ -742,7 +842,10 @@ Item {
|
||||
id: compareBtn
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 32
|
||||
enabled: root.compareFileA !== "" && root.compareFileB !== "" && !root.comparing
|
||||
readonly property bool sameFile: root.compareFileA !== "" && root.compareFileA === root.compareFileB
|
||||
enabled: root.compareFileA !== "" && root.compareFileB !== "" && !sameFile && !root.comparing
|
||||
ToolTip.visible: sameFile && hovered
|
||||
ToolTip.text: "Choose two different runs to compare"
|
||||
onClicked: {
|
||||
root.comparing = true
|
||||
root.clearResults()
|
||||
@@ -761,11 +864,13 @@ Item {
|
||||
anchors.centerIn: parent
|
||||
Label {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: compareBtn.enabled
|
||||
? (root.comparing ? "Comparing…" : "Run DTW Comparison")
|
||||
: (root.comparing ? "Comparing…" : "Select both runs")
|
||||
text: root.comparing
|
||||
? "Comparing…"
|
||||
: compareBtn.enabled
|
||||
? "Run DTW Comparison"
|
||||
: (compareBtn.sameFile ? "Choose different runs" : "Select both runs")
|
||||
color: compareBtn.enabled ? Theme.tone100 : Theme.sideMutedText
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.pixelSize: Theme.fontSm
|
||||
font.bold: true
|
||||
}
|
||||
BusyIndicator {
|
||||
@@ -790,22 +895,14 @@ Item {
|
||||
anchors.margins: 12
|
||||
spacing: 8
|
||||
|
||||
SectionTitle { text: "OVERLAP MAP SETTINGS" }
|
||||
SectionTitle { text: "OVERLAP SETTINGS" }
|
||||
|
||||
CheckBox {
|
||||
PanelCheckBox {
|
||||
id: enableOverlapCheck
|
||||
Layout.fillWidth: true
|
||||
checked: true
|
||||
text: "Enable Overlap Map"
|
||||
font.pixelSize: Theme.fontXs
|
||||
|
||||
contentItem: Label {
|
||||
leftPadding: enableOverlapCheck.indicator.width + 6
|
||||
text: enableOverlapCheck.text
|
||||
font.pixelSize: Theme.fontXs
|
||||
color: Theme.headingColor
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
font.pixelSize: Theme.fontSm
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
@@ -824,11 +921,11 @@ Item {
|
||||
spacing: 6
|
||||
Label {
|
||||
text: "Blend"
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.pixelSize: Theme.fontSm
|
||||
color: Theme.bodyColor
|
||||
Layout.preferredWidth: 40
|
||||
}
|
||||
Slider {
|
||||
PanelSlider {
|
||||
id: blendSlider
|
||||
Layout.fillWidth: true
|
||||
from: 0; to: 100; value: 50
|
||||
@@ -836,7 +933,7 @@ Item {
|
||||
}
|
||||
Label {
|
||||
text: Math.round(blendSlider.value) + "%"
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.pixelSize: Theme.fontSm
|
||||
color: Theme.sideMutedText
|
||||
Layout.preferredWidth: 30
|
||||
}
|
||||
@@ -849,17 +946,17 @@ Item {
|
||||
Row {
|
||||
spacing: 3
|
||||
Rectangle { width: 8; height: 8; radius: 4; color: Theme.sensorLow; anchors.verticalCenter: parent.verticalCenter }
|
||||
Label { text: "B cooler"; font.pixelSize: 9; color: Theme.sideMutedText }
|
||||
Label { text: "B cooler"; font.pixelSize: Theme.fontXs; color: Theme.sideMutedText }
|
||||
}
|
||||
Row {
|
||||
spacing: 3
|
||||
Rectangle { width: 8; height: 8; radius: 4; color: Theme.sensorInRange; anchors.verticalCenter: parent.verticalCenter }
|
||||
Label { text: "Match"; font.pixelSize: 9; color: Theme.sideMutedText }
|
||||
Label { text: "Match"; font.pixelSize: Theme.fontXs; color: Theme.sideMutedText }
|
||||
}
|
||||
Row {
|
||||
spacing: 3
|
||||
Rectangle { width: 8; height: 8; radius: 4; color: Theme.sensorHigh; anchors.verticalCenter: parent.verticalCenter }
|
||||
Label { text: "B hotter"; font.pixelSize: 9; color: Theme.sideMutedText }
|
||||
Label { text: "B hotter"; font.pixelSize: Theme.fontXs; color: Theme.sideMutedText }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -877,7 +974,7 @@ Item {
|
||||
anchors.margins: 12
|
||||
spacing: 8
|
||||
|
||||
SectionTitle { text: "DTW COMPARISON READOUT" }
|
||||
SectionTitle { text: "DTW READOUT" }
|
||||
|
||||
ReadoutCard {
|
||||
label: "DTW ALIGNMENT DISTANCE"
|
||||
@@ -889,7 +986,8 @@ Item {
|
||||
ReadoutCard {
|
||||
label: "TEMPORAL FRAME OFFSET"
|
||||
value: root.warpingDistance >= 0
|
||||
? ((root.frameOffset >= 0 ? "+" : "") + root.frameOffset + " frames")
|
||||
? ((root.frameOffset >= 0 ? "+" : "") + root.frameOffset + " frames / "
|
||||
+ (root.frameOffsetSeconds >= 0 ? "+" : "") + root.frameOffsetSeconds.toFixed(1) + "s")
|
||||
: "--"
|
||||
valueColor: root.warpingDistance >= 0 ? Theme.headingColor : Theme.sideMutedText
|
||||
}
|
||||
@@ -907,4 +1005,63 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Frame Mismatch Banner (amber, floats over content, auto-hides) ──
|
||||
Rectangle {
|
||||
id: mismatchBanner
|
||||
visible: root.mismatchMessage !== ""
|
||||
z: 100
|
||||
anchors.top: parent.top
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.topMargin: 12
|
||||
width: Math.min(parent.width - 48, 640)
|
||||
height: mismatchRow.implicitHeight + 16
|
||||
color: Theme.cardBackground
|
||||
border.color: Theme.statusWarningColor
|
||||
border.width: 1
|
||||
radius: Theme.radiusSm
|
||||
|
||||
RowLayout {
|
||||
id: mismatchRow
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
spacing: 8
|
||||
|
||||
IconImage {
|
||||
source: "icons/triangle-alert.svg"
|
||||
width: 16; height: 16
|
||||
sourceSize.width: 16
|
||||
sourceSize.height: 16
|
||||
color: Theme.statusWarningColor
|
||||
Layout.alignment: Qt.AlignTop
|
||||
}
|
||||
|
||||
Label {
|
||||
text: root.mismatchMessage
|
||||
color: Theme.statusWarningColor
|
||||
font.pixelSize: Theme.fontSm
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Button {
|
||||
flat: true
|
||||
implicitWidth: 24
|
||||
implicitHeight: 24
|
||||
Layout.alignment: Qt.AlignTop
|
||||
onClicked: {
|
||||
root.mismatchMessage = ""
|
||||
mismatchHideTimer.stop()
|
||||
}
|
||||
background: Rectangle { color: "transparent" }
|
||||
contentItem: Label {
|
||||
text: "✕"
|
||||
color: Theme.statusWarningColor
|
||||
font.pixelSize: Theme.fontXs
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,9 +149,9 @@ ColumnLayout {
|
||||
Text {
|
||||
text: "CONNECTION STATUS"
|
||||
color: Theme.sideMutedText
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.weight: Font.Medium
|
||||
font.letterSpacing: 1.2
|
||||
font.pixelSize: Theme.fontSm
|
||||
font.bold: true
|
||||
font.letterSpacing: 1.5
|
||||
font.family: Theme.uiFontFamily
|
||||
}
|
||||
|
||||
@@ -297,9 +297,9 @@ ColumnLayout {
|
||||
Text {
|
||||
text: "DIRECTORY"
|
||||
color: Theme.sideMutedText
|
||||
font.pixelSize: Theme.fontMd
|
||||
font.weight: Font.Medium
|
||||
font.letterSpacing: 1.2
|
||||
font.pixelSize: Theme.fontSm
|
||||
font.bold: true
|
||||
font.letterSpacing: 1.5
|
||||
font.family: Theme.uiFontFamily
|
||||
}
|
||||
Text {
|
||||
|
||||
@@ -101,7 +101,7 @@ ColumnLayout {
|
||||
text: "LIVE STREAM"
|
||||
color: Theme.sideMutedText
|
||||
font.family: Theme.uiFontFamily
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.pixelSize: Theme.fontSm
|
||||
font.letterSpacing: 1.5
|
||||
font.bold: true
|
||||
Layout.bottomMargin: 4
|
||||
@@ -113,7 +113,7 @@ ColumnLayout {
|
||||
text: "Received"
|
||||
color: Theme.sideMutedText
|
||||
font.family: Theme.uiFontFamily
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.pixelSize: Theme.fontSm
|
||||
font.bold: true
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
@@ -121,7 +121,7 @@ ColumnLayout {
|
||||
text: streamController.receivedCount
|
||||
color: Theme.headingColor
|
||||
font.family: Theme.codeFontFamily
|
||||
font.pixelSize: Theme.fontMd
|
||||
font.pixelSize: Theme.fontLg
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
@@ -138,7 +138,7 @@ ColumnLayout {
|
||||
text: "Errors"
|
||||
color: Theme.sideMutedText
|
||||
font.family: Theme.uiFontFamily
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.pixelSize: Theme.fontSm
|
||||
font.bold: true
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
@@ -146,7 +146,7 @@ ColumnLayout {
|
||||
text: streamController.errorCount
|
||||
color: streamController.errorCount > 0 ? Theme.statusWarningColor : Theme.headingColor
|
||||
font.family: Theme.codeFontFamily
|
||||
font.pixelSize: Theme.fontMd
|
||||
font.pixelSize: Theme.fontLg
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
@@ -207,7 +207,7 @@ ColumnLayout {
|
||||
text: "READOUT"
|
||||
color: Theme.sideMutedText
|
||||
font.family: Theme.uiFontFamily
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.pixelSize: Theme.fontSm
|
||||
font.letterSpacing: 1.5
|
||||
font.bold: true
|
||||
Layout.leftMargin: 14
|
||||
@@ -226,7 +226,7 @@ ColumnLayout {
|
||||
text: "MIN TEMP"
|
||||
color: Theme.sideMutedText
|
||||
font.family: Theme.uiFontFamily
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.pixelSize: Theme.fontSm
|
||||
font.bold: true
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
@@ -236,7 +236,7 @@ ColumnLayout {
|
||||
text: s.min !== undefined ? s.min : "—"
|
||||
color: Theme.sensorLow
|
||||
font.family: Theme.codeFontFamily
|
||||
font.pixelSize: Theme.fontMd
|
||||
font.pixelSize: Theme.fontLg
|
||||
font.bold: true
|
||||
}
|
||||
Label {
|
||||
@@ -267,7 +267,7 @@ ColumnLayout {
|
||||
text: "MAX TEMP"
|
||||
color: Theme.sideMutedText
|
||||
font.family: Theme.uiFontFamily
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.pixelSize: Theme.fontSm
|
||||
font.bold: true
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
@@ -277,7 +277,7 @@ ColumnLayout {
|
||||
text: s.max !== undefined ? s.max : "—"
|
||||
color: Theme.sensorHigh
|
||||
font.family: Theme.codeFontFamily
|
||||
font.pixelSize: Theme.fontMd
|
||||
font.pixelSize: Theme.fontLg
|
||||
font.bold: true
|
||||
}
|
||||
Label {
|
||||
@@ -308,7 +308,7 @@ ColumnLayout {
|
||||
text: "DIFF"
|
||||
color: Theme.sideMutedText
|
||||
font.family: Theme.uiFontFamily
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.pixelSize: Theme.fontSm
|
||||
font.bold: true
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
@@ -316,7 +316,7 @@ ColumnLayout {
|
||||
text: s.diff !== undefined ? s.diff : "—"
|
||||
color: Theme.diffAccent
|
||||
font.family: Theme.codeFontFamily
|
||||
font.pixelSize: Theme.fontMd
|
||||
font.pixelSize: Theme.fontLg
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
@@ -337,7 +337,7 @@ ColumnLayout {
|
||||
text: "AVERAGE"
|
||||
color: Theme.sideMutedText
|
||||
font.family: Theme.uiFontFamily
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.pixelSize: Theme.fontSm
|
||||
font.bold: true
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
@@ -345,7 +345,7 @@ ColumnLayout {
|
||||
text: s.avg !== undefined ? s.avg : "—"
|
||||
color: Theme.headingColor
|
||||
font.family: Theme.codeFontFamily
|
||||
font.pixelSize: Theme.fontMd
|
||||
font.pixelSize: Theme.fontLg
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
@@ -366,7 +366,7 @@ ColumnLayout {
|
||||
text: "SIGMA (Σ)"
|
||||
color: Theme.sideMutedText
|
||||
font.family: Theme.uiFontFamily
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.pixelSize: Theme.fontSm
|
||||
font.bold: true
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
@@ -374,7 +374,7 @@ ColumnLayout {
|
||||
text: s.sigma !== undefined ? s.sigma : "—"
|
||||
color: Theme.headingColor
|
||||
font.family: Theme.codeFontFamily
|
||||
font.pixelSize: Theme.fontMd
|
||||
font.pixelSize: Theme.fontLg
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
@@ -395,7 +395,7 @@ ColumnLayout {
|
||||
text: "3Σ VALUE"
|
||||
color: Theme.sideMutedText
|
||||
font.family: Theme.uiFontFamily
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.pixelSize: Theme.fontSm
|
||||
font.bold: true
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
@@ -403,7 +403,7 @@ ColumnLayout {
|
||||
text: s.threeSigma !== undefined ? s.threeSigma : "—"
|
||||
color: Theme.headingColor
|
||||
font.family: Theme.codeFontFamily
|
||||
font.pixelSize: Theme.fontMd
|
||||
font.pixelSize: Theme.fontLg
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
@@ -432,7 +432,7 @@ ColumnLayout {
|
||||
text: "DISPLAY"
|
||||
color: Theme.sideMutedText
|
||||
font.family: Theme.uiFontFamily
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.pixelSize: Theme.fontSm
|
||||
font.letterSpacing: 1.5
|
||||
font.bold: true
|
||||
Layout.bottomMargin: 4
|
||||
@@ -442,21 +442,21 @@ ColumnLayout {
|
||||
id: thicknessToggle
|
||||
text: "Show Thickness"
|
||||
checked: false
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.pixelSize: Theme.fontSm
|
||||
}
|
||||
|
||||
PanelCheckBox {
|
||||
id: labelsToggle
|
||||
text: "Labels"
|
||||
checked: true
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.pixelSize: Theme.fontSm
|
||||
}
|
||||
|
||||
PanelCheckBox {
|
||||
id: clusterAverageToggle
|
||||
text: "Average Clusters"
|
||||
checked: streamController.clusterAveragingEnabled
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.pixelSize: Theme.fontSm
|
||||
onCheckedChanged: streamController.clusterAveragingEnabled = checked
|
||||
}
|
||||
|
||||
@@ -472,7 +472,7 @@ ColumnLayout {
|
||||
Label {
|
||||
text: "Heatmap"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.pixelSize: Theme.fontSm
|
||||
Layout.preferredWidth: 52
|
||||
}
|
||||
Slider {
|
||||
@@ -500,7 +500,7 @@ ColumnLayout {
|
||||
Label {
|
||||
text: Math.round(heatmapSlider.value * 100) + "%"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.pixelSize: Theme.fontSm
|
||||
font.weight: Font.Medium
|
||||
Layout.preferredWidth: 32
|
||||
horizontalAlignment: Text.AlignRight
|
||||
@@ -531,7 +531,7 @@ ColumnLayout {
|
||||
text: "THRESHOLDS"
|
||||
color: Theme.sideMutedText
|
||||
font.family: Theme.uiFontFamily
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.pixelSize: Theme.fontSm
|
||||
font.letterSpacing: 1.5
|
||||
font.bold: true
|
||||
Layout.bottomMargin: 4
|
||||
@@ -540,7 +540,7 @@ ColumnLayout {
|
||||
Label {
|
||||
text: "Set Point (°C)"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.pixelSize: Theme.fontSm
|
||||
}
|
||||
TextField {
|
||||
id: spField
|
||||
@@ -570,7 +570,7 @@ ColumnLayout {
|
||||
Label {
|
||||
text: "Margin (±°C)"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.pixelSize: Theme.fontSm
|
||||
}
|
||||
TextField {
|
||||
id: mgField
|
||||
@@ -601,7 +601,7 @@ ColumnLayout {
|
||||
id: autoCheck
|
||||
text: "Auto range (mean ± 1σ)"
|
||||
checked: true
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.pixelSize: Theme.fontSm
|
||||
onCheckedChanged: pushThresholds()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,6 +212,7 @@ Popup {
|
||||
Label { text: "START (s)"; Layout.preferredWidth: 120; color: Theme.sideMutedText; font.bold: true; font.pixelSize: Theme.fontSm }
|
||||
Label { text: "END (s)"; Layout.preferredWidth: 120; color: Theme.sideMutedText; font.bold: true; font.pixelSize: Theme.fontSm }
|
||||
Label { text: "AVG TEMP (°C)"; Layout.fillWidth: true; color: Theme.sideMutedText; font.bold: true; font.pixelSize: Theme.fontSm }
|
||||
Item { Layout.preferredWidth: 80 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,6 +268,31 @@ Popup {
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: Theme.fontSm
|
||||
}
|
||||
|
||||
Button {
|
||||
id: exportBtn
|
||||
text: "Export"
|
||||
Layout.preferredWidth: 80
|
||||
Layout.preferredHeight: 26
|
||||
hoverEnabled: true
|
||||
onClicked: streamController.exportSegment(index)
|
||||
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusXs
|
||||
color: exportBtn.pressed ? Theme.buttonNeutralPressed
|
||||
: exportBtn.hovered ? Theme.buttonNeutralHover
|
||||
: Theme.buttonNeutralBackground
|
||||
border.color: Theme.fieldBorder
|
||||
border.width: 1
|
||||
}
|
||||
contentItem: Label {
|
||||
text: exportBtn.text
|
||||
color: Theme.buttonNeutralText
|
||||
font.pixelSize: Theme.fontXs
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,6 +321,16 @@ Popup {
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: Theme.fontSm
|
||||
}
|
||||
|
||||
// ── Export feedback ───────────────────────────────────────
|
||||
Label {
|
||||
id: exportStatus
|
||||
visible: text !== ""
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: Theme.fontSm
|
||||
elide: Text.ElideMiddle
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
// ── Backend Connection ────────────────────────────────────────
|
||||
@@ -302,6 +338,7 @@ Popup {
|
||||
target: streamController
|
||||
function onSplitResult(result) {
|
||||
root.segmenting = false;
|
||||
exportStatus.text = "";
|
||||
if (result && result.success) {
|
||||
root.segments = result.segments || [];
|
||||
console.log("[SplitDialog] Segmented into", root.segments.length, "phases");
|
||||
@@ -310,6 +347,13 @@ Popup {
|
||||
console.log("[SplitDialog] Split failed:", result.error || "unknown");
|
||||
}
|
||||
}
|
||||
|
||||
function onSegmentExported(result) {
|
||||
if (result && result.success)
|
||||
exportStatus.text = "Exported: " + result.path.split("/").pop();
|
||||
else
|
||||
exportStatus.text = "Export failed: " + (result.error || "unknown");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user