640 lines
26 KiB
QML
640 lines
26 KiB
QML
import QtQuick
|
|
import QtQuick.Layouts
|
|
import QtQuick.Controls
|
|
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.centerIn: parent
|
|
spacing: 4
|
|
Text {
|
|
id: valueText
|
|
color: active ? Theme.headingColor : Theme.sideMutedText
|
|
font.pixelSize: valSize
|
|
font.weight: Font.Bold
|
|
font.family: Theme.uiFontFamily
|
|
Layout.alignment: Qt.AlignHCenter
|
|
}
|
|
Text {
|
|
id: labelText
|
|
color: Theme.sideMutedText
|
|
font.pixelSize: Theme.fontXs
|
|
font.weight: Font.Medium
|
|
font.letterSpacing: 1.2
|
|
font.family: Theme.uiFontFamily
|
|
Layout.alignment: Qt.AlignHCenter
|
|
}
|
|
}
|
|
}
|
|
|
|
component GridCell : RowLayout {
|
|
property alias label: labelText.text
|
|
property alias value: valueText.text
|
|
property bool active: root.waferDetected
|
|
|
|
spacing: 8
|
|
Layout.fillWidth: true
|
|
|
|
Text {
|
|
id: labelText
|
|
color: Theme.sideMutedText
|
|
font.pixelSize: Theme.fontXs
|
|
font.family: Theme.uiFontFamily
|
|
Layout.fillWidth: true
|
|
elide: Text.ElideRight
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
Text {
|
|
id: valueText
|
|
color: active ? Theme.headingColor : Theme.sideMutedText
|
|
font.pixelSize: Theme.fontSm
|
|
font.weight: Font.Bold
|
|
font.family: Theme.uiFontFamily
|
|
Layout.alignment: Qt.AlignRight
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
}
|
|
|
|
// ── 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: {
|
|
deviceController.setSaveDataDir(root.cleanFolderUrl(selectedFolder));
|
|
root.parseAndSavePendingRead();
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// ROW 1: Connection Status (100%)
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 90
|
|
Layout.maximumHeight: 90
|
|
spacing: 8
|
|
|
|
Rectangle {
|
|
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 {
|
|
id: connStatusCol
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.leftMargin: Theme.panelPadding
|
|
anchors.rightMargin: Theme.panelPadding
|
|
spacing: 8
|
|
|
|
Text {
|
|
text: "CONNECTION STATUS"
|
|
color: Theme.sideMutedText
|
|
font.pixelSize: Theme.fontXs
|
|
font.weight: Font.Medium
|
|
font.letterSpacing: 1.2
|
|
font.family: Theme.uiFontFamily
|
|
}
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 0
|
|
|
|
ColumnLayout {
|
|
spacing: 1
|
|
Text {
|
|
text: "HOST PC"
|
|
color: Theme.sideMutedText
|
|
font.pixelSize: Theme.fontXs
|
|
font.family: Theme.uiFontFamily
|
|
}
|
|
Text {
|
|
text: "localhost"
|
|
color: Theme.headingColor
|
|
font.pixelSize: Theme.fontMd
|
|
font.weight: Font.Bold
|
|
font.family: Theme.uiFontFamily
|
|
}
|
|
}
|
|
|
|
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: "#111111"
|
|
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 }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
spacing: 1
|
|
Text {
|
|
text: "COM PORT"
|
|
color: Theme.sideMutedText
|
|
font.pixelSize: Theme.fontXs
|
|
font.family: Theme.uiFontFamily
|
|
Layout.alignment: Qt.AlignRight
|
|
}
|
|
Text {
|
|
text: root.portName
|
|
color: root.isConnected ? Theme.statusSuccessColor : Theme.headingColor
|
|
font.pixelSize: Theme.fontMd
|
|
font.weight: Font.Bold
|
|
font.family: Theme.uiFontFamily
|
|
Layout.alignment: Qt.AlignRight
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// ROW 2: Wafer & Sensor + Directory (65%) | Total Runtime (35%)
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 90
|
|
Layout.maximumHeight: 90
|
|
spacing: 8
|
|
|
|
RowLayout {
|
|
Layout.preferredWidth: 65
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
spacing: 8
|
|
|
|
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
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 2
|
|
|
|
Text {
|
|
text: "DIRECTORY"
|
|
color: Theme.sideMutedText
|
|
font.pixelSize: Theme.fontMd
|
|
font.weight: Font.Medium
|
|
font.letterSpacing: 1.2
|
|
font.family: Theme.uiFontFamily
|
|
}
|
|
Text {
|
|
text: deviceController.saveDataDir || "Not configured"
|
|
color: Theme.bodyColor
|
|
font.pixelSize: Theme.fontSm
|
|
font.family: Theme.uiFontFamily
|
|
font.italic: true
|
|
opacity: 0.7
|
|
elide: Text.ElideMiddle
|
|
Layout.fillWidth: true
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 8
|
|
|
|
Rectangle {
|
|
visible: root.csvPath !== ""
|
|
width: sessionSavedLabel.implicitWidth + 12
|
|
height: 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.fillWidth: true }
|
|
|
|
Button {
|
|
id: browseBtn
|
|
text: "Save As"
|
|
font.pixelSize: Theme.fontSm
|
|
font.weight: Font.Medium
|
|
font.family: Theme.uiFontFamily
|
|
implicitHeight: 24
|
|
implicitWidth: 70
|
|
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: saveDirDialog.open()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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: 10
|
|
spacing: 4
|
|
|
|
Text {
|
|
text: "WAFER & SENSOR DATA"
|
|
color: Theme.sideMutedText
|
|
font.pixelSize: Theme.fontXs
|
|
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: 4
|
|
columnSpacing: 16
|
|
|
|
GridCell {
|
|
id: waferInfoFamilyCell
|
|
label: "Family Code"
|
|
value: {
|
|
var info = deviceController.lastWaferInfo;
|
|
return (info && info.length > 0 && info[0]) ? info[0] : "—";
|
|
}
|
|
}
|
|
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
|
|
}
|
|
|
|
Rectangle {
|
|
Layout.columnSpan: 2
|
|
Layout.fillWidth: true
|
|
height: 1
|
|
color: Theme.cardBorder
|
|
opacity: 0.5
|
|
}
|
|
|
|
GridCell {
|
|
id: waferSerialCell
|
|
label: "Serial"
|
|
value: {
|
|
var info = deviceController.lastWaferInfo;
|
|
return (info && info.length > 1 && info[1]) ? info[1] : "—";
|
|
}
|
|
}
|
|
GridCell {
|
|
id: waferSensorsCell
|
|
label: "Sensors"
|
|
value: {
|
|
var info = deviceController.lastWaferInfo;
|
|
return (info && info.length > 2 && info[2]) ? String(info[2]) : "—";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
StatCard {
|
|
id: runtimeCard
|
|
Layout.preferredWidth: 35
|
|
value: {
|
|
var info = deviceController.lastWaferInfo;
|
|
return (info && info.length > 4 && info[4] !== undefined) ? info[4] + "s" : "—";
|
|
}
|
|
label: "TOTAL RUNTIME"
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// 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
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: Theme.panelPadding
|
|
anchors.rightMargin: Theme.panelPadding
|
|
spacing: 8
|
|
|
|
Text {
|
|
text: "📄"
|
|
font.pixelSize: Theme.fontSm
|
|
visible: root.csvPath !== ""
|
|
Layout.alignment: Qt.AlignVCenter
|
|
}
|
|
|
|
Label {
|
|
text: root.csvPath !== "" ? root.csvPath.split("/").pop() : "ACTIVITY LOG"
|
|
color: Theme.bodyColor
|
|
font.pixelSize: root.csvPath !== "" ? Theme.fontMd : Theme.fontSm
|
|
font.letterSpacing: root.csvPath !== "" ? 0 : 1.8
|
|
font.weight: Font.Medium
|
|
font.family: Theme.uiFontFamily
|
|
elide: Text.ElideMiddle
|
|
Layout.fillWidth: true
|
|
Layout.alignment: Qt.AlignVCenter
|
|
}
|
|
|
|
Button {
|
|
id: refreshSessionBtn
|
|
text: "Refresh"
|
|
font.pixelSize: Theme.fontSm
|
|
font.family: Theme.uiFontFamily
|
|
implicitHeight: 24
|
|
hoverEnabled: true
|
|
background: Rectangle {
|
|
color: refreshSessionBtn.hovered ? Theme.buttonNeutralHover : "transparent"
|
|
radius: Theme.radiusSm
|
|
}
|
|
contentItem: Text {
|
|
text: refreshSessionBtn.text
|
|
color: Theme.bodyColor
|
|
font: refreshSessionBtn.font
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
onClicked: deviceController.clearSession()
|
|
}
|
|
|
|
Button {
|
|
id: clearLogBtn
|
|
text: "Clear"
|
|
font.pixelSize: Theme.fontSm
|
|
font.family: Theme.uiFontFamily
|
|
implicitHeight: 24
|
|
hoverEnabled: true
|
|
background: Rectangle {
|
|
color: clearLogBtn.hovered ? Theme.buttonNeutralHover : "transparent"
|
|
radius: Theme.radiusSm
|
|
}
|
|
contentItem: Text {
|
|
text: clearLogBtn.text
|
|
color: Theme.bodyColor
|
|
font: clearLogBtn.font
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
onClicked: deviceController.clearActivityLog()
|
|
}
|
|
}
|
|
|
|
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
|
|
text: 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
|
|
}
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: deviceController
|
|
function onActivityLogUpdated(newLog) {
|
|
activityLog.text = newLog ? newLog : qsTr("Welcome — use the side rail to detect a wafer, or import data to review.");
|
|
}
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// 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 || "";
|
|
} else {
|
|
root.dataParsed = false;
|
|
}
|
|
}
|
|
}
|
|
}
|