Files
pyGUI/src/pygui/ISC/Tabs/StatusTab.qml
T

640 lines
25 KiB
QML

import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Controls.impl
import QtQuick.Dialogs
import ISC
// ===== Status Tab — Bento Grid Dashboard =====
// ROW 1: Connection Status (65%) | Cycles Completed (35%)
// ROW 2: Wafer & Sensor + Directory (65%) | Total Runtime (35%)
// ROW 3: Activity Log (100%, fills remaining)
ColumnLayout {
id: root
anchors.fill: parent
anchors.margins: Theme.panelPadding
spacing: 8
// ── Reusable Inline Components ──
component StatCard : Rectangle {
property alias value: valueText.text
property alias label: labelText.text
property bool active: root.waferDetected
property int valSize: active ? Theme.font3xl : Theme.font2xl
Layout.fillWidth: true
Layout.fillHeight: true
color: Theme.cardBackground
border.color: Theme.cardBorder
border.width: 1
radius: Theme.radiusSm
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.panelPadding
spacing: 4
Text {
id: labelText
color: Theme.sideMutedText
font.pixelSize: Theme.fontSm
font.bold: true
font.letterSpacing: 1.5
font.family: Theme.uiFontFamily
}
Text {
id: valueText
color: active ? Theme.headingColor : Theme.sideMutedText
font.pixelSize: valSize
font.weight: Font.Bold
font.family: Theme.uiFontFamily
Layout.fillHeight: true
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
fontSizeMode: Text.HorizontalFit
minimumPixelSize: Theme.fontMd
}
}
}
// Stacked stat block: small label over big value, centered in its grid
// quadrant so the card's height is used instead of leaving dead space.
component GridCell : ColumnLayout {
property alias label: labelText.text
property alias value: valueText.text
property bool active: root.waferDetected
spacing: 2
Layout.fillWidth: true
Layout.fillHeight: true
Item { Layout.fillHeight: true }
Text {
id: labelText
color: Theme.sideMutedText
font.pixelSize: Theme.fontXs
font.family: Theme.uiFontFamily
Layout.fillWidth: true
elide: Text.ElideRight
}
Text {
id: valueText
color: active ? Theme.headingColor : Theme.sideMutedText
font.pixelSize: Theme.fontXl
font.weight: Font.Bold
font.family: Theme.uiFontFamily
}
Item { Layout.fillHeight: true }
}
// ── Aliases for external wiring ──
property alias waferFamilyCode: waferInfoFamilyCell.value
property alias waferSerialNumber: waferSerialCell.value
property alias waferSensorCount: waferSensorsCell.value
property alias waferRuntime: runtimeCard.value
property alias waferCycles: waferCyclesCell.value
property bool waferDetected: {
var info = deviceController.lastWaferInfo;
return info && info.length > 0 && info[0] !== "";
}
property int dataRows: 0
property int dataCols: 0
property string csvPath: ""
property bool dataParsed: false
readonly property bool isConnected: deviceController.connectionStatus === "Connected"
readonly property bool isBusy: deviceController.connectionStatus.endsWith("...")
readonly property string portName: deviceController.selectedPort || "—"
function cleanFolderUrl(url) {
return decodeURIComponent(String(url).replace(/^file:\/\//, ""));
}
function currentFamilyCode() {
var info = deviceController.lastWaferInfo;
return info && info.length > 0 ? (info[0] || "") : "";
}
function parseAndSavePendingRead() {
deviceController.parseAndSaveData(root.currentFamilyCode(), deviceController.selectedPort || "");
}
FolderDialog {
id: saveDirDialog
title: "Choose a folder to save."
onAccepted: {
var dir = root.cleanFolderUrl(selectedFolder);
deviceController.setSaveDataDir(dir);
file_browser.setCurrentDirectory(dir);
root.parseAndSavePendingRead();
}
}
// Directory-only picker for the DIRECTORY card button — sets the save dir
// without kicking off a parse/save of pending read data.
FolderDialog {
id: dirOnlyDialog
title: "Choose a folder to save Data"
onAccepted: {
var dir = root.cleanFolderUrl(selectedFolder);
deviceController.setSaveDataDir(dir);
file_browser.setCurrentDirectory(dir);
}
}
// Post-read metadata editor (C# parity: EditCSVMetadataDialog after read).
// Cancel keeps the CSV, skips the sidecar — matching C# behavior loosely.
Dialog {
id: editCsvDialog
parent: Overlay.overlay
modal: true
title: "Edit CSV Metadata"
width: 460
anchors.centerIn: parent
standardButtons: Dialog.Save | Dialog.Cancel
onAccepted: file_browser.saveMetadata(root.csvPath, metaWafer.text, metaDate.text, metaChamber.text, metaNotes.text, false, "")
ColumnLayout {
anchors.fill: parent
spacing: 6
Text {
text: root.csvPath.split("/").pop()
color: Theme.sideMutedText
font.pixelSize: Theme.fontXs
elide: Text.ElideMiddle
Layout.fillWidth: true
}
Label { text: "Wafer" }
TextField { id: metaWafer; Layout.fillWidth: true }
Label { text: "Date" }
TextField { id: metaDate; Layout.fillWidth: true }
Label { text: "Chamber" }
TextField { id: metaChamber; Layout.fillWidth: true }
Label { text: "Notes" }
TextField { id: metaNotes; Layout.fillWidth: true }
}
}
// ═══════════════════════════════════════════════════════════════════════
// ROWS 1-2 (3-column bento):
// [Connection Status] [Total Runtime ] [Wafer & Sensor]
// [Directory (span 2) ] [ (rowspan) ]
// ═══════════════════════════════════════════════════════════════════════
GridLayout {
Layout.fillWidth: true
Layout.preferredHeight: 188
Layout.maximumHeight: 188
columns: 3
rowSpacing: 8
columnSpacing: 8
// ── Connection Status: two cells wide, top-left ──
Rectangle {
Layout.columnSpan: 2
Layout.fillWidth: true
Layout.fillHeight: true
color: Theme.cardBackground
border.color: root.isConnected ? Theme.statusSuccessColor : (root.isBusy ? Theme.statusWarningColor : Theme.statusErrorColor)
border.width: 1
radius: Theme.radiusSm
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.panelPadding
spacing: 6
Text {
text: "CONNECTION STATUS"
color: Theme.sideMutedText
font.pixelSize: Theme.fontSm
font.bold: true
font.letterSpacing: 1.5
font.family: Theme.uiFontFamily
}
Item { Layout.fillHeight: true }
RowLayout {
Layout.fillWidth: true
spacing: 8
Item {
Layout.fillWidth: true
implicitHeight: 22
Row {
anchors.centerIn: parent
spacing: 5
Repeater {
model: 4
Rectangle {
width: 4; height: 4; radius: 2
anchors.verticalCenter: parent.verticalCenter
color: root.isConnected ? Theme.statusSuccessColor : (root.isBusy ? Theme.statusWarningColor : Theme.sideMutedText)
opacity: (root.isConnected || root.isBusy) ? 1.0 : 0.3
SequentialAnimation on opacity {
running: root.isConnected || root.isBusy
loops: Animation.Infinite
NumberAnimation { to: 0.3; duration: 1400 }
NumberAnimation { to: 1.0; duration: 1400 }
}
}
}
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: statusPillText.implicitWidth + 16
height: 20
radius: 10
color: root.isConnected ? Theme.statusSuccessColor : (root.isBusy ? Theme.statusWarningColor : Theme.statusErrorColor)
Text {
id: statusPillText
anchors.centerIn: parent
text: root.isConnected ? "ACTIVE" : (root.isBusy ? deviceController.connectionStatus.replace("...", "").toUpperCase() : "INACTIVE")
color: Theme.statusBadgeText
font.pixelSize: Theme.fontXs
font.weight: Font.Bold
font.family: Theme.uiFontFamily
font.letterSpacing: 0.6
}
}
Repeater {
model: 4
Rectangle {
width: 4; height: 4; radius: 2
anchors.verticalCenter: parent.verticalCenter
color: root.isConnected ? Theme.statusSuccessColor : (root.isBusy ? Theme.statusWarningColor : Theme.sideMutedText)
opacity: (root.isConnected || root.isBusy) ? 1.0 : 0.3
SequentialAnimation on opacity {
running: root.isConnected || root.isBusy
loops: Animation.Infinite
NumberAnimation { to: 0.3; duration: 1400 }
NumberAnimation { to: 1.0; duration: 1400 }
}
}
}
}
}
Text {
text: root.portName
color: root.isConnected ? Theme.statusSuccessColor : Theme.headingColor
font.pixelSize: Theme.fontMd
font.weight: Font.Bold
font.family: Theme.uiFontFamily
elide: Text.ElideMiddle
}
}
}
}
// ── Wafer & Sensor Data: far right, spans both rows ──
Rectangle {
Layout.rowSpan: 2
Layout.fillWidth: true
Layout.fillHeight: true
color: Theme.cardBackground
border.color: Theme.cardBorder
border.width: 1
radius: Theme.radiusSm
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.panelPadding
spacing: 4
Text {
text: "WAFER & SENSOR DATA"
color: Theme.sideMutedText
font.pixelSize: Theme.fontSm
font.weight: Font.Bold
font.family: Theme.uiFontFamily
font.letterSpacing: 0.6
Layout.bottomMargin: 2
}
GridLayout {
Layout.fillWidth: true
Layout.fillHeight: true
columns: 2
rowSpacing: 6
columnSpacing: 16
GridCell {
id: waferInfoFamilyCell
label: "Family Code"
value: {
var info = deviceController.lastWaferInfo;
return (info && info.length > 0 && info[0]) ? info[0] : "—";
}
}
GridCell {
id: waferSerialCell
label: "Serial"
value: {
var info = deviceController.lastWaferInfo;
return (info && info.length > 1 && info[1]) ? info[1] : "—";
}
}
Rectangle {
Layout.columnSpan: 2
Layout.fillWidth: true
Layout.preferredHeight: 1
color: Theme.cardBorder
opacity: 0.5
}
GridCell {
id: waferCyclesCell
label: "Cycles Completed"
value: {
var info = deviceController.lastWaferInfo;
return (info && info.length > 5 && info[5] !== undefined) ? String(info[5]) : "—";
}
active: root.waferDetected
}
GridCell {
id: waferSensorsCell
label: "Sensors"
value: {
var info = deviceController.lastWaferInfo;
return (info && info.length > 2 && info[2]) ? String(info[2]) : "—";
}
}
}
}
}
// ── Directory: bottom-left cell ──
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Theme.cardBackground
border.color: Theme.cardBorder
border.width: 1
radius: Theme.radiusSm
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.panelPadding
spacing: 6
RowLayout {
Layout.fillWidth: true
spacing: 8
Text {
text: "DIRECTORY"
color: Theme.sideMutedText
font.pixelSize: Theme.fontSm
font.bold: true
font.letterSpacing: 1.5
font.family: Theme.uiFontFamily
Layout.fillWidth: true
}
Rectangle {
visible: root.csvPath !== ""
implicitWidth: sessionSavedLabel.implicitWidth + 12
implicitHeight: 18
radius: 4
color: Theme.fieldBackground
border.color: Theme.sideBorder
border.width: 1
Text {
id: sessionSavedLabel
anchors.centerIn: parent
text: "SESSION SAVED"
color: Theme.sideMutedText
font.pixelSize: Theme.fontXs
font.weight: Font.Bold
font.family: Theme.uiFontFamily
font.letterSpacing: 0.6
}
}
}
Item { Layout.fillHeight: true }
RowLayout {
Layout.fillWidth: true
spacing: 12
// Path pinned bottom-left
Text {
text: deviceController.saveDataDir || "Not configured"
color: Theme.bodyColor
font.pixelSize: Theme.fontMd
font.family: Theme.uiFontFamily
font.italic: true
opacity: 0.7
elide: Text.ElideMiddle
Layout.fillWidth: true
}
Button {
id: browseBtn
text: "Select Folder"
font.pixelSize: Theme.fontSm
font.weight: Font.Medium
font.family: Theme.uiFontFamily
implicitHeight: 24
hoverEnabled: true
background: Rectangle {
color: browseBtn.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground
border.color: Theme.cardBorder
border.width: 1
radius: Theme.radiusSm
}
contentItem: Text {
text: browseBtn.text
color: Theme.bodyColor
font: browseBtn.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: dirOnlyDialog.open()
}
}
}
}
// ── Total Runtime: bottom center ──
StatCard {
id: runtimeCard
label: "TOTAL RUNTIME"
value: {
var info = deviceController.lastWaferInfo;
if (!(info && info.length > 4 && info[4] !== undefined)) return "—";
var secs = parseFloat(info[4]);
if (isNaN(secs)) return info[4] + "s";
var m = Math.floor(secs / 60);
var s = Math.round(secs % 60);
return secs + "s / " + m + ":" + (s < 10 ? "0" : "") + s + " min";
}
}
}
// ═══════════════════════════════════════════════════════════════════════
// ROW 3: Activity Log — ALWAYS visible (fills remaining height)
// ═══════════════════════════════════════════════════════════════════════
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Theme.cardBackground
border.color: Theme.cardBorder
border.width: 1
radius: Theme.radiusMd
clip: true
ColumnLayout {
anchors.fill: parent
spacing: 0
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
color: Theme.subtleSectionBackground
radius: Theme.radiusMd
// Cover bottom corners so only top-left and top-right are rounded
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: parent.radius
color: parent.color
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: Theme.panelPadding
anchors.rightMargin: Theme.panelPadding
spacing: 8
IconImage {
source: "icons/file-text.svg"
width: 14; height: 14
sourceSize.width: 14
sourceSize.height: 14
color: Theme.sideMutedText
visible: root.csvPath !== ""
Layout.alignment: Qt.AlignVCenter
}
Label {
text: (root.csvPath !== "" ? root.csvPath.split("/").pop() : "ACTIVITY LOG").toUpperCase()
color: Theme.sideMutedText
font.pixelSize: Theme.fontMd
font.letterSpacing: 1.5
font.bold: true
font.family: Theme.uiFontFamily
elide: Text.ElideMiddle
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
}
}
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: 1
color: Theme.cardBorder
}
}
ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.margins: Theme.panelPadding
clip: true
TextArea {
id: activityLog
width: parent.width
// Bound to backend so log survives tab switches (Loader
// destroys this tab); clears only via Clear Log / restart.
text: deviceController.activityLog || qsTr("Welcome — use the side rail to detect a wafer, or import data to review.")
readOnly: true
font.family: "monospace"
font.pixelSize: Theme.fontSm
wrapMode: TextArea.WordWrap
color: Theme.bodyColor
background: null
}
}
}
}
// ═══════════════════════════════════════════════════════════════════════
// SIGNAL HANDLERS
// ═══════════════════════════════════════════════════════════════════════
Connections {
target: deviceController
function onLogMessage(message) {
if (message == "CHOOSE_SAVE_DIR") {
saveDirDialog.open();
}
}
function onPortsUpdated() {}
function onDetectResult(result) {
if (!result) {
root.dataParsed = false;
root.dataRows = 0;
root.dataCols = 0;
root.csvPath = "";
}
}
function onStatusRestored() {
if (deviceController.dataRowCount > 0) {
root.dataParsed = true;
root.dataRows = deviceController.dataRowCount;
root.dataCols = deviceController.dataColCount;
root.csvPath = "";
}
}
}
Connections {
target: deviceController
function onReadResult(result) {
root.dataParsed = false;
root.dataRows = 0;
root.dataCols = 0;
root.csvPath = "";
if (result && result.success === true)
saveDirDialog.open();
}
}
Connections {
target: deviceController
function onParsedDataReady(result) {
if (result && result.success) {
root.dataParsed = true;
root.dataRows = result.rows;
root.dataCols = result.cols;
root.csvPath = result.csv_path || "";
if (root.csvPath) {
metaWafer.text = result.serialNumber || "";
metaDate.text = Qt.formatDateTime(new Date(), "yyyy-MM-dd hh:mm:ss");
metaChamber.text = settingsModel.chamberId || "";
metaNotes.text = "";
editCsvDialog.open();
}
} else {
root.dataParsed = false;
}
}
}
}