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, string extra) 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" // Nothing to export until a file is loaded or a stream has played enabled: streamController.sensorValues.length > 0 opacity: enabled ? 1 : 0.4 onClicked: { // Default into /Export (created on demand), // named after the loaded CSV so exports are traceable // back to their source (falls back to a generic name for // a live stream with no review file loaded). var loaded = streamController.loadedFile var baseName = loaded ? loaded.split("/").pop().replace(/\.[^.]+$/, "") : "wafer_map" var ts = Qt.formatDateTime(new Date(), "yyyyMMdd_HHmmss") exportDialog.selectedFile = "file://" + deviceController.exportDir() + "/" + baseName + "_" + ts + ".png" 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 defaultSuffix: "png" nameFilters: ["PNG files (*.png)"] onAccepted: { var path = String(selectedFile).replace(/^file:\/\//, ""); var extra = ""; if (streamController.mode === "live") { extra = "Frames: " + streamController.receivedCount + " Errors: " + streamController.errorCount + " Resyncs: " + streamController.resyncCount + " Elapsed: " + root.fmtTime(root._liveSecs); } root.exportRequested(path, extra); } } } 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 } } } }