refactor: extract StreamControlPanel and MetricRow from WaferMapTab
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Controls.impl
|
import QtQuick.Controls.impl
|
||||||
import QtQuick.Dialogs
|
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import ISC
|
import ISC
|
||||||
import ISC.Tabs.components
|
import ISC.Tabs.components
|
||||||
@@ -11,22 +10,6 @@ Item {
|
|||||||
id: root
|
id: root
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
// Live elapsed-time counter
|
|
||||||
property int _liveSecs: 0
|
|
||||||
Timer {
|
|
||||||
id: liveTimer
|
|
||||||
interval: 1000
|
|
||||||
repeat: true
|
|
||||||
running: streamController.mode === "live" && streamController.state !== "idle"
|
|
||||||
onTriggered: root._liveSecs++
|
|
||||||
onRunningChanged: if (!running)
|
|
||||||
root._liveSecs = 0
|
|
||||||
}
|
|
||||||
function fmtTime(s) {
|
|
||||||
var m = Math.floor(s / 60);
|
|
||||||
var ss = s % 60;
|
|
||||||
return (m < 10 ? "0" : "") + m + ":" + (ss < 10 ? "0" : "") + ss;
|
|
||||||
}
|
|
||||||
// Wire streamController.trendData (Signal(str) carrying JSON list of floats)
|
// Wire streamController.trendData (Signal(str) carrying JSON list of floats)
|
||||||
// into the trend chart's data property. The slot parseJsonToData() is defined
|
// into the trend chart's data property. The slot parseJsonToData() is defined
|
||||||
// on the Python TrendChartItem; we use it so malformed payloads are logged
|
// on the Python TrendChartItem; we use it so malformed payloads are logged
|
||||||
@@ -37,13 +20,8 @@ Item {
|
|||||||
function onTrendData(avgsJson) {
|
function onTrendData(avgsJson) {
|
||||||
trendChart.setDataFromJson(avgsJson);
|
trendChart.setDataFromJson(avgsJson);
|
||||||
}
|
}
|
||||||
// A file was picked from the rail (any tab) — loadFile() switches the
|
|
||||||
// backend to review mode; mirror that in the toolbar toggle so a
|
|
||||||
// successful load is never hidden behind a stale "Live" selection.
|
|
||||||
function onLoadedFileChanged() {
|
function onLoadedFileChanged() {
|
||||||
root.loadErrorMessage = "";
|
root.loadErrorMessage = "";
|
||||||
if (streamController.mode === "review")
|
|
||||||
modeBar.currentIndex = 0;
|
|
||||||
}
|
}
|
||||||
function onLoadFileError(message) {
|
function onLoadFileError(message) {
|
||||||
root.loadErrorMessage = message;
|
root.loadErrorMessage = message;
|
||||||
@@ -60,107 +38,6 @@ Item {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: 10
|
spacing: 10
|
||||||
|
|
||||||
// Mode toggle
|
|
||||||
TabBar {
|
|
||||||
id: modeBar
|
|
||||||
currentIndex: 0 // Default to "Review"
|
|
||||||
spacing: 2
|
|
||||||
padding: 2
|
|
||||||
background: Rectangle {
|
|
||||||
color: Theme.subtleSectionBackground
|
|
||||||
radius: Theme.radiusSm
|
|
||||||
border.color: Theme.cardBorder
|
|
||||||
border.width: Theme.borderThin
|
|
||||||
}
|
|
||||||
TabButton {
|
|
||||||
text: "Review"
|
|
||||||
implicitWidth: 84
|
|
||||||
implicitHeight: 34
|
|
||||||
background: Rectangle {
|
|
||||||
color: parent.checked ? Theme.tabActiveBackground : "transparent"
|
|
||||||
radius: Theme.radiusSm - 1
|
|
||||||
}
|
|
||||||
contentItem: Text {
|
|
||||||
text: parent.text
|
|
||||||
color: parent.checked ? Theme.headingColor : Theme.bodyColor
|
|
||||||
font.pixelSize: Theme.fontMd
|
|
||||||
font.weight: parent.checked ? Font.Medium : Font.Normal
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TabButton {
|
|
||||||
id: liveTab
|
|
||||||
text: "Live"
|
|
||||||
enabled: deviceController.connectionStatus === "Connected"
|
|
||||||
opacity: enabled ? 1.0 : 0.4
|
|
||||||
implicitWidth: 84
|
|
||||||
implicitHeight: 34
|
|
||||||
// Disabled controls swallow no events at all, so hover for the
|
|
||||||
// "why is this greyed out" tooltip needs its own MouseArea.
|
|
||||||
AppToolTip {
|
|
||||||
visible: disabledHover.containsMouse && !liveTab.enabled
|
|
||||||
text: "Connect a wafer to enable Live mode"
|
|
||||||
delay: 300
|
|
||||||
}
|
|
||||||
MouseArea {
|
|
||||||
id: disabledHover
|
|
||||||
anchors.fill: parent
|
|
||||||
enabled: !liveTab.enabled
|
|
||||||
hoverEnabled: true
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
}
|
|
||||||
background: Rectangle {
|
|
||||||
color: parent.checked ? Theme.tabActiveBackground : "transparent"
|
|
||||||
radius: Theme.radiusSm - 1
|
|
||||||
}
|
|
||||||
contentItem: Row {
|
|
||||||
spacing: 5
|
|
||||||
leftPadding: 8
|
|
||||||
Rectangle {
|
|
||||||
id: liveTabDot
|
|
||||||
width: 7; height: 7; radius: 3.5
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
color: liveTab.enabled ? Theme.metricGood : Theme.sideMutedText
|
|
||||||
|
|
||||||
// Flash while actually streaming live
|
|
||||||
SequentialAnimation on opacity {
|
|
||||||
running: streamController.mode === "live" && streamController.state !== "idle"
|
|
||||||
loops: Animation.Infinite
|
|
||||||
NumberAnimation { to: 0.2; duration: 600; easing.type: Easing.InOutQuad }
|
|
||||||
NumberAnimation { to: 1.0; duration: 600; easing.type: Easing.InOutQuad }
|
|
||||||
onRunningChanged: if (!running) liveTabDot.opacity = 1.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Text {
|
|
||||||
text: liveTab.text
|
|
||||||
color: liveTab.checked ? Theme.headingColor : Theme.bodyColor
|
|
||||||
font.pixelSize: Theme.fontMd
|
|
||||||
font.weight: liveTab.checked ? Font.Medium : Font.Normal
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
height: parent.height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onCurrentIndexChanged: if (currentIndex === 1) {
|
|
||||||
// Guard: revert to Review if not connected
|
|
||||||
if (deviceController.connectionStatus !== "Connected") {
|
|
||||||
currentIndex = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Entering Live mode
|
|
||||||
streamController.setMode("live");
|
|
||||||
var fc = "";
|
|
||||||
if (deviceController.lastWaferInfo && deviceController.lastWaferInfo.length > 0) {
|
|
||||||
fc = deviceController.lastWaferInfo[0];
|
|
||||||
}
|
|
||||||
streamController.startStream(deviceController.selectedPort, fc);
|
|
||||||
} else {
|
|
||||||
// Entering Review Mode (automatically calls stopStream in backend)
|
|
||||||
streamController.setMode("review");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
@@ -215,49 +92,6 @@ Item {
|
|||||||
font.letterSpacing: 1.2
|
font.letterSpacing: 1.2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record start/stop toggle (Live mode)
|
|
||||||
Button {
|
|
||||||
visible: streamController.mode === "live"
|
|
||||||
enabled: streamController.state !== "idle" || streamController.recording
|
|
||||||
text: streamController.recording ? "Stop REC" : "Record"
|
|
||||||
onClicked: {
|
|
||||||
if (streamController.recording) {
|
|
||||||
streamController.stopRecording();
|
|
||||||
} else {
|
|
||||||
var info = deviceController.lastWaferInfo;
|
|
||||||
var serial = (info && info.length > 1) ? info[1] : "";
|
|
||||||
var ts = Qt.formatDateTime(new Date(), "yyyyMMdd_HHmmss");
|
|
||||||
var name = "live_" + (serial ? serial + "_" : "") + ts + ".csv";
|
|
||||||
streamController.startRecording(deviceController.saveDataDir + "/" + name, serial);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stream statistics popup (Live mode)
|
|
||||||
Button {
|
|
||||||
visible: streamController.mode === "live"
|
|
||||||
text: "Stats"
|
|
||||||
onClicked: streamStatsDialog.open()
|
|
||||||
}
|
|
||||||
|
|
||||||
StreamStatsDialog {
|
|
||||||
id: streamStatsDialog
|
|
||||||
elapsedSecs: root._liveSecs
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
text: "Export PNG"
|
|
||||||
onClicked: exportDialog.open()
|
|
||||||
}
|
|
||||||
|
|
||||||
FileDialog {
|
|
||||||
id: exportDialog
|
|
||||||
title: "Export Wafer Map"
|
|
||||||
fileMode: FileDialog.SaveFile
|
|
||||||
nameFilters: ["PNG files (*.png)"]
|
|
||||||
onAccepted: waferView.exportImage(String(selectedFile).replace(/^file:\/\//, ""))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Load error banner ───────────────────────────────────────────
|
// ── Load error banner ───────────────────────────────────────────
|
||||||
@@ -354,6 +188,9 @@ Item {
|
|||||||
id: readoutPanel
|
id: readoutPanel
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 0
|
anchors.margins: 0
|
||||||
|
onExportRequested: function(filePath) {
|
||||||
|
waferView.exportImage(filePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import ISC
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
property string label
|
||||||
|
property string value
|
||||||
|
property color valueColor: Theme.headingColor
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: label
|
||||||
|
color: Theme.sideMutedText
|
||||||
|
font.family: Theme.uiFontFamily
|
||||||
|
font.pixelSize: Theme.fontMd
|
||||||
|
font.bold: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
text: value
|
||||||
|
color: valueColor
|
||||||
|
font.family: Theme.codeFontFamily
|
||||||
|
font.pixelSize: Theme.fontMd
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,8 @@ ColumnLayout {
|
|||||||
property alias heatmapBlend: heatmapSlider.value
|
property alias heatmapBlend: heatmapSlider.value
|
||||||
property alias showThickness: thicknessToggle.checked
|
property alias showThickness: thicknessToggle.checked
|
||||||
|
|
||||||
|
signal exportRequested(string filePath)
|
||||||
|
|
||||||
component PanelCheckBox: CheckBox {
|
component PanelCheckBox: CheckBox {
|
||||||
id: toggle
|
id: toggle
|
||||||
indicator: Rectangle {
|
indicator: Rectangle {
|
||||||
@@ -75,112 +77,10 @@ ColumnLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Live-mode stream stats + stop — moved here from the footer transport bar
|
StreamControlPanel {
|
||||||
// so that bar collapses to zero height in live mode, freeing vertical
|
|
||||||
// space for the trend chart.
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
visible: streamController.mode === "live"
|
onExportRequested: function(filePath) {
|
||||||
implicitHeight: visible ? liveStatsCard.implicitHeight + 24 : 0
|
root.exportRequested(filePath);
|
||||||
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.fontSm
|
|
||||||
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.fontSm
|
|
||||||
font.bold: true
|
|
||||||
}
|
|
||||||
Item { Layout.fillWidth: true }
|
|
||||||
Label {
|
|
||||||
text: streamController.receivedCount
|
|
||||||
color: Theme.headingColor
|
|
||||||
font.family: Theme.codeFontFamily
|
|
||||||
font.pixelSize: Theme.fontLg
|
|
||||||
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.fontSm
|
|
||||||
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.fontLg
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,359 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Controls.impl
|
||||||
|
import QtQuick.Dialogs
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import ISC
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: contentCol.implicitHeight + 24
|
||||||
|
|
||||||
|
color: Theme.sidePanelBackground
|
||||||
|
radius: Theme.sidePanelRadius
|
||||||
|
border.color: Theme.sideBorder
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
signal exportRequested(string filePath)
|
||||||
|
|
||||||
|
property int _liveSecs: 0
|
||||||
|
Connections {
|
||||||
|
target: streamController
|
||||||
|
function onModeChanged() {
|
||||||
|
if (streamController.mode === "review" && modeBar.currentIndex !== 0) {
|
||||||
|
modeBar.currentIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Timer {
|
||||||
|
id: liveTimer
|
||||||
|
interval: 1000
|
||||||
|
repeat: true
|
||||||
|
running: streamController.mode === "live" && streamController.state !== "idle"
|
||||||
|
onTriggered: root._liveSecs++
|
||||||
|
onRunningChanged: if (!running) root._liveSecs = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtTime(s) {
|
||||||
|
var m = Math.floor(s / 60);
|
||||||
|
var ss = s % 60;
|
||||||
|
return (m < 10 ? "0" : "") + m + ":" + (ss < 10 ? "0" : "") + ss;
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchToReview() {
|
||||||
|
modeBar.currentIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: contentCol
|
||||||
|
anchors {
|
||||||
|
fill: parent
|
||||||
|
topMargin: 10
|
||||||
|
leftMargin: 12
|
||||||
|
rightMargin: 12
|
||||||
|
bottomMargin: 12
|
||||||
|
}
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
TabBar {
|
||||||
|
id: modeBar
|
||||||
|
currentIndex: 0
|
||||||
|
spacing: 2
|
||||||
|
padding: 2
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 28
|
||||||
|
background: Rectangle {
|
||||||
|
color: Theme.subtleSectionBackground
|
||||||
|
radius: Theme.radiusXs
|
||||||
|
border.color: Theme.cardBorder
|
||||||
|
border.width: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
TabButton {
|
||||||
|
text: "Review"
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: 24
|
||||||
|
font.pixelSize: Theme.fontMd
|
||||||
|
font.weight: parent.checked ? Font.Medium : Font.Normal
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.checked ? Theme.tabActiveBackground : "transparent"
|
||||||
|
radius: Theme.radiusXs - 1
|
||||||
|
}
|
||||||
|
contentItem: Text {
|
||||||
|
text: parent.text
|
||||||
|
color: parent.checked ? Theme.headingColor : Theme.bodyColor
|
||||||
|
font: parent.font
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TabButton {
|
||||||
|
id: liveTab
|
||||||
|
text: "Live"
|
||||||
|
enabled: deviceController.connectionStatus === "Connected"
|
||||||
|
opacity: enabled ? 1.0 : 0.4
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: 24
|
||||||
|
font.pixelSize: Theme.fontMd
|
||||||
|
font.weight: parent.checked ? Font.Medium : Font.Normal
|
||||||
|
|
||||||
|
AppToolTip {
|
||||||
|
visible: disabledHover.containsMouse && !liveTab.enabled
|
||||||
|
text: "Connect a wafer to enable Live mode"
|
||||||
|
delay: 300
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: disabledHover
|
||||||
|
anchors.fill: parent
|
||||||
|
enabled: !liveTab.enabled
|
||||||
|
hoverEnabled: true
|
||||||
|
acceptedButtons: Qt.NoButton
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.checked ? Theme.tabActiveBackground : "transparent"
|
||||||
|
radius: Theme.radiusXs - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: Item {
|
||||||
|
Row {
|
||||||
|
spacing: 4
|
||||||
|
anchors.centerIn: parent
|
||||||
|
Rectangle {
|
||||||
|
id: liveTabDot
|
||||||
|
width: 5; height: 5; radius: 2.5
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: liveTab.enabled ? Theme.metricGood : Theme.sideMutedText
|
||||||
|
|
||||||
|
SequentialAnimation on opacity {
|
||||||
|
running: streamController.mode === "live" && streamController.state !== "idle"
|
||||||
|
loops: Animation.Infinite
|
||||||
|
NumberAnimation { to: 0.2; duration: 600; easing.type: Easing.InOutQuad }
|
||||||
|
NumberAnimation { to: 1.0; duration: 600; easing.type: Easing.InOutQuad }
|
||||||
|
onRunningChanged: if (!running) liveTabDot.opacity = 1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: liveTab.text
|
||||||
|
color: liveTab.checked ? Theme.headingColor : Theme.bodyColor
|
||||||
|
font: liveTab.font
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
height: parent.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
if (currentIndex === 1) {
|
||||||
|
if (deviceController.connectionStatus !== "Connected") {
|
||||||
|
currentIndex = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
streamController.setMode("live");
|
||||||
|
var fc = "";
|
||||||
|
if (deviceController.lastWaferInfo && deviceController.lastWaferInfo.length > 0) {
|
||||||
|
fc = deviceController.lastWaferInfo[0];
|
||||||
|
}
|
||||||
|
streamController.startStream(deviceController.selectedPort, fc);
|
||||||
|
} else {
|
||||||
|
streamController.setMode("review");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
height: 1
|
||||||
|
color: Theme.cardBorder
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: "METRICS"
|
||||||
|
color: Theme.sideMutedText
|
||||||
|
font.family: Theme.uiFontFamily
|
||||||
|
font.pixelSize: Theme.fontMd
|
||||||
|
font.letterSpacing: 1.5
|
||||||
|
font.bold: true
|
||||||
|
Layout.bottomMargin: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
MetricRow {
|
||||||
|
label: "Received Frames"
|
||||||
|
value: modeBar.currentIndex === 1 ? streamController.receivedCount : "—"
|
||||||
|
}
|
||||||
|
|
||||||
|
MetricRow {
|
||||||
|
label: "Errors"
|
||||||
|
value: modeBar.currentIndex === 1 ? streamController.errorCount : "—"
|
||||||
|
valueColor: streamController.errorCount > 0 ? Theme.statusWarningColor : Theme.headingColor
|
||||||
|
}
|
||||||
|
|
||||||
|
MetricRow {
|
||||||
|
label: "Resyncs"
|
||||||
|
value: modeBar.currentIndex === 1 ? streamController.resyncCount : "—"
|
||||||
|
}
|
||||||
|
|
||||||
|
MetricRow {
|
||||||
|
label: "Elapsed"
|
||||||
|
value: modeBar.currentIndex === 1 ? root.fmtTime(root._liveSecs) : "—"
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
height: 1
|
||||||
|
color: Theme.cardBorder
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: "ACTIONS"
|
||||||
|
color: Theme.sideMutedText
|
||||||
|
font.family: Theme.uiFontFamily
|
||||||
|
font.pixelSize: Theme.fontMd
|
||||||
|
font.letterSpacing: 1.5
|
||||||
|
font.bold: true
|
||||||
|
Layout.bottomMargin: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
Button {
|
||||||
|
id: recordBtn
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: 28
|
||||||
|
enabled: streamController.mode === "live"
|
||||||
|
opacity: enabled ? 1.0 : 0.4
|
||||||
|
text: streamController.recording ? "Stop REC" : "Record"
|
||||||
|
|
||||||
|
AppToolTip {
|
||||||
|
visible: recordBtn.hovered && !recordBtn.enabled
|
||||||
|
text: "Connect to Live to start recording"
|
||||||
|
delay: 300
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
if (streamController.recording) {
|
||||||
|
streamController.stopRecording();
|
||||||
|
} else {
|
||||||
|
var info = deviceController.lastWaferInfo;
|
||||||
|
var serial = (info && info.length > 1) ? info[1] : "";
|
||||||
|
var ts = Qt.formatDateTime(new Date(), "yyyyMMdd_HHmmss");
|
||||||
|
var name = "live_" + (serial ? serial + "_" : "") + ts + ".csv";
|
||||||
|
streamController.startRecording(deviceController.saveDataDir + "/" + name, serial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
radius: Theme.radiusSm
|
||||||
|
color: recordBtn.down ? Theme.transportButtonHover
|
||||||
|
: (recordBtn.hovered ? Theme.transportButtonBg : "transparent")
|
||||||
|
border.width: Theme.borderThin
|
||||||
|
border.color: Theme.sideBorder
|
||||||
|
}
|
||||||
|
contentItem: Text {
|
||||||
|
text: recordBtn.text
|
||||||
|
color: Theme.headingColor
|
||||||
|
font.pixelSize: Theme.fontMd
|
||||||
|
font.weight: Font.Medium
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
id: exportBtn
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: 28
|
||||||
|
text: "Export"
|
||||||
|
|
||||||
|
onClicked: exportDialog.open()
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
radius: Theme.radiusSm
|
||||||
|
color: exportBtn.down ? Theme.transportButtonHover
|
||||||
|
: (exportBtn.hovered ? Theme.transportButtonBg : "transparent")
|
||||||
|
border.width: Theme.borderThin
|
||||||
|
border.color: Theme.sideBorder
|
||||||
|
}
|
||||||
|
contentItem: Text {
|
||||||
|
text: exportBtn.text
|
||||||
|
color: Theme.headingColor
|
||||||
|
font.pixelSize: Theme.fontMd
|
||||||
|
font.weight: Font.Medium
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileDialog {
|
||||||
|
id: exportDialog
|
||||||
|
title: "Export Wafer Map"
|
||||||
|
fileMode: FileDialog.SaveFile
|
||||||
|
nameFilters: ["PNG files (*.png)"]
|
||||||
|
onAccepted: {
|
||||||
|
var path = String(selectedFile).replace(/^file:\/\//, "");
|
||||||
|
root.exportRequested(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
height: 1
|
||||||
|
color: Theme.cardBorder
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
id: primaryBtn
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: 32
|
||||||
|
text: streamController.mode === "live" ? "STOP" : "START"
|
||||||
|
enabled: streamController.mode === "live"
|
||||||
|
? true
|
||||||
|
: deviceController.connectionStatus === "Connected"
|
||||||
|
opacity: enabled ? 1.0 : 0.4
|
||||||
|
|
||||||
|
AppToolTip {
|
||||||
|
visible: primaryBtn.hovered && !primaryBtn.enabled
|
||||||
|
text: "Connect a wafer to enable Live mode"
|
||||||
|
delay: 300
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
if (streamController.mode === "live") {
|
||||||
|
streamController.stopStream();
|
||||||
|
modeBar.currentIndex = 0;
|
||||||
|
} else {
|
||||||
|
modeBar.currentIndex = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
radius: Theme.radiusSm
|
||||||
|
color: primaryBtn.down ? Theme.transportButtonHover
|
||||||
|
: (primaryBtn.hovered ? Theme.transportButtonBg : "transparent")
|
||||||
|
border.width: Theme.borderThin
|
||||||
|
border.color: Theme.sideBorder
|
||||||
|
}
|
||||||
|
contentItem: Text {
|
||||||
|
text: primaryBtn.text
|
||||||
|
color: Theme.headingColor
|
||||||
|
font.pixelSize: Theme.fontMd
|
||||||
|
font.weight: Font.Medium
|
||||||
|
font.letterSpacing: 1.0
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Controls.impl
|
|
||||||
import ISC
|
|
||||||
|
|
||||||
// ===== Stream Stats Dialog =====
|
|
||||||
// Popup showing live stream statistics: received frames, errors,
|
|
||||||
// resync count, and elapsed time. Opened from the Live toolbar.
|
|
||||||
|
|
||||||
Popup {
|
|
||||||
id: root
|
|
||||||
modal: true
|
|
||||||
dim: true
|
|
||||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
|
||||||
width: 320
|
|
||||||
implicitHeight: content.implicitHeight + 40
|
|
||||||
anchors.centerIn: Overlay.overlay
|
|
||||||
|
|
||||||
// Elapsed seconds supplied by the Live timer in WaferMapTab.
|
|
||||||
property int elapsedSecs: 0
|
|
||||||
|
|
||||||
function fmtTime(s) {
|
|
||||||
var m = Math.floor(s / 60);
|
|
||||||
var ss = s % 60;
|
|
||||||
return (m < 10 ? "0" : "") + m + ":" + (ss < 10 ? "0" : "") + ss;
|
|
||||||
}
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
radius: Theme.radiusMd
|
|
||||||
color: Theme.cardBackground
|
|
||||||
border.color: Theme.cardBorder
|
|
||||||
border.width: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
component StatRow: RowLayout {
|
|
||||||
property string label
|
|
||||||
property string value
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
Label {
|
|
||||||
text: label
|
|
||||||
color: Theme.bodyColor
|
|
||||||
font.pixelSize: Theme.fontXs
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
Label {
|
|
||||||
text: value
|
|
||||||
color: Theme.headingColor
|
|
||||||
font.pixelSize: Theme.fontSm
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: content
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 20
|
|
||||||
spacing: 16
|
|
||||||
|
|
||||||
// ── Header ────────────────────────────────────────────────
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 10
|
|
||||||
|
|
||||||
Label {
|
|
||||||
text: "STREAM DATA"
|
|
||||||
font.pixelSize: Theme.fontLg
|
|
||||||
font.bold: true
|
|
||||||
color: Theme.headingColor
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
flat: true
|
|
||||||
Layout.preferredWidth: 32
|
|
||||||
Layout.preferredHeight: 32
|
|
||||||
onClicked: root.close()
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
radius: Theme.radiusXs
|
|
||||||
color: parent.hovered ? Theme.buttonNeutralHover : "transparent"
|
|
||||||
}
|
|
||||||
contentItem: IconImage {
|
|
||||||
source: "../icons/x.svg"
|
|
||||||
width: 14; height: 14
|
|
||||||
sourceSize.width: 14
|
|
||||||
sourceSize.height: 14
|
|
||||||
color: Theme.bodyColor
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Stats box ─────────────────────────────────────────────
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
implicitHeight: statRows.implicitHeight + 20
|
|
||||||
radius: Theme.radiusSm
|
|
||||||
color: Theme.subtleSectionBackground
|
|
||||||
border.color: Theme.cardBorder
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: statRows
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 10
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
StatRow {
|
|
||||||
label: "Received frames"
|
|
||||||
value: streamController.receivedCount
|
|
||||||
}
|
|
||||||
StatRow {
|
|
||||||
label: "Errors"
|
|
||||||
value: streamController.errorCount
|
|
||||||
}
|
|
||||||
StatRow {
|
|
||||||
label: "Resyncs"
|
|
||||||
value: streamController.resyncCount
|
|
||||||
}
|
|
||||||
StatRow {
|
|
||||||
label: "Elapsed"
|
|
||||||
value: root.fmtTime(root.elapsedSecs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,12 +8,13 @@ ReplaceSensorDialog 1.0 ReplaceSensorDialog.qml
|
|||||||
RailActionButton 1.0 RailActionButton.qml
|
RailActionButton 1.0 RailActionButton.qml
|
||||||
SelectFileDialog 1.0 SelectFileDialog.qml
|
SelectFileDialog 1.0 SelectFileDialog.qml
|
||||||
SplitDialog 1.0 SplitDialog.qml
|
SplitDialog 1.0 SplitDialog.qml
|
||||||
StreamStatsDialog 1.0 StreamStatsDialog.qml
|
StreamControlPanel 1.0 StreamControlPanel.qml
|
||||||
StatusActionsPanel 1.0 StatusActionsPanel.qml
|
StatusActionsPanel 1.0 StatusActionsPanel.qml
|
||||||
ConnectionFooter 1.0 ConnectionFooter.qml
|
ConnectionFooter 1.0 ConnectionFooter.qml
|
||||||
UtilityFooter 1.0 UtilityFooter.qml
|
UtilityFooter 1.0 UtilityFooter.qml
|
||||||
PanelCheckBox 1.0 PanelCheckBox.qml
|
PanelCheckBox 1.0 PanelCheckBox.qml
|
||||||
ReadoutStat 1.0 ReadoutStat.qml
|
ReadoutStat 1.0 ReadoutStat.qml
|
||||||
|
MetricRow 1.0 MetricRow.qml
|
||||||
AppToolTip 1.0 AppToolTip.qml
|
AppToolTip 1.0 AppToolTip.qml
|
||||||
SectionTitle 1.0 SectionTitle.qml
|
SectionTitle 1.0 SectionTitle.qml
|
||||||
PanelBox 1.0 PanelBox.qml
|
PanelBox 1.0 PanelBox.qml
|
||||||
|
|||||||
Reference in New Issue
Block a user