feat(tabs): update all tab views, dialogs, and session controller
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
@@ -148,13 +151,16 @@ Item {
|
|||||||
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,7 +417,7 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -317,7 +398,7 @@ ColumnLayout {
|
|||||||
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
|
||||||
@@ -328,7 +409,7 @@ ColumnLayout {
|
|||||||
Connections {
|
Connections {
|
||||||
target: deviceController
|
target: deviceController
|
||||||
function onActivityLogUpdated(newLog) {
|
function onActivityLogUpdated(newLog) {
|
||||||
activityLog.text = newLog
|
activityLog.text = newLog;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -340,7 +421,7 @@ ColumnLayout {
|
|||||||
|
|
||||||
function onLogMessage(message) {
|
function onLogMessage(message) {
|
||||||
if (message == "CHOOSE_SAVE_DIR") {
|
if (message == "CHOOSE_SAVE_DIR") {
|
||||||
saveDirDialog.open()
|
saveDirDialog.open();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,36 +430,35 @@ ColumnLayout {
|
|||||||
// Status tab always visible now
|
// Status tab always visible now
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function onDetectResult(result) {
|
function onDetectResult(result) {
|
||||||
if (result && result.familyCode) {
|
if (result && result.familyCode) {
|
||||||
root.waferDetected = true
|
root.waferDetected = true;
|
||||||
waferInfoFamily.text = result.familyCode
|
waferInfoFamily.text = result.familyCode;
|
||||||
waferSerial.text = result.serialNumber
|
waferSerial.text = result.serialNumber;
|
||||||
waferSensors.text = String(result.sensorCount)
|
waferSensors.text = String(result.sensorCount);
|
||||||
waferRuntime.text = result.runtime + "s"
|
waferRuntime.text = result.runtime + "s";
|
||||||
waferCycles.text = String(result.cycleCount)
|
waferCycles.text = String(result.cycleCount);
|
||||||
}
|
}
|
||||||
// On failure, keep the last displayed wafer info and do not change waferDetected.
|
// On failure, keep the last displayed wafer info and do not change waferDetected.
|
||||||
}
|
}
|
||||||
|
|
||||||
function onStatusRestored() {
|
function onStatusRestored() {
|
||||||
// Show restored wafer info if available
|
// Show restored wafer info if available
|
||||||
var info = deviceController.lastWaferInfo
|
var info = deviceController.lastWaferInfo;
|
||||||
if (info && info.length > 0) {
|
if (info && info.length > 0) {
|
||||||
root.waferDetected = true
|
root.waferDetected = true;
|
||||||
waferInfoFamily.text = info[0] || "—"
|
waferInfoFamily.text = info[0] || "—";
|
||||||
waferSerial.text = info[1] || "—"
|
waferSerial.text = info[1] || "—";
|
||||||
waferSensors.text = String(info[2] || 0)
|
waferSensors.text = String(info[2] || 0);
|
||||||
waferRuntime.text = (info[3] || 0) + "s"
|
waferRuntime.text = (info[3] || 0) + "s";
|
||||||
waferCycles.text = String(info[4] || 0)
|
waferCycles.text = String(info[4] || 0);
|
||||||
}
|
}
|
||||||
// Show data summary if data was previously parsed
|
// Show data summary if data was previously parsed
|
||||||
if (deviceController.dataRowCount > 0) {
|
if (deviceController.dataRowCount > 0) {
|
||||||
root.dataParsed = true
|
root.dataParsed = true;
|
||||||
root.dataRows = deviceController.dataRowCount
|
root.dataRows = deviceController.dataRowCount;
|
||||||
root.dataCols = deviceController.dataColCount
|
root.dataCols = deviceController.dataColCount;
|
||||||
root.csvPath = "" // CSV path not persisted as full path, just show it was parsed
|
root.csvPath = ""; // CSV path not persisted as full path, just show it was parsed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -386,13 +466,13 @@ ColumnLayout {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,29 +89,28 @@ 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
|
// Entering Live mode
|
||||||
streamController.setMode("live")
|
streamController.setMode("live");
|
||||||
var fc = ""
|
var fc = "";
|
||||||
if (deviceController.lastWaferInfo && deviceController.lastWaferInfo.length > 0) {
|
if (deviceController.lastWaferInfo && deviceController.lastWaferInfo.length > 0) {
|
||||||
fc = deviceController.lastWaferInfo[0]
|
fc = deviceController.lastWaferInfo[0];
|
||||||
}
|
}
|
||||||
streamController.startStream(deviceController.selectedPort, fc)
|
streamController.startStream(deviceController.selectedPort, fc);
|
||||||
} else {
|
} else {
|
||||||
// Entering Review Mode (automatically calls stopStream in backend)
|
// Entering Review Mode (automatically calls stopStream in backend)
|
||||||
streamController.setMode("review")
|
streamController.setMode("review");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,7 +228,7 @@ 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
|
||||||
// }
|
// }
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -29,21 +28,20 @@ Item {
|
|||||||
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) {
|
function exportImage(filePath) {
|
||||||
return map.export_image(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
|
||||||
|
|||||||
Reference in New Issue
Block a user