feat(tabs): update all tab views, dialogs, and session controller

This commit is contained in:
jack
2026-06-29 14:58:10 -07:00
parent db21f201f6
commit 5105ab1ab6
10 changed files with 507 additions and 307 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ import ISC
Window { Window {
// ===== Window Dimensions ===== // ===== Window Dimensions =====
width: 1920 width: 1920
height: 1080 height: 1080
minimumWidth: 1100 minimumWidth: 1100
minimumHeight: 700 minimumHeight: 700
visible: true visible: true
+31 -21
View File
@@ -1,7 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.TableView
import ISC import ISC
// ===== Data Tab ===== // ===== Data Tab =====
@@ -18,7 +17,7 @@ Item {
target: deviceController target: deviceController
function onParsedDataReady(result) { function onParsedDataReady(result) {
if (result && result.success) { if (result && result.success) {
dataTable.forceLayout() dataTable.forceLayout();
} }
} }
} }
@@ -31,7 +30,7 @@ Item {
Label { Label {
text: "No Data" text: "No Data"
font.pixelSize: 24 font.pixelSize: Theme.font2xl
font.bold: true font.bold: true
color: Theme.headingColor color: Theme.headingColor
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@@ -39,7 +38,7 @@ Item {
Label { Label {
text: "Read and parse wafer data to see temperature readings here." text: "Read and parse wafer data to see temperature readings here."
font.pixelSize: 14 font.pixelSize: Theme.fontMd
color: Theme.bodyColor color: Theme.bodyColor
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
@@ -60,17 +59,18 @@ Item {
Label { Label {
text: "Temperature Data" text: "Temperature Data"
font.pixelSize: 18 font.pixelSize: Theme.fontXl
font.bold: true font.bold: true
color: Theme.headingColor color: Theme.headingColor
} }
Item { Layout.fillWidth: true } Item {
Layout.fillWidth: true
}
Label { Label {
text: deviceController.dataRowCount + " rows × " + text: deviceController.dataRowCount + " rows × " + deviceController.dataColCount + " sensors"
deviceController.dataColCount + " sensors" font.pixelSize: Theme.fontSm
font.pixelSize: 13
color: Theme.bodyColor color: Theme.bodyColor
} }
} }
@@ -82,7 +82,8 @@ Item {
Button { Button {
text: "Open in Excel" text: "Open in Excel"
icon.source: "icon/excel.svg" icon.source: "icons/excel.svg"
icon.color: Theme.headingColor
onClicked: deviceController.openCsvFile() onClicked: deviceController.openCsvFile()
} }
@@ -96,7 +97,9 @@ Item {
onClicked: splitDialog.open() onClicked: splitDialog.open()
} }
Item { Layout.fillWidth: true} Item {
Layout.fillWidth: true
}
} }
// --- Table container --- // --- Table container ---
Rectangle { Rectangle {
@@ -131,7 +134,7 @@ Item {
// 'display' is the Qt.DisplayRole value from headerData() // 'display' is the Qt.DisplayRole value from headerData()
text: display !== undefined ? display : "" text: display !== undefined ? display : ""
color: Theme.headingColor color: Theme.headingColor
font.pixelSize: 11 font.pixelSize: Theme.fontXs
font.bold: true font.bold: true
elide: Text.ElideRight elide: Text.ElideRight
} }
@@ -147,14 +150,17 @@ Item {
clip: true clip: true
reuseItems: true reuseItems: true
columnWidthProvider: function(col) { columnWidthProvider: function (col) {
if (col === 0) return 52 // row index column if (col === 0)
const sensorCols = Math.max(1, dataTable.columns - 1) return 52; // row index column
const available = dataTable.width - 52 const sensorCols = Math.max(1, dataTable.columns - 1);
return Math.max(52, Math.min(90, available / sensorCols)) const available = dataTable.width - 52;
return Math.max(52, Math.min(90, available / sensorCols));
} }
rowHeightProvider: function() { return 24 } rowHeightProvider: function () {
return 24;
}
delegate: Rectangle { delegate: Rectangle {
color: row % 2 === 0 ? Theme.panelBackground : Theme.cardBackground color: row % 2 === 0 ? Theme.panelBackground : Theme.cardBackground
@@ -163,13 +169,17 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
text: display !== undefined ? display : "" text: display !== undefined ? display : ""
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 11 font.pixelSize: Theme.fontXs
elide: Text.ElideRight elide: Text.ElideRight
} }
} }
ScrollBar.horizontal: ScrollBar { policy: ScrollBar.AsNeeded } ScrollBar.horizontal: ScrollBar {
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } policy: ScrollBar.AsNeeded
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
} }
} }
} }
+40 -37
View File
@@ -27,18 +27,20 @@ Item {
Label { Label {
text: "Line Chart" text: "Line Chart"
color: Theme.headingColor color: Theme.headingColor
font.pixelSize: 13 font.pixelSize: Theme.fontSm
font.bold: true font.bold: true
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
Item { Layout.fillWidth: true } Item {
Layout.fillWidth: true
}
// Refresh button — pulls data from deviceController // Refresh button — pulls data from deviceController
Button { Button {
id: refreshBtn id: refreshBtn
text: "REFRESH" text: "REFRESH"
font.pixelSize: 10 font.pixelSize: Theme.fontXs
font.weight: Font.Medium font.weight: Font.Medium
implicitHeight: 26 implicitHeight: 26
implicitWidth: 72 implicitWidth: 72
@@ -68,7 +70,7 @@ Item {
Label { Label {
text: "Source:" text: "Source:"
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 10 font.pixelSize: Theme.fontXs
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
@@ -77,7 +79,7 @@ Item {
model: ["Parsed Data", "Live Trend"] model: ["Parsed Data", "Live Trend"]
currentIndex: 0 currentIndex: 0
implicitHeight: 26 implicitHeight: 26
font.pixelSize: 11 font.pixelSize: Theme.fontXs
background: Rectangle { background: Rectangle {
color: Theme.fieldBackground color: Theme.fieldBackground
border.color: Theme.fieldBorder border.color: Theme.fieldBorder
@@ -95,13 +97,15 @@ Item {
onCurrentIndexChanged: reloadData() onCurrentIndexChanged: reloadData()
} }
Item { Layout.fillWidth: true } Item {
Layout.fillWidth: true
}
Label { Label {
id: statusLabel id: statusLabel
text: "Ready" text: "Ready"
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 10 font.pixelSize: Theme.fontXs
font.italic: true font.italic: true
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
@@ -125,8 +129,7 @@ Item {
gridColor: Theme.softBorder gridColor: Theme.softBorder
axisColor: Theme.bodyColor axisColor: Theme.bodyColor
textColor: Theme.headingColor textColor: Theme.headingColor
seriesColors: [Theme.sensorLow, Theme.sensorHigh, Theme.sensorInRange, seriesColors: [Theme.sensorLow, Theme.sensorHigh, Theme.sensorInRange, "#FFA726", "#AB47BC", "#00BCD4"]
"#FFA726", "#AB47BC", "#00BCD4"]
showLegend: true showLegend: true
} }
} }
@@ -137,44 +140,44 @@ Item {
function reloadData() { function reloadData() {
if (sourceSelector.currentIndex === 0) { if (sourceSelector.currentIndex === 0) {
// Parsed data from DeviceController // Parsed data from DeviceController
loadParsedData() loadParsedData();
} else { } else {
// Live trend from SessionController // Live trend from SessionController
loadLiveTrend() loadLiveTrend();
} }
} }
function loadParsedData() { function loadParsedData() {
statusLabel.text = "Loading..." statusLabel.text = "Loading...";
var result = deviceController.getChartData() var result = deviceController.getChartData();
if (!result || !result.success) { if (!result || !result.success) {
graph.seriesData = [] graph.seriesData = [];
graph.sensorNames = [] graph.sensorNames = [];
graph.title = "Sensor Temperature Over Time" graph.title = "Sensor Temperature Over Time";
statusLabel.text = "No data — read a wafer first" statusLabel.text = "No data — read a wafer first";
return return;
} }
graph.seriesData = result.series || [] graph.seriesData = result.series || [];
graph.sensorNames = result.sensor_names || [] graph.sensorNames = result.sensor_names || [];
graph.title = "Sensor Temperature Over Time" graph.title = "Sensor Temperature Over Time";
statusLabel.text = (result.series ? result.series.length : 0) + " sensors loaded" statusLabel.text = (result.series ? result.series.length : 0) + " sensors loaded";
} }
function loadLiveTrend() { function loadLiveTrend() {
// Live trend: connects to streamController.trendData // Live trend: connects to streamController.trendData
// The trend data comes as a JSON array of per-frame averages // The trend data comes as a JSON array of per-frame averages
statusLabel.text = "Connecting to live trend..." statusLabel.text = "Connecting to live trend...";
// We need at least one data point // We need at least one data point
if (streamController.stats && streamController.stats.avg !== undefined) { if (streamController.stats && streamController.stats.avg !== undefined) {
// Build a single-series graph from the trend buffer // Build a single-series graph from the trend buffer
// For now show a placeholder — the trendData signal handles updates // For now show a placeholder — the trendData signal handles updates
graph.title = "Live Average Temperature" graph.title = "Live Average Temperature";
graph.yLabel = "Avg Temperature (°C)" graph.yLabel = "Avg Temperature (°C)";
graph.xLabel = "Time (s)" graph.xLabel = "Time (s)";
statusLabel.text = "Live trend — waiting for data..." statusLabel.text = "Live trend — waiting for data...";
} else { } else {
statusLabel.text = "No live data — start a stream on the Wafer Map tab" statusLabel.text = "No live data — start a stream on the Wafer Map tab";
} }
} }
@@ -184,7 +187,7 @@ Item {
target: deviceController target: deviceController
function onParsedDataReady(result) { function onParsedDataReady(result) {
if (sourceSelector.currentIndex === 0) { if (sourceSelector.currentIndex === 0) {
root.reloadData() root.reloadData();
} }
} }
} }
@@ -194,14 +197,14 @@ Item {
function onTrendData(avgsJson) { function onTrendData(avgsJson) {
if (sourceSelector.currentIndex === 1) { if (sourceSelector.currentIndex === 1) {
try { try {
var avgs = JSON.parse(avgsJson) var avgs = JSON.parse(avgsJson);
if (avgs && avgs.length > 0) { if (avgs && avgs.length > 0) {
graph.seriesData = [avgs] graph.seriesData = [avgs];
graph.sensorNames = ["Average"] graph.sensorNames = ["Average"];
graph.title = "Live Average Temperature" graph.title = "Live Average Temperature";
graph.yLabel = "Avg Temperature (°C)" graph.yLabel = "Avg Temperature (°C)";
graph.xLabel = "Frame" graph.xLabel = "Frame";
statusLabel.text = "Live: " + avgs.length + " data points" statusLabel.text = "Live: " + avgs.length + " data points";
} }
} catch (e) { } catch (e) {
// ignore parse errors // ignore parse errors
@@ -213,7 +216,7 @@ Item {
// On startup, try to load parsed data if available // On startup, try to load parsed data if available
Component.onCompleted: { Component.onCompleted: {
if (deviceController.dataRowCount > 0) { if (deviceController.dataRowCount > 0) {
reloadData() reloadData();
} }
} }
} }
+25 -17
View File
@@ -24,9 +24,11 @@ Dialog {
property int selectedRow: -1 property int selectedRow: -1
property int metadataEditRow: -1 property int metadataEditRow: -1
readonly property string masterTypeFieldText: { readonly property string masterTypeFieldText: {
if (metadataEditRow < 0) return ""; if (metadataEditRow < 0)
return "";
const rec = tableModel[metadataEditRow]; const rec = tableModel[metadataEditRow];
if (!rec) return ""; if (!rec)
return "";
return rec.masterType ?? ""; return rec.masterType ?? "";
} }
@@ -42,7 +44,7 @@ Dialog {
chamberField.text = rec.chamber ?? ""; chamberField.text = rec.chamber ?? "";
if (!chamberField.text && settingsModel.chamberId) if (!chamberField.text && settingsModel.chamberId)
chamberField.text = settingsModel.chamberId; chamberField.text = settingsModel.chamberId;
notesField.text = rec.notes ?? ""; notesField.text = rec.notes ?? "";
filePathLabel.text = rec.fileName ?? ""; filePathLabel.text = rec.fileName ?? "";
metadataEditDialog.open(); metadataEditDialog.open();
} }
@@ -76,7 +78,7 @@ Dialog {
readonly property var normalizedRows: { readonly property var normalizedRows: {
// Touch settingsModel.masters so QML tracks this dependency. // Touch settingsModel.masters so QML tracks this dependency.
const _ = settingsModel.masters; const _ = settingsModel.masters;
return (tableModel || []).map(function(row) { return (tableModel || []).map(function (row) {
row = row || {}; row = row || {};
// settingsModel is the authoritative source; fall back to sidecar masterType. // settingsModel is the authoritative source; fall back to sidecar masterType.
const masterType = root.masterFamilyForPath(row.fileName ?? "") || row.masterType || ""; const masterType = root.masterFamilyForPath(row.fileName ?? "") || row.masterType || "";
@@ -113,7 +115,7 @@ Dialog {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight elide: Text.ElideRight
font.bold: true font.bold: true
font.pixelSize: 13 font.pixelSize: Theme.fontSm
} }
} }
@@ -129,6 +131,12 @@ Dialog {
color: Theme.fieldBackground color: Theme.fieldBackground
border.width: dialogTextInput.activeFocus ? Theme.borderStrong : Theme.borderThin border.width: dialogTextInput.activeFocus ? Theme.borderStrong : Theme.borderThin
border.color: dialogTextInput.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder border.color: dialogTextInput.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
Behavior on border.color {
ColorAnimation { duration: Theme.durationFast }
}
} }
} }
@@ -163,7 +171,7 @@ Dialog {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 16 anchors.leftMargin: 16
text: root.title text: root.title
font.pixelSize: 15 font.pixelSize: Theme.fontLg
font.bold: true font.bold: true
color: Theme.headingColor color: Theme.headingColor
} }
@@ -221,7 +229,7 @@ Dialog {
text: file_browser.currentDirectory text: file_browser.currentDirectory
color: Theme.fieldText color: Theme.fieldText
elide: Text.ElideMiddle elide: Text.ElideMiddle
font.pixelSize: 13 font.pixelSize: Theme.fontSm
} }
} }
} }
@@ -242,7 +250,7 @@ Dialog {
anchors.centerIn: parent anchors.centerIn: parent
text: root.headerTitles[index] ?? "" text: root.headerTitles[index] ?? ""
font.bold: true font.bold: true
font.pixelSize: 12 font.pixelSize: Theme.fontSm
color: Theme.headingColor color: Theme.headingColor
} }
} }
@@ -329,7 +337,7 @@ Dialog {
const mt = root.normalizedRows[row]?.masterType ?? ""; const mt = root.normalizedRows[row]?.masterType ?? "";
return mt || "—"; return mt || "—";
} }
font.pixelSize: 14 font.pixelSize: Theme.fontMd
font.bold: true font.bold: true
color: { color: {
const mt = root.normalizedRows[row]?.masterType ?? ""; const mt = root.normalizedRows[row]?.masterType ?? "";
@@ -346,7 +354,7 @@ Dialog {
sourceComponent: Button { sourceComponent: Button {
hoverEnabled: true hoverEnabled: true
text: "Edit…" text: "Edit…"
font.pixelSize: 12 font.pixelSize: Theme.fontSm
onClicked: root.openMetadataEditor(row) onClicked: root.openMetadataEditor(row)
background: Rectangle { background: Rectangle {
@@ -361,7 +369,7 @@ Dialog {
color: Theme.buttonNeutralText color: Theme.buttonNeutralText
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
font.pixelSize: 12 font.pixelSize: Theme.fontSm
} }
} }
} }
@@ -388,7 +396,7 @@ Dialog {
return ""; return "";
} }
elide: Text.ElideRight elide: Text.ElideRight
font.pixelSize: 13 font.pixelSize: Theme.fontSm
color: Theme.bodyColor color: Theme.bodyColor
} }
} }
@@ -409,12 +417,12 @@ Dialog {
return record.notes ?? ""; return record.notes ?? "";
} }
elide: Text.ElideRight elide: Text.ElideRight
font.pixelSize: 13 font.pixelSize: Theme.fontSm
color: Theme.bodyColor color: Theme.bodyColor
} }
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
enabled: column !== 6 enabled: column !== 6
onClicked: { onClicked: {
@@ -456,7 +464,7 @@ Dialog {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
text: root.selectedPath text: root.selectedPath
elide: Text.ElideLeft elide: Text.ElideLeft
font.pixelSize: 13 font.pixelSize: Theme.fontSm
color: root.selectedPath ? Theme.fieldText : Theme.fieldPlaceholder color: root.selectedPath ? Theme.fieldText : Theme.fieldPlaceholder
} }
} }
@@ -504,7 +512,7 @@ Dialog {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 16 anchors.leftMargin: 16
text: metadataEditDialog.title text: metadataEditDialog.title
font.pixelSize: 15 font.pixelSize: Theme.fontLg
font.bold: true font.bold: true
color: Theme.headingColor color: Theme.headingColor
} }
@@ -530,7 +538,7 @@ Dialog {
maximumLineCount: 3 maximumLineCount: 3
elide: Text.ElideRight elide: Text.ElideRight
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 12 font.pixelSize: Theme.fontSm
} }
Label { Label {
+44 -12
View File
@@ -27,8 +27,7 @@ Item {
} }
// ===== Settings Data Helpers ===== // ===== Settings Data Helpers =====
readonly property var masterFamilies: ["A", "B", "C", "D", "E", "F", "P", "X", "Z"] // ListModel for master families defined inline in the ColumnLayout below
function masterPath(family) { function masterPath(family) {
const table = settingsModel.masters; const table = settingsModel.masters;
return table && table[family] ? table[family] : ""; return table && table[family] ? table[family] : "";
@@ -58,7 +57,7 @@ Item {
label: Label { label: Label {
text: settingsGroup.title text: settingsGroup.title
color: Theme.panelTitleText color: Theme.panelTitleText
font.pixelSize: 16 font.pixelSize: Theme.fontLg
font.bold: true font.bold: true
} }
} }
@@ -75,6 +74,12 @@ Item {
color: Theme.fieldBackground color: Theme.fieldBackground
border.width: textInput.activeFocus ? Theme.borderStrong : Theme.borderThin border.width: textInput.activeFocus ? Theme.borderStrong : Theme.borderThin
border.color: textInput.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder border.color: textInput.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
Behavior on border.color {
ColorAnimation { duration: Theme.durationFast }
}
} }
} }
@@ -111,13 +116,23 @@ Item {
border.width: Theme.borderThin border.width: Theme.borderThin
border.color: toggle.checked ? Theme.primaryAccent : Theme.fieldBorder border.color: toggle.checked ? Theme.primaryAccent : Theme.fieldBorder
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
Behavior on border.color {
ColorAnimation { duration: Theme.durationFast }
}
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: "✓" text: "✓"
font.pixelSize: 14 font.pixelSize: Theme.fontMd
font.bold: true font.bold: true
color: Theme.panelBackground color: Theme.panelBackground
visible: toggle.checked opacity: toggle.checked ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation { duration: Theme.durationFast }
}
// Small nudge to center the checkmark perfectly // Small nudge to center the checkmark perfectly
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@@ -129,7 +144,7 @@ Item {
color: Theme.headingColor color: Theme.headingColor
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
leftPadding: toggle.indicator.width + toggle.spacing leftPadding: toggle.indicator.width + toggle.spacing
font.pixelSize: 13 font.pixelSize: Theme.fontSm
} }
} }
@@ -159,6 +174,7 @@ Item {
anchors.top: settingsScroll.top anchors.top: settingsScroll.top
anchors.right: settingsScroll.right anchors.right: settingsScroll.right
anchors.bottom: settingsScroll.bottom anchors.bottom: settingsScroll.bottom
anchors.leftMargin: 6
policy: ScrollBar.AsNeeded policy: ScrollBar.AsNeeded
contentItem: Rectangle { contentItem: Rectangle {
@@ -174,16 +190,19 @@ Item {
} }
} }
// Gap reserved between the centered content and the right-edge scrollbar.
readonly property int scrollGap: 100
ColumnLayout { ColumnLayout {
id: container id: container
width: Math.min(settingsScroll.availableWidth, Theme.settingsPanelMaxWidth) width: Math.min(settingsScroll.availableWidth - settingsScroll.scrollGap, Theme.settingsPanelMaxWidth)
x: (settingsScroll.availableWidth - width) / 2 x: (settingsScroll.availableWidth - width) / 2
spacing: Theme.settingsSectionSpacing spacing: Theme.settingsSectionSpacing
// ===== Page Title ===== // ===== Page Title =====
Label { Label {
text: "Settings" text: "Settings"
font.pixelSize: 30 font.pixelSize: Theme.font3xl
font.bold: true font.bold: true
color: Theme.headingColor color: Theme.headingColor
} }
@@ -241,11 +260,24 @@ Item {
anchors.fill: parent anchors.fill: parent
spacing: Theme.settingsGridSpacing spacing: Theme.settingsGridSpacing
ListModel {
id: masterModel
ListElement { letter: "A" }
ListElement { letter: "B" }
ListElement { letter: "C" }
ListElement { letter: "D" }
ListElement { letter: "E" }
ListElement { letter: "F" }
ListElement { letter: "P" }
ListElement { letter: "X" }
ListElement { letter: "Z" }
}
Repeater { Repeater {
model: root.masterFamilies model: masterModel
RowLayout { RowLayout {
property string family: modelData property string family: model.letter
Layout.fillWidth: true Layout.fillWidth: true
spacing: Theme.settingsRowSpacing spacing: Theme.settingsRowSpacing
@@ -361,7 +393,7 @@ Item {
Label { Label {
text: Theme.isDarkMode ? "Dark" : "Light" text: Theme.isDarkMode ? "Dark" : "Light"
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 13 font.pixelSize: Theme.fontSm
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Behavior on color { Behavior on color {
@@ -423,7 +455,7 @@ Item {
Label { Label {
anchors.centerIn: parent anchors.centerIn: parent
text: "↓ Scroll for more" text: "↓ Scroll for more"
font.pixelSize: 13 font.pixelSize: Theme.fontSm
font.bold: true font.bold: true
color: Theme.bodyColor color: Theme.bodyColor
opacity: (settingsScroll.ScrollBar.vertical.position <= 0 && settingsScroll.contentHeight > settingsScroll.height) ? (scrollHintTimer.running ? 1 : 0) : 0 opacity: (settingsScroll.ScrollBar.vertical.position <= 0 && settingsScroll.contentHeight > settingsScroll.height) ? (scrollHintTimer.running ? 1 : 0) : 0
+176 -96
View File
@@ -27,24 +27,24 @@ ColumnLayout {
property bool dataParsed: false property bool dataParsed: false
function cleanFolderUrl(url) { function cleanFolderUrl(url) {
return decodeURIComponent(String(url).replace(/^file:\/\//, "")) return decodeURIComponent(String(url).replace(/^file:\/\//, ""));
} }
function currentFamilyCode() { function currentFamilyCode() {
var info = deviceController.lastWaferInfo var info = deviceController.lastWaferInfo;
return info && info.length > 0 ? (info[0] || "") : "" return info && info.length > 0 ? (info[0] || "") : "";
} }
function parseAndSavePendingRead() { function parseAndSavePendingRead() {
deviceController.parseAndSaveData(root.currentFamilyCode(), deviceController.selectedPort || "") deviceController.parseAndSaveData(root.currentFamilyCode(), deviceController.selectedPort || "");
} }
FolderDialog { FolderDialog {
id: saveDirDialog id: saveDirDialog
title: "Choose a folder to save." title: "Choose a folder to save."
onAccepted: { onAccepted: {
deviceController.setSaveDataDir(root.cleanFolderUrl(selectedFolder)) deviceController.setSaveDataDir(root.cleanFolderUrl(selectedFolder));
root.parseAndSavePendingRead() root.parseAndSavePendingRead();
} }
} }
@@ -59,9 +59,11 @@ ColumnLayout {
Layout.preferredHeight: 92 Layout.preferredHeight: 92
color: Theme.cardBackground color: Theme.cardBackground
border.color: { border.color: {
if (deviceController.connectionStatus === "Connected") return Theme.statusSuccessColor if (deviceController.connectionStatus === "Connected")
if (deviceController.connectionStatus === "Disconnected") return Theme.statusErrorColor return Theme.statusSuccessColor;
return Theme.cardBorder if (deviceController.connectionStatus === "Disconnected")
return Theme.statusErrorColor;
return Theme.cardBorder;
} }
radius: Theme.radiusSm radius: Theme.radiusSm
@@ -73,19 +75,21 @@ ColumnLayout {
Text { Text {
text: deviceController.connectionStatus text: deviceController.connectionStatus
color: { color: {
if (deviceController.connectionStatus === "Connected") return Theme.statusSuccessColor if (deviceController.connectionStatus === "Connected")
if (deviceController.connectionStatus === "Disconnected") return Theme.statusErrorColor return Theme.statusSuccessColor;
return Theme.bodyColor if (deviceController.connectionStatus === "Disconnected")
return Theme.statusErrorColor;
return Theme.bodyColor;
} }
font.bold: true font.bold: true
font.pixelSize: 16 font.pixelSize: Theme.fontLg
Layout.fillWidth: true Layout.fillWidth: true
} }
Text { Text {
text: "Port: " + (deviceController.selectedPort || "None selected") text: "Port: " + (deviceController.selectedPort || "None selected")
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 14 font.pixelSize: Theme.fontMd
Layout.fillWidth: true Layout.fillWidth: true
} }
@@ -97,7 +101,7 @@ ColumnLayout {
Text { Text {
text: "Save dir: " + (deviceController.saveDataDir || "None selected") text: "Save dir: " + (deviceController.saveDataDir || "None selected")
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 12 font.pixelSize: Theme.fontSm
opacity: 0.7 opacity: 0.7
elide: Text.ElideMiddle elide: Text.ElideMiddle
Layout.fillWidth: true Layout.fillWidth: true
@@ -107,7 +111,7 @@ ColumnLayout {
Button { Button {
id: browseBtn id: browseBtn
text: "BROWSE..." text: "BROWSE..."
font.pixelSize: 9 font.pixelSize: Theme.font2xs
font.weight: Font.Medium font.weight: Font.Medium
implicitHeight: 20 implicitHeight: 20
implicitWidth: 65 implicitWidth: 65
@@ -149,22 +153,52 @@ ColumnLayout {
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 4 spacing: 4
Text { text: "Family Code"; color: Theme.subheadingColor; font.pixelSize: 12 } Text {
Text { id: waferInfoFamily; text: "—"; color: Theme.bodyColor; font.pixelSize: 16; font.bold: true } text: "Family Code"
color: Theme.subheadingColor
font.pixelSize: Theme.fontSm
}
Text {
id: waferInfoFamily
text: "—"
color: Theme.bodyColor
font.pixelSize: Theme.fontLg
font.bold: true
}
} }
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 4 spacing: 4
Text { text: "Serial Number"; color: Theme.subheadingColor; font.pixelSize: 12 } Text {
Text { id: waferSerial; text: "—"; color: Theme.bodyColor; font.pixelSize: 16; font.bold: true } text: "Serial Number"
color: Theme.subheadingColor
font.pixelSize: Theme.fontSm
}
Text {
id: waferSerial
text: "—"
color: Theme.bodyColor
font.pixelSize: Theme.fontLg
font.bold: true
}
} }
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 4 spacing: 4
Text { text: "Sensor Count"; color: Theme.subheadingColor; font.pixelSize: 12 } Text {
Text { id: waferSensors; text: "—"; color: Theme.bodyColor; font.pixelSize: 16; font.bold: true } text: "Sensor Count"
color: Theme.subheadingColor
font.pixelSize: Theme.fontSm
}
Text {
id: waferSensors
text: "—"
color: Theme.bodyColor
font.pixelSize: Theme.fontLg
font.bold: true
}
} }
} }
@@ -175,15 +209,35 @@ ColumnLayout {
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 4 spacing: 4
Text { text: "Runtime"; color: Theme.subheadingColor; font.pixelSize: 12 } Text {
Text { id: waferRuntime; text: "—"; color: Theme.bodyColor; font.pixelSize: 16; font.bold: true } text: "Runtime"
color: Theme.subheadingColor
font.pixelSize: Theme.fontSm
}
Text {
id: waferRuntime
text: "—"
color: Theme.bodyColor
font.pixelSize: Theme.fontLg
font.bold: true
}
} }
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 4 spacing: 4
Text { text: "Cycles"; color: Theme.subheadingColor; font.pixelSize: 12 } Text {
Text { id: waferCycles; text: "—"; color: Theme.bodyColor; font.pixelSize: 16; font.bold: true } text: "Cycles"
color: Theme.subheadingColor
font.pixelSize: Theme.fontSm
}
Text {
id: waferCycles
text: "—"
color: Theme.bodyColor
font.pixelSize: Theme.fontLg
font.bold: true
}
} }
} }
} }
@@ -203,22 +257,49 @@ ColumnLayout {
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 4 spacing: 4
Text { text: "Rows"; color: Theme.subheadingColor; font.pixelSize: 12 } Text {
Text { text: String(root.dataRows); color: Theme.bodyColor; font.pixelSize: 16; font.bold: true } text: "Rows"
color: Theme.subheadingColor
font.pixelSize: Theme.fontSm
}
Text {
text: String(root.dataRows)
color: Theme.bodyColor
font.pixelSize: Theme.fontLg
font.bold: true
}
} }
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 4 spacing: 4
Text { text: "Sensors"; color: Theme.subheadingColor; font.pixelSize: 12 } Text {
Text { text: String(root.dataCols); color: Theme.bodyColor; font.pixelSize: 16; font.bold: true } text: "Sensors"
color: Theme.subheadingColor
font.pixelSize: Theme.fontSm
}
Text {
text: String(root.dataCols)
color: Theme.bodyColor
font.pixelSize: Theme.fontLg
font.bold: true
}
} }
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 4 spacing: 4
Text { text: "CSV"; color: Theme.subheadingColor; font.pixelSize: 12 } Text {
Text { text: root.csvPath; color: Theme.bodyColor; font.pixelSize: 12; elide: Text.ElideMiddle } text: "CSV"
color: Theme.subheadingColor
font.pixelSize: Theme.fontSm
}
Text {
text: root.csvPath
color: Theme.bodyColor
font.pixelSize: Theme.fontSm
elide: Text.ElideMiddle
}
} }
} }
} }
@@ -250,7 +331,7 @@ ColumnLayout {
Label { Label {
text: "ACTIVITY LOG" text: "ACTIVITY LOG"
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 10 font.pixelSize: Theme.fontXs
font.letterSpacing: 1.8 font.letterSpacing: 1.8
font.weight: Font.Medium font.weight: Font.Medium
Layout.fillWidth: true Layout.fillWidth: true
@@ -259,7 +340,7 @@ ColumnLayout {
Button { Button {
text: "REFRESH SESSION" text: "REFRESH SESSION"
font.pixelSize: 10 font.pixelSize: Theme.fontXs
implicitHeight: 24 implicitHeight: 24
hoverEnabled: true hoverEnabled: true
background: Rectangle { background: Rectangle {
@@ -278,7 +359,7 @@ ColumnLayout {
Button { Button {
text: "CLEAR LOG" text: "CLEAR LOG"
font.pixelSize: 10 font.pixelSize: Theme.fontXs
implicitHeight: 24 implicitHeight: 24
hoverEnabled: true hoverEnabled: true
background: Rectangle { background: Rectangle {
@@ -311,88 +392,87 @@ ColumnLayout {
Layout.margins: Theme.panelPadding Layout.margins: Theme.panelPadding
clip: true clip: true
TextArea { TextArea {
id: activityLog id: activityLog
width: parent.width width: parent.width
text: qsTr("Welcome — use the side rail to detect a wafer, or import data to review.") text: qsTr("Welcome — use the side rail to detect a wafer, or import data to review.")
readOnly: true readOnly: true
font.family: "monospace" font.family: "monospace"
font.pixelSize: 12 font.pixelSize: Theme.fontSm
wrapMode: TextArea.WordWrap wrapMode: TextArea.WordWrap
color: Theme.bodyColor color: Theme.bodyColor
background: null background: null
} }
} }
} }
Connections { Connections {
target: deviceController target: deviceController
function onActivityLogUpdated(newLog) { function onActivityLogUpdated(newLog) {
activityLog.text = newLog activityLog.text = newLog;
} }
} }
} }
} }
// --- Signal Handlers --- // --- Signal Handlers ---
Connections { Connections {
target: deviceController target: deviceController
function onLogMessage(message) { function onLogMessage(message) {
if (message == "CHOOSE_SAVE_DIR"){ if (message == "CHOOSE_SAVE_DIR") {
saveDirDialog.open() saveDirDialog.open();
} }
} }
// Any operation start (detect/read/erase/debug) latches the panel on. // Any operation start (detect/read/erase/debug) latches the panel on.
function onPortsUpdated() { function onPortsUpdated() {
// Status tab always visible now // Status tab always visible now
} }
function onDetectResult(result) {
if (result && result.familyCode) {
root.waferDetected = true;
waferInfoFamily.text = result.familyCode;
waferSerial.text = result.serialNumber;
waferSensors.text = String(result.sensorCount);
waferRuntime.text = result.runtime + "s";
waferCycles.text = String(result.cycleCount);
}
// On failure, keep the last displayed wafer info and do not change waferDetected.
}
function onDetectResult(result) { function onStatusRestored() {
if (result && result.familyCode) { // Show restored wafer info if available
root.waferDetected = true var info = deviceController.lastWaferInfo;
waferInfoFamily.text = result.familyCode if (info && info.length > 0) {
waferSerial.text = result.serialNumber root.waferDetected = true;
waferSensors.text = String(result.sensorCount) waferInfoFamily.text = info[0] || "—";
waferRuntime.text = result.runtime + "s" waferSerial.text = info[1] || "";
waferCycles.text = String(result.cycleCount) waferSensors.text = String(info[2] || 0);
} waferRuntime.text = (info[3] || 0) + "s";
// On failure, keep the last displayed wafer info and do not change waferDetected. waferCycles.text = String(info[4] || 0);
} }
// Show data summary if data was previously parsed
function onStatusRestored() { if (deviceController.dataRowCount > 0) {
// Show restored wafer info if available root.dataParsed = true;
var info = deviceController.lastWaferInfo root.dataRows = deviceController.dataRowCount;
if (info && info.length > 0) { root.dataCols = deviceController.dataColCount;
root.waferDetected = true root.csvPath = ""; // CSV path not persisted as full path, just show it was parsed
waferInfoFamily.text = info[0] || "—" }
waferSerial.text = info[1] || "—" }
waferSensors.text = String(info[2] || 0) }
waferRuntime.text = (info[3] || 0) + "s"
waferCycles.text = String(info[4] || 0)
}
// Show data summary if data was previously parsed
if (deviceController.dataRowCount > 0) {
root.dataParsed = true
root.dataRows = deviceController.dataRowCount
root.dataCols = deviceController.dataColCount
root.csvPath = "" // CSV path not persisted as full path, just show it was parsed
}
}
}
Connections { Connections {
target: deviceController target: deviceController
function onReadResult(result) { function onReadResult(result) {
root.dataParsed = false root.dataParsed = false;
root.dataRows = 0 root.dataRows = 0;
root.dataCols = 0 root.dataCols = 0;
root.csvPath = "" root.csvPath = "";
if (result && result.success === true) if (result && result.success === true)
saveDirDialog.open() saveDirDialog.open();
} }
} }
@@ -400,12 +480,12 @@ ColumnLayout {
target: deviceController target: deviceController
function onParsedDataReady(result) { function onParsedDataReady(result) {
if (result && result.success) { if (result && result.success) {
root.dataParsed = true root.dataParsed = true;
root.dataRows = result.rows root.dataRows = result.rows;
root.dataCols = result.cols root.dataCols = result.cols;
root.csvPath = result.csv_path || "" root.csvPath = result.csv_path || "";
} else { } else {
root.dataParsed = false root.dataParsed = false;
} }
} }
} }
+76 -70
View File
@@ -18,12 +18,13 @@ Item {
repeat: true repeat: true
running: streamController.mode === "live" && streamController.state !== "idle" running: streamController.mode === "live" && streamController.state !== "idle"
onTriggered: root._liveSecs++ onTriggered: root._liveSecs++
onRunningChanged: if (!running) root._liveSecs = 0 onRunningChanged: if (!running)
root._liveSecs = 0
} }
function fmtTime(s) { function fmtTime(s) {
var m = Math.floor(s / 60) var m = Math.floor(s / 60);
var ss = s % 60 var ss = s % 60;
return (m < 10 ? "0" : "") + m + ":" + (ss < 10 ? "0" : "") + ss 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
@@ -32,7 +33,7 @@ Item {
Connections { Connections {
target: streamController target: streamController
function onTrendData(avgsJson) { function onTrendData(avgsJson) {
trendChart.setDataFromJson(avgsJson) trendChart.setDataFromJson(avgsJson);
} }
} }
@@ -60,7 +61,8 @@ Item {
} }
TabButton { TabButton {
text: "Review" text: "Review"
implicitWidth: 64; implicitHeight: 28 implicitWidth: 64
implicitHeight: 28
background: Rectangle { background: Rectangle {
color: parent.checked ? Theme.tabActiveBackground : "transparent" color: parent.checked ? Theme.tabActiveBackground : "transparent"
radius: Theme.radiusSm - 1 radius: Theme.radiusSm - 1
@@ -68,7 +70,7 @@ Item {
contentItem: Text { contentItem: Text {
text: parent.text text: parent.text
color: parent.checked ? Theme.headingColor : Theme.bodyColor color: parent.checked ? Theme.headingColor : Theme.bodyColor
font.pixelSize: 11 font.pixelSize: Theme.fontSm
font.weight: parent.checked ? Font.Medium : Font.Normal font.weight: parent.checked ? Font.Medium : Font.Normal
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
@@ -78,7 +80,8 @@ Item {
text: "Live" text: "Live"
enabled: deviceController.connectionStatus === "Connected" enabled: deviceController.connectionStatus === "Connected"
opacity: enabled ? 1.0 : 0.4 opacity: enabled ? 1.0 : 0.4
implicitWidth: 58; implicitHeight: 28 implicitWidth: 58
implicitHeight: 28
background: Rectangle { background: Rectangle {
color: parent.checked ? Theme.tabActiveBackground : "transparent" color: parent.checked ? Theme.tabActiveBackground : "transparent"
radius: Theme.radiusSm - 1 radius: Theme.radiusSm - 1
@@ -86,30 +89,29 @@ Item {
contentItem: Text { contentItem: Text {
text: parent.text text: parent.text
color: parent.checked ? Theme.headingColor : Theme.bodyColor color: parent.checked ? Theme.headingColor : Theme.bodyColor
font.pixelSize: 11 font.pixelSize: Theme.fontSm
font.weight: parent.checked ? Font.Medium : Font.Normal font.weight: parent.checked ? Font.Medium : Font.Normal
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }
} }
onCurrentIndexChanged: onCurrentIndexChanged: if (currentIndex === 1) {
if (currentIndex === 1){ // Guard: revert to Review if not connected
// Guard: revert to Review if not connected if (deviceController.connectionStatus !== "Connected") {
if (deviceController.connectionStatus !== "Connected") { currentIndex = 0;
currentIndex = 0 return;
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")
} }
// 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");
}
} }
// Live source info: WiFi icon + port · serial // Live source info: WiFi icon + port · serial
@@ -120,33 +122,44 @@ Item {
Label { Label {
id: liveIndicator id: liveIndicator
text: "◉" text: "◉"
color: (deviceController.connectionStatus === "Connected" && streamController.state !== "idle") color: (deviceController.connectionStatus === "Connected" && streamController.state !== "idle") ? Theme.liveColor : Theme.bodyColor
? Theme.liveColor : Theme.bodyColor font.pixelSize: Theme.fontSm
font.pixelSize: 13
SequentialAnimation on opacity { SequentialAnimation on opacity {
running: deviceController.connectionStatus === "Connected" && streamController.state !== "idle" running: deviceController.connectionStatus === "Connected" && streamController.state !== "idle"
loops: Animation.Infinite loops: Animation.Infinite
NumberAnimation { to: 0.2; duration: 600; easing.type: Easing.InOutQuad } NumberAnimation {
NumberAnimation { to: 1.0; duration: 600; easing.type: Easing.InOutQuad } to: 0.2
duration: 600
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 1.0
duration: 600
easing.type: Easing.InOutQuad
}
onRunningChanged: { onRunningChanged: {
if (!running) liveIndicator.opacity = 1.0 if (!running)
liveIndicator.opacity = 1.0;
} }
} }
} }
Label { Label {
text: "Live stream" text: "Live stream"
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 11 font.pixelSize: Theme.fontXs
} }
} }
Item { Layout.fillWidth: true } Item {
Layout.fillWidth: true
}
// SET badge + avg temp // SET badge + avg temp
Rectangle { Rectangle {
visible: streamController.state === "set" visible: streamController.state === "set"
height: 22; radius: Theme.radiusSm height: 22
radius: Theme.radiusSm
color: "transparent" color: "transparent"
border.color: Theme.liveColor border.color: Theme.liveColor
border.width: 1 border.width: 1
@@ -154,10 +167,9 @@ Item {
Label { Label {
id: setLabel id: setLabel
anchors.centerIn: parent anchors.centerIn: parent
text: "SET " + (streamController.stats.avg !== undefined text: "SET " + (streamController.stats.avg !== undefined ? streamController.stats.avg + " avg" : "")
? streamController.stats.avg + " avg" : "")
color: Theme.liveColor color: Theme.liveColor
font.pixelSize: 10 font.pixelSize: Theme.fontXs
font.weight: Font.Medium font.weight: Font.Medium
font.letterSpacing: 0.8 font.letterSpacing: 0.8
} }
@@ -168,19 +180,27 @@ Item {
spacing: 5 spacing: 5
visible: streamController.recording visible: streamController.recording
Rectangle { Rectangle {
width: 7; height: 7; radius: 4 width: 7
height: 7
radius: 4
color: Theme.recordColor color: Theme.recordColor
SequentialAnimation on opacity { SequentialAnimation on opacity {
running: streamController.recording running: streamController.recording
loops: Animation.Infinite loops: Animation.Infinite
NumberAnimation { to: 0.2; duration: 600 } NumberAnimation {
NumberAnimation { to: 1.0; duration: 600 } to: 0.2
duration: 600
}
NumberAnimation {
to: 1.0
duration: 600
}
} }
} }
Label { Label {
text: "REC" text: "REC"
color: Theme.recordColor color: Theme.recordColor
font.pixelSize: 10 font.pixelSize: Theme.fontXs
font.weight: Font.Medium font.weight: Font.Medium
font.letterSpacing: 1.2 font.letterSpacing: 1.2
} }
@@ -197,7 +217,7 @@ Item {
// Label { // Label {
// text: "LIVE " + root.fmtTime(root._liveSecs) // text: "LIVE " + root.fmtTime(root._liveSecs)
// color: Theme.liveColor // color: Theme.liveColor
// font.pixelSize: 10 // font.pixelSize: Theme.fontXs
// font.weight: Font.Medium // font.weight: Font.Medium
// font.letterSpacing: 0.8 // font.letterSpacing: 0.8
// } // }
@@ -208,11 +228,11 @@ Item {
// visible: streamController.mode === "review" || streamController.state === "idle" // visible: streamController.mode === "review" || streamController.state === "idle"
// text: streamController.state.toUpperCas`e`() // text: streamController.state.toUpperCas`e`()
// color: Theme.bodyColor // color: Theme.bodyColor
// font.pixelSize: 11 // font.pixelSize: Theme.fontXs
// font.weight: Font.Medium // font.weight: Font.Medium
// font.letterSpacing: 1.2 // font.letterSpacing: 1.2
// } // }
Button{ Button {
text: "Export PNG" text: "Export PNG"
onClicked: exportDialog.open() onClicked: exportDialog.open()
} }
@@ -248,21 +268,7 @@ Item {
Layout.fillHeight: true Layout.fillHeight: true
spacing: 16 spacing: 16
// Source panel — bordered card // Source panel — moved to side rail (P2.1)
Rectangle {
Layout.preferredWidth: 220
Layout.fillHeight: true
color: Theme.cardBackground
border.color: Theme.cardBorder
border.width: 1
radius: Theme.radiusMd
clip: true
SourcePanel {
anchors.fill: parent
anchors.margins: 10
}
}
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
@@ -280,7 +286,7 @@ Item {
id: trendPane id: trendPane
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: 160 implicitHeight: 160
visible: trendChart.hasData visible: trendChart.hasData && streamController.mode === "live"
color: Theme.cardBackground color: Theme.cardBackground
border.color: Theme.cardBorder border.color: Theme.cardBorder
border.width: 1 border.width: 1
@@ -293,28 +299,28 @@ Item {
} }
} }
Item { Layout.fillWidth: true; implicitHeight: 16 } Item {
Layout.fillWidth: true
implicitHeight: 16
}
TransportBar { TransportBar {
Layout.fillWidth: true Layout.fillWidth: true
visible: streamController.mode !== "live" || trendChart.hasData
height: visible ? implicitHeight : 0
} }
} }
// Readout panel — bordered card // Readout panel — floating wrapper box
Rectangle { Rectangle {
Layout.preferredWidth: 220 Layout.preferredWidth: 220
Layout.fillHeight: true Layout.fillHeight: true
color: Theme.cardBackground color: "transparent"
border.color: Theme.cardBorder border.color: "transparent"
border.width: 1 border.width: 0
radius: Theme.radiusMd
clip: true clip: true
ReadoutPanel { ReadoutPanel {
id: readoutPanel id: readoutPanel
anchors.fill: parent anchors.fill: parent
anchors.margins: 10 anchors.margins: 0
} }
} }
} }
@@ -17,14 +17,15 @@ Dialog {
property bool hasOverride: streamController.overriddenSensors.indexOf(sensorIndex) >= 0 property bool hasOverride: streamController.overriddenSensors.indexOf(sensorIndex) >= 0
function openFor(index) { function openFor(index) {
sensorIndex = index sensorIndex = index;
var dot = streamController.sensorDots[index] var dot = streamController.sensorDots[index];
if (!dot) return if (!dot)
sensorLabel = dot.label !== undefined ? String(dot.label) : String(index + 1) return;
currentValue = dot.value sensorLabel = dot.label !== undefined ? String(dot.label) : String(index + 1);
valueField.text = "" currentValue = dot.value;
offsetField.text = "" valueField.text = "";
open() offsetField.text = "";
open();
} }
background: Rectangle { background: Rectangle {
@@ -42,11 +43,10 @@ Dialog {
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
Label { Label {
text: "Sensor #" + (root.sensorIndex + 1) + text: "Sensor #" + (root.sensorIndex + 1) + (root.sensorLabel ? " (" + root.sensorLabel + ")" : "")
(root.sensorLabel ? " (" + root.sensorLabel + ")" : "")
color: Theme.headingColor color: Theme.headingColor
font.bold: true font.bold: true
font.pixelSize: 13 font.pixelSize: Theme.fontSm
Layout.fillWidth: true Layout.fillWidth: true
} }
Rectangle { Rectangle {
@@ -61,7 +61,7 @@ Dialog {
anchors.centerIn: parent anchors.centerIn: parent
text: "OVERRIDE" text: "OVERRIDE"
color: "white" color: "white"
font.pixelSize: 10 font.pixelSize: Theme.fontXs
font.bold: true font.bold: true
font.letterSpacing: 1 font.letterSpacing: 1
} }
@@ -71,13 +71,21 @@ Dialog {
Label { Label {
text: "Live: " + root.currentValue.toFixed(2) text: "Live: " + root.currentValue.toFixed(2)
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 12 font.pixelSize: Theme.fontSm
} }
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder } Rectangle {
Layout.fillWidth: true
height: 1
color: Theme.cardBorder
}
// ── replace field ───────────────────────────────────────── // ── replace field ─────────────────────────────────────────
Label { text: "Replace with value"; color: Theme.bodyColor; font.pixelSize: 11 } Label {
text: "Replace with value"
color: Theme.bodyColor
font.pixelSize: Theme.fontXs
}
TextField { TextField {
id: valueField id: valueField
Layout.fillWidth: true Layout.fillWidth: true
@@ -92,11 +100,21 @@ Dialog {
color: Theme.fieldBackground color: Theme.fieldBackground
border.width: valueField.activeFocus ? Theme.borderStrong : Theme.borderThin border.width: valueField.activeFocus ? Theme.borderStrong : Theme.borderThin
border.color: valueField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder border.color: valueField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
Behavior on border.color {
ColorAnimation { duration: Theme.durationFast }
}
} }
} }
// ── offset field ────────────────────────────────────────── // ── offset field ──────────────────────────────────────────
Label { text: "Or add offset (± delta)"; color: Theme.bodyColor; font.pixelSize: 11 } Label {
text: "Or add offset (± delta)"
color: Theme.bodyColor
font.pixelSize: Theme.fontXs
}
TextField { TextField {
id: offsetField id: offsetField
Layout.fillWidth: true Layout.fillWidth: true
@@ -111,10 +129,20 @@ Dialog {
color: Theme.fieldBackground color: Theme.fieldBackground
border.width: offsetField.activeFocus ? Theme.borderStrong : Theme.borderThin border.width: offsetField.activeFocus ? Theme.borderStrong : Theme.borderThin
border.color: offsetField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder border.color: offsetField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
Behavior on border.color {
ColorAnimation { duration: Theme.durationFast }
}
} }
} }
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder } Rectangle {
Layout.fillWidth: true
height: 1
color: Theme.cardBorder
}
// ── buttons ─────────────────────────────────────────────── // ── buttons ───────────────────────────────────────────────
RowLayout { RowLayout {
@@ -140,20 +168,20 @@ Dialog {
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
font.bold: true font.bold: true
font.pixelSize: 13 font.pixelSize: Theme.fontSm
} }
onClicked: { onClicked: {
if (valueField.text !== "") { if (valueField.text !== "") {
var v = parseFloat(valueField.text) var v = parseFloat(valueField.text);
if (!isNaN(v)) if (!isNaN(v))
streamController.replaceSensor(root.sensorIndex, v) streamController.replaceSensor(root.sensorIndex, v);
} }
if (offsetField.text !== "") { if (offsetField.text !== "") {
var d = parseFloat(offsetField.text) var d = parseFloat(offsetField.text);
if (!isNaN(d)) if (!isNaN(d))
streamController.offsetSensor(root.sensorIndex, d) streamController.offsetSensor(root.sensorIndex, d);
} }
root.close() root.close();
} }
} }
Button { Button {
@@ -175,11 +203,11 @@ Dialog {
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
font.bold: true font.bold: true
font.pixelSize: 13 font.pixelSize: Theme.fontSm
} }
onClicked: { onClicked: {
streamController.clearSensorEdit(root.sensorIndex) streamController.clearSensorEdit(root.sensorIndex);
root.close() root.close();
} }
} }
Button { Button {
@@ -198,7 +226,7 @@ Dialog {
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
font.bold: true font.bold: true
font.pixelSize: 13 font.pixelSize: Theme.fontSm
} }
onClicked: root.close() onClicked: root.close()
} }
+14 -16
View File
@@ -3,7 +3,6 @@ import QtQuick.Controls
import ISC import ISC
import ISC.Wafer import ISC.Wafer
Item { Item {
id: root id: root
property real blend: 0.0 property real blend: 0.0
@@ -22,28 +21,27 @@ Item {
blend: root.blend blend: root.blend
showLabels: root.showLabels showLabels: root.showLabels
// Bind to Theme so colors update on dark/light mode switch // Bind to Theme so colors update on dark/light mode switch
ringColor: Theme.waferRingColor ringColor: Theme.waferRingColor
axisColor: Theme.waferAxisColor axisColor: Theme.waferAxisColor
lowColor: Theme.sensorLow lowColor: Theme.sensorLow
inRangeColor: Theme.sensorInRange inRangeColor: Theme.sensorInRange
highColor: Theme.sensorHigh highColor: Theme.sensorHigh
textColor: Theme.headingColor textColor: Theme.headingColor
TapHandler { TapHandler {
onTapped: (ev) => { onTapped: ev => {
var idx = map.which_marker(ev.position.x, ev.position.y) var idx = map.which_marker(ev.position.x, ev.position.y);
if (idx >= 0) replaceDialog.openFor(idx) if (idx >= 0)
replaceDialog.openFor(idx);
} }
} }
} }
ReplaceSensorDialog { id: replaceDialog} ReplaceSensorDialog {
id: replaceDialog
function exportImage(filePath) {
return map.export_image(filePath)
} }
function exportImage(filePath) {
return map.export_image(filePath);
}
} }
@@ -37,10 +37,12 @@ class SessionController(QObject):
loadedFileChanged = Signal() loadedFileChanged = Signal()
clusterAveragingEnabledChanged = Signal() clusterAveragingEnabledChanged = Signal()
settingsChanged = Signal() # emitted when session state changes that should be persisted settingsChanged = Signal() # emitted when session state changes that should be persisted
liveStatsChanged = Signal() # emitted when receivedCount or errorCount change
# trend: per-frame avg for live graph # trend: per-frame avg for live graph
trendData = Signal(str) # JSON array of floats trendData = Signal(str) # JSON array of floats
# private: marshal a worker-thread frame onto the main thread # private: marshal a worker-thread frame onto the main thread
_liveFrame = Signal(object) # Frame _liveFrame = Signal(object) # Frame
_liveError = Signal() # worker-thread error ping
def __init__(self, parent: QObject | None = None, def __init__(self, parent: QObject | None = None,
settings: dict | None = None) -> None: settings: dict | None = None) -> None:
@@ -72,6 +74,7 @@ class SessionController(QObject):
self._dirty = False self._dirty = False
self._liveFrame.connect(self._on_live_frame, Qt.ConnectionType.QueuedConnection) self._liveFrame.connect(self._on_live_frame, Qt.ConnectionType.QueuedConnection)
self._liveError.connect(self._on_live_error, Qt.ConnectionType.QueuedConnection)
self._sensor_editor = SensorEditor() self._sensor_editor = SensorEditor()
self._last_raw_frame: Frame | None = None self._last_raw_frame: Frame | None = None
@@ -79,6 +82,8 @@ class SessionController(QObject):
self._cluster_averaging_enabled = False self._cluster_averaging_enabled = False
self._active_clusters: list[list[int]] = [] self._active_clusters: list[list[int]] = []
self._stats_tracker = ReplayStatsTracker() self._stats_tracker = ReplayStatsTracker()
self._received_count: int = 0
self._error_count: int = 0
# Restore persisted state if available # Restore persisted state if available
self._load_settings(settings or {}) self._load_settings(settings or {})
@@ -105,6 +110,9 @@ class SessionController(QObject):
@Property(int, notify=frameUpdated) @Property(int, notify=frameUpdated)
def frameTotal(self) -> int: return self._player.total def frameTotal(self) -> int: return self._player.total
@Property(bool, notify=stateChanged)
def playing(self) -> bool: return self._play_timer.isActive()
@Property(str, notify=stateChanged) @Property(str, notify=stateChanged)
def state(self) -> str: def state(self) -> str:
if self._last: if self._last:
@@ -210,6 +218,14 @@ class SessionController(QObject):
self.clusterAveragingEnabledChanged.emit() self.clusterAveragingEnabledChanged.emit()
self._reprocess_current() self._reprocess_current()
@Property(int, notify=liveStatsChanged)
def receivedCount(self) -> int:
return self._received_count
@Property(int, notify=liveStatsChanged)
def errorCount(self) -> int:
return self._error_count
# ---- mode + thresholds ---- # ---- mode + thresholds ----
@Slot(str) @Slot(str)
@slot_error_boundary @slot_error_boundary
@@ -292,11 +308,13 @@ class SessionController(QObject):
@slot_error_boundary @slot_error_boundary
def play(self) -> None: def play(self) -> None:
self._play_timer.start(self._next_interval_ms()) self._play_timer.start(self._next_interval_ms())
self.stateChanged.emit()
@Slot() @Slot()
@slot_error_boundary @slot_error_boundary
def pause(self) -> None: def pause(self) -> None:
self._play_timer.stop() self._play_timer.stop()
self.stateChanged.emit()
@Slot() @Slot()
@slot_error_boundary @slot_error_boundary
@@ -304,6 +322,7 @@ class SessionController(QObject):
self._play_timer.stop() self._play_timer.stop()
self._player.seek(0) self._player.seek(0)
self._emit_current() self._emit_current()
self.stateChanged.emit()
@Slot(int) @Slot(int)
@slot_error_boundary @slot_error_boundary
@@ -311,6 +330,7 @@ class SessionController(QObject):
self._play_timer.stop() self._play_timer.stop()
self._player.step(delta) self._player.step(delta)
self._emit_current() self._emit_current()
self.stateChanged.emit()
@Slot(int) @Slot(int)
@slot_error_boundary @slot_error_boundary
@@ -335,6 +355,7 @@ class SessionController(QObject):
def _advance(self) -> None: def _advance(self) -> None:
if self._player.at_end: if self._player.at_end:
self._play_timer.stop() self._play_timer.stop()
self.stateChanged.emit()
return return
self._player.step(1) self._player.step(1)
self._emit_current() self._emit_current()
@@ -419,6 +440,9 @@ class SessionController(QObject):
self._stats_tracker.reset() self._stats_tracker.reset()
self._last = None self._last = None
self._last_raw_frame = None self._last_raw_frame = None
self._received_count = 0
self._error_count = 0
self.liveStatsChanged.emit()
transport = SerialPort.open_port(port, timeout=1) transport = SerialPort.open_port(port, timeout=1)
# Send 'D2' command padded to 512 bytes to start the stream # Send 'D2' command padded to 512 bytes to start the stream
@@ -427,6 +451,7 @@ class SessionController(QObject):
def on_error(exc: Exception): def on_error(exc: Exception):
log.error("Live stream error: %s", exc) log.error("Live stream error: %s", exc)
self._liveError.emit()
self._reader = StreamReader( self._reader = StreamReader(
transport, parse_binary_frame, transport, parse_binary_frame,
@@ -444,6 +469,9 @@ class SessionController(QObject):
def stopStream(self) -> None: def stopStream(self) -> None:
self._repaint_timer.stop() self._repaint_timer.stop()
self._trend_timer.stop() self._trend_timer.stop()
self._received_count = 0
self._error_count = 0
self.liveStatsChanged.emit()
if self._reader: if self._reader:
transport = self._reader._transport transport = self._reader._transport
self._reader.stop() self._reader.stop()
@@ -465,6 +493,8 @@ class SessionController(QObject):
@slot_error_boundary @slot_error_boundary
def _on_live_frame(self, frame: Frame) -> None: def _on_live_frame(self, frame: Frame) -> None:
import json import json
self._received_count += 1
self.liveStatsChanged.emit()
self._stats_tracker.track(frame.seq, frame.values) self._stats_tracker.track(frame.seq, frame.values)
# Main thread (queued). Record raw, process edited; mark dirty for repaint. # Main thread (queued). Record raw, process edited; mark dirty for repaint.
self._last_raw_frame = frame self._last_raw_frame = frame
@@ -498,6 +528,11 @@ class SessionController(QObject):
self.frameUpdated.emit() self.frameUpdated.emit()
self.stateChanged.emit() self.stateChanged.emit()
def _on_live_error(self) -> None:
"""Main-thread callback for worker-thread stream errors."""
self._error_count += 1
self.liveStatsChanged.emit()
# ---- recording ---- # ---- recording ----
@Slot(str, str) @Slot(str, str)
@slot_error_boundary @slot_error_boundary