Restructure into src/ layout under pygui package
Move all application source under src/pygui/ and rewire imports, build config, and QML module path to match. - Relocate backend/, serialcomm/, and the ISC QML module into src/pygui/; convert main.py into pygui/__main__.py with a main() entry point (run via `python -m pygui` or the new `isc` script) - Rewrite absolute imports: backend.* -> pygui.backend.*, serialcomm.* -> pygui.serialcomm.* (source + tests) - Move app icons (isc.ico/icns) into packaging/ - Update README and ISC.qmlproject to the new paths
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import ISC
|
||||
|
||||
// ===== Data Tab =====
|
||||
// Displays parsed temperature data in a scrollable table.
|
||||
// Column 0 = Row index, remaining columns = Sensor1, Sensor2, ...
|
||||
Item {
|
||||
id: root
|
||||
anchors.fill: parent
|
||||
|
||||
readonly property int rowCount: deviceController.dataRowCount
|
||||
|
||||
// Re-layout columns whenever the model is reset
|
||||
Connections {
|
||||
target: deviceController
|
||||
function onParsedDataReady(result) {
|
||||
if (result && result.success) {
|
||||
dataTable.forceLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Empty State =====
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 16
|
||||
visible: root.rowCount === 0
|
||||
|
||||
Label {
|
||||
text: "No Data"
|
||||
font.pixelSize: 24
|
||||
font.bold: true
|
||||
color: Theme.headingColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Label {
|
||||
text: "Read and parse wafer data to see temperature readings here."
|
||||
font.pixelSize: 14
|
||||
color: Theme.bodyColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Data Table =====
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.panelPadding
|
||||
spacing: Theme.rightPaneGap
|
||||
visible: root.rowCount > 0
|
||||
|
||||
// --- Header row ---
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.rightPaneGap
|
||||
|
||||
Label {
|
||||
text: "Temperature Data"
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
color: Theme.headingColor
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Label {
|
||||
text: deviceController.dataRowCount + " rows × " +
|
||||
deviceController.dataColCount + " sensors"
|
||||
font.pixelSize: 13
|
||||
color: Theme.bodyColor
|
||||
}
|
||||
}
|
||||
|
||||
// --- Table container ---
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: Theme.panelBackground
|
||||
border.color: Theme.cardBorder
|
||||
border.width: Theme.borderThin
|
||||
radius: Theme.radiusSm
|
||||
clip: true
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
spacing: 0
|
||||
|
||||
// Column header strip
|
||||
HorizontalHeaderView {
|
||||
id: headerView
|
||||
Layout.fillWidth: true
|
||||
syncView: dataTable
|
||||
clip: true
|
||||
|
||||
delegate: Rectangle {
|
||||
implicitHeight: 28
|
||||
color: Theme.cardBackground
|
||||
border.color: Theme.cardBorder
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
// 'display' is the Qt.DisplayRole value from headerData()
|
||||
text: display !== undefined ? display : ""
|
||||
color: Theme.headingColor
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Data rows
|
||||
TableView {
|
||||
id: dataTable
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
model: deviceController.dataModel
|
||||
clip: true
|
||||
reuseItems: true
|
||||
|
||||
columnWidthProvider: function(col) {
|
||||
if (col === 0) return 52 // row index column
|
||||
const sensorCols = Math.max(1, dataTable.columns - 1)
|
||||
const available = dataTable.width - 52
|
||||
return Math.max(52, Math.min(90, available / sensorCols))
|
||||
}
|
||||
|
||||
rowHeightProvider: function() { return 24 }
|
||||
|
||||
delegate: Rectangle {
|
||||
color: row % 2 === 0 ? Theme.panelBackground : Theme.cardBackground
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: display !== undefined ? display : ""
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.horizontal: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,603 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Qt.labs.qmlmodels
|
||||
import ISC
|
||||
|
||||
Dialog {
|
||||
id: root
|
||||
title: "Select File"
|
||||
width: 980
|
||||
height: 640
|
||||
modal: true
|
||||
padding: 16
|
||||
topPadding: 0
|
||||
parent: Overlay.overlay
|
||||
x: Math.round(((parent ? parent.width : width) - width) / 2)
|
||||
y: Math.round(((parent ? parent.height : height) - height) / 2)
|
||||
|
||||
signal fileChosen(string path)
|
||||
|
||||
// ===== Dialog State =====
|
||||
property string selectedPath: ""
|
||||
property var tableModel: []
|
||||
property int selectedRow: -1
|
||||
property int metadataEditRow: -1
|
||||
readonly property string masterTypeFieldText: {
|
||||
if (metadataEditRow < 0) return "";
|
||||
const rec = tableModel[metadataEditRow];
|
||||
if (!rec) return "";
|
||||
return rec.masterType ?? "";
|
||||
}
|
||||
|
||||
function openMetadataEditor(row) {
|
||||
metadataEditRow = row;
|
||||
const rec = tableModel[row];
|
||||
if (!rec) {
|
||||
metadataEditDialog.close();
|
||||
return;
|
||||
}
|
||||
waferField.text = rec.wafer ?? "";
|
||||
dateField.text = rec.date ?? "";
|
||||
chamberField.text = rec.chamber ?? "";
|
||||
if (!chamberField.text && settingsModel.chamberId)
|
||||
chamberField.text = settingsModel.chamberId;
|
||||
notesField.text = rec.notes ?? "";
|
||||
filePathLabel.text = rec.fileName ?? "";
|
||||
metadataEditDialog.open();
|
||||
}
|
||||
|
||||
function applyMetadataFromEditor() {
|
||||
if (metadataEditRow < 0)
|
||||
return;
|
||||
const rec = tableModel[metadataEditRow];
|
||||
if (!rec) {
|
||||
metadataEditDialog.close();
|
||||
return;
|
||||
}
|
||||
file_browser.saveMetadata(rec.fileName ?? "", waferField.text, dateField.text, chamberField.text, notesField.text, false, "");
|
||||
metadataEditDialog.close();
|
||||
metadataEditRow = -1;
|
||||
}
|
||||
|
||||
readonly property var headerTitles: ["Master", "Wafer", "Date", "Chamber", "Notes", "File", "Edit"]
|
||||
// Build a path→family reverse map from settingsModel.masters.
|
||||
// settingsModel is a QML context property; accessing it here makes the
|
||||
// binding reactive — any mastersChanged signal re-evaluates normalizedRows.
|
||||
function masterFamilyForPath(filePath) {
|
||||
const m = settingsModel.masters;
|
||||
for (var fam in m) {
|
||||
if (m[fam] && m[fam] === filePath)
|
||||
return fam;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
readonly property var normalizedRows: {
|
||||
// Touch settingsModel.masters so QML tracks this dependency.
|
||||
const _ = settingsModel.masters;
|
||||
return (tableModel || []).map(function(row) {
|
||||
row = row || {};
|
||||
// settingsModel is the authoritative source; fall back to sidecar masterType.
|
||||
const masterType = root.masterFamilyForPath(row.fileName ?? "") || row.masterType || "";
|
||||
return {
|
||||
masterType: masterType,
|
||||
wafer: row.wafer ?? "",
|
||||
date: row.date ?? "",
|
||||
chamber: row.chamber ?? "",
|
||||
edit: "",
|
||||
notes: row.notes ?? "",
|
||||
fileName: row.fileName ?? "",
|
||||
highlight: row.highlight ?? false
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Reusable Controls =====
|
||||
component DialogActionButton: Button {
|
||||
id: dialogButton
|
||||
hoverEnabled: true
|
||||
implicitHeight: 36
|
||||
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusSm
|
||||
color: dialogButton.down ? Theme.buttonNeutralPressed : dialogButton.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground
|
||||
border.width: Theme.borderThin
|
||||
border.color: Theme.fieldBorder
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: dialogButton.text
|
||||
color: Theme.buttonNeutralText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
font.bold: true
|
||||
font.pixelSize: 13
|
||||
}
|
||||
}
|
||||
|
||||
component DialogTextInput: TextField {
|
||||
id: dialogTextInput
|
||||
color: Theme.fieldText
|
||||
placeholderTextColor: Theme.fieldPlaceholder
|
||||
selectedTextColor: Theme.fieldBackground
|
||||
selectionColor: Theme.fieldText
|
||||
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusXs
|
||||
color: Theme.fieldBackground
|
||||
border.width: dialogTextInput.activeFocus ? Theme.borderStrong : Theme.borderThin
|
||||
border.color: dialogTextInput.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Dialog Chrome =====
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusLg
|
||||
color: Theme.panelBackground
|
||||
border.color: Theme.outerFrameBorder
|
||||
border.width: Theme.borderStrong
|
||||
}
|
||||
|
||||
// Custom header — replaces the native dark title bar
|
||||
header: Rectangle {
|
||||
width: root.width
|
||||
height: 48
|
||||
radius: Theme.radiusLg
|
||||
color: Theme.subtleSectionBackground
|
||||
border.color: Theme.innerFrameBorder
|
||||
border.width: Theme.borderThin
|
||||
|
||||
// Square off the bottom corners
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: parent.radius
|
||||
color: parent.color
|
||||
}
|
||||
|
||||
Label {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 16
|
||||
text: root.title
|
||||
font.pixelSize: 15
|
||||
font.bold: true
|
||||
color: Theme.headingColor
|
||||
}
|
||||
|
||||
// Close button
|
||||
DialogActionButton {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 12
|
||||
text: "✕"
|
||||
implicitWidth: 32
|
||||
implicitHeight: 32
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Content =====
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
|
||||
// ===== Browser Toolbar =====
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 12
|
||||
spacing: 8
|
||||
|
||||
DialogActionButton {
|
||||
text: "Choose Folder"
|
||||
Layout.preferredWidth: 140
|
||||
onClicked: file_browser.chooseDirectory()
|
||||
}
|
||||
|
||||
DialogActionButton {
|
||||
text: "Refresh"
|
||||
Layout.preferredWidth: 100
|
||||
onClicked: file_browser.refreshFiles()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 36
|
||||
Layout.minimumWidth: 80
|
||||
clip: true
|
||||
radius: Theme.radiusXs
|
||||
color: Theme.fieldBackground
|
||||
border.color: Theme.fieldBorder
|
||||
border.width: Theme.borderThin
|
||||
|
||||
Text {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: file_browser.currentDirectory
|
||||
color: Theme.fieldText
|
||||
elide: Text.ElideMiddle
|
||||
font.pixelSize: 13
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== File Table =====
|
||||
HorizontalHeaderView {
|
||||
id: headerView
|
||||
syncView: tableView
|
||||
Layout.fillWidth: true
|
||||
|
||||
delegate: Rectangle {
|
||||
implicitHeight: 34
|
||||
color: Theme.subtleSectionBackground
|
||||
border.color: Theme.innerFrameBorder
|
||||
border.width: Theme.borderThin
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: root.headerTitles[index] ?? ""
|
||||
font.bold: true
|
||||
font.pixelSize: 12
|
||||
color: Theme.headingColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TableView {
|
||||
id: tableView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
columnSpacing: 0
|
||||
rowSpacing: 0
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
|
||||
columnWidthProvider: function (col) {
|
||||
// [master, wafer, date, chamber, notes, file (variable), edit]
|
||||
const fixed = {
|
||||
0: 48,
|
||||
1: 90,
|
||||
2: 170,
|
||||
3: 120,
|
||||
4: 180,
|
||||
6: 56
|
||||
};
|
||||
if (col in fixed)
|
||||
return fixed[col];
|
||||
// col 5 (File) takes remaining width
|
||||
const w = Math.max(400, tableView.width);
|
||||
const used = 48 + 90 + 170 + 120 + 180 + 56;
|
||||
return Math.max(120, w - used);
|
||||
}
|
||||
|
||||
model: TableModel {
|
||||
TableModelColumn {
|
||||
display: "masterType"
|
||||
}
|
||||
TableModelColumn {
|
||||
display: "wafer"
|
||||
}
|
||||
TableModelColumn {
|
||||
display: "date"
|
||||
}
|
||||
TableModelColumn {
|
||||
display: "chamber"
|
||||
}
|
||||
TableModelColumn {
|
||||
display: "notes"
|
||||
}
|
||||
TableModelColumn {
|
||||
display: "fileName"
|
||||
}
|
||||
TableModelColumn {
|
||||
display: "edit"
|
||||
}
|
||||
rows: root.normalizedRows
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
required property bool current
|
||||
required property int row
|
||||
required property int column
|
||||
|
||||
implicitHeight: 40
|
||||
border.color: Theme.softBorder
|
||||
border.width: Theme.borderThin
|
||||
color: {
|
||||
if (root.selectedRow === row)
|
||||
return Theme.sideActiveBackground;
|
||||
if (root.tableModel[row]?.highlight)
|
||||
return Theme.subtleSectionBackground;
|
||||
return row % 2 === 0 ? Theme.cardBackground : Theme.tone100;
|
||||
}
|
||||
|
||||
// Master type indicator (col 0)
|
||||
// Reads normalizedRows (which merges settingsModel.masters + sidecar).
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
active: column === 0
|
||||
sourceComponent: Text {
|
||||
anchors.fill: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: {
|
||||
const mt = root.normalizedRows[row]?.masterType ?? "";
|
||||
return mt || "—";
|
||||
}
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
color: {
|
||||
const mt = root.normalizedRows[row]?.masterType ?? "";
|
||||
return mt ? Theme.primaryAccent : Theme.fieldPlaceholder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Edit button (col 6)
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
active: column === 6
|
||||
sourceComponent: Button {
|
||||
hoverEnabled: true
|
||||
text: "Edit…"
|
||||
font.pixelSize: 12
|
||||
onClicked: root.openMetadataEditor(row)
|
||||
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusXs
|
||||
color: parent.down ? Theme.buttonNeutralPressed : parent.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground
|
||||
border.width: Theme.borderThin
|
||||
border.color: Theme.fieldBorder
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: parent.text
|
||||
color: Theme.buttonNeutralText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Text columns: Wafer (1), Date (2), File (5)
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
active: column === 1 || column === 2 || column === 5
|
||||
sourceComponent: Text {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: {
|
||||
const record = root.tableModel[row] || {};
|
||||
if (column === 1)
|
||||
return record.wafer ?? "";
|
||||
if (column === 2)
|
||||
return record.date ?? "";
|
||||
if (column === 5) {
|
||||
const p = record.fileName ?? "";
|
||||
return p.split("/").pop();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
elide: Text.ElideRight
|
||||
font.pixelSize: 13
|
||||
color: Theme.bodyColor
|
||||
}
|
||||
}
|
||||
|
||||
// Text columns: Chamber (3), Notes (4)
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
active: column === 3 || column === 4
|
||||
sourceComponent: Text {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: {
|
||||
const record = root.tableModel[row] || {};
|
||||
if (column === 3)
|
||||
return record.chamber ?? "";
|
||||
return record.notes ?? "";
|
||||
}
|
||||
elide: Text.ElideRight
|
||||
font.pixelSize: 13
|
||||
color: Theme.bodyColor
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: column !== 6
|
||||
onClicked: {
|
||||
root.selectedRow = row;
|
||||
root.selectedPath = root.tableModel[row]?.fileName ?? "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Bottom Bar =====
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
DialogActionButton {
|
||||
text: "OK"
|
||||
Layout.preferredWidth: 90
|
||||
onClicked: {
|
||||
root.fileChosen(root.selectedPath);
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 32
|
||||
Layout.minimumWidth: 120
|
||||
clip: true
|
||||
radius: Theme.radiusXs
|
||||
color: Theme.fieldBackground
|
||||
border.color: Theme.fieldBorder
|
||||
border.width: Theme.borderThin
|
||||
|
||||
Text {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: root.selectedPath
|
||||
elide: Text.ElideLeft
|
||||
font.pixelSize: 13
|
||||
color: root.selectedPath ? Theme.fieldText : Theme.fieldPlaceholder
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Metadata Editor Dialog =====
|
||||
Dialog {
|
||||
id: metadataEditDialog
|
||||
parent: Overlay.overlay
|
||||
modal: true
|
||||
title: qsTr("Edit Metadata")
|
||||
width: 520
|
||||
height: 520
|
||||
padding: 16
|
||||
topPadding: 0
|
||||
x: Math.round(((parent ? parent.width : width) - width) / 2)
|
||||
y: Math.round(((parent ? parent.height : height) - height) / 2)
|
||||
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusLg
|
||||
color: Theme.panelBackground
|
||||
border.color: Theme.outerFrameBorder
|
||||
border.width: Theme.borderStrong
|
||||
}
|
||||
|
||||
header: Rectangle {
|
||||
width: metadataEditDialog.width
|
||||
height: 48
|
||||
radius: Theme.radiusLg
|
||||
color: Theme.subtleSectionBackground
|
||||
border.color: Theme.innerFrameBorder
|
||||
border.width: Theme.borderThin
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: parent.radius
|
||||
color: parent.color
|
||||
}
|
||||
|
||||
Label {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 16
|
||||
text: metadataEditDialog.title
|
||||
font.pixelSize: 15
|
||||
font.bold: true
|
||||
color: Theme.headingColor
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.settingsGridSpacing
|
||||
|
||||
Label {
|
||||
text: qsTr("File")
|
||||
font.bold: true
|
||||
color: Theme.headingColor
|
||||
Layout.topMargin: 8
|
||||
}
|
||||
|
||||
Text {
|
||||
id: filePathLabel
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumHeight: 56
|
||||
Layout.bottomMargin: 16
|
||||
wrapMode: Text.WrapAnywhere
|
||||
maximumLineCount: 3
|
||||
elide: Text.ElideRight
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("Wafer")
|
||||
color: Theme.headingColor
|
||||
}
|
||||
DialogTextInput {
|
||||
id: waferField
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 36
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("Date")
|
||||
color: Theme.headingColor
|
||||
}
|
||||
DialogTextInput {
|
||||
id: dateField
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 36
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("Chamber")
|
||||
color: Theme.headingColor
|
||||
}
|
||||
DialogTextInput {
|
||||
id: chamberField
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 36
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("Notes")
|
||||
color: Theme.headingColor
|
||||
}
|
||||
DialogTextInput {
|
||||
id: notesField
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 36
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.bottomMargin: 16
|
||||
spacing: 8
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
DialogActionButton {
|
||||
text: qsTr("Cancel")
|
||||
Layout.preferredWidth: 90
|
||||
onClicked: {
|
||||
root.metadataEditRow = -1;
|
||||
metadataEditDialog.close();
|
||||
}
|
||||
}
|
||||
|
||||
DialogActionButton {
|
||||
text: qsTr("Save")
|
||||
Layout.preferredWidth: 90
|
||||
onClicked: root.applyMetadataFromEditor()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Dialogs
|
||||
import ISC
|
||||
|
||||
Item {
|
||||
id: root
|
||||
anchors.fill: parent
|
||||
|
||||
// ===== CSV metadata dialog (must not live inside RowLayout: implicit size breaks the tab) =====
|
||||
SelectFileDialog {
|
||||
id: selectFileDialog
|
||||
tableModel: file_browser.files
|
||||
onFileChosen: function (path) {
|
||||
root.handleCsvSelected(path);
|
||||
}
|
||||
}
|
||||
|
||||
function handleEditCsvMetadata() {
|
||||
file_browser.refreshFiles();
|
||||
selectFileDialog.open();
|
||||
}
|
||||
|
||||
function handleCsvSelected(filePath) {
|
||||
// Selection confirmed — metadata editing is handled inline inside SelectFileDialog.
|
||||
}
|
||||
|
||||
// ===== Settings Data Helpers =====
|
||||
readonly property var masterFamilies: ["A", "B", "C", "D", "E", "F", "P", "X", "Z"]
|
||||
|
||||
function masterPath(family) {
|
||||
const table = settingsModel.masters;
|
||||
return table && table[family] ? table[family] : "";
|
||||
}
|
||||
|
||||
function statusColor() {
|
||||
if (!settingsModel.isValid) {
|
||||
return Theme.statusWarningColor;
|
||||
}
|
||||
return settingsModel.saveStatus.indexOf("error:") === 0 ? Theme.statusErrorColor : Theme.statusSuccessColor;
|
||||
}
|
||||
|
||||
// ===== Reusable Settings Components =====
|
||||
component SettingsGroupBox: GroupBox {
|
||||
id: settingsGroup
|
||||
|
||||
background: Rectangle {
|
||||
y: settingsGroup.topPadding - settingsGroup.bottomPadding
|
||||
width: settingsGroup.width
|
||||
height: settingsGroup.height - settingsGroup.topPadding + settingsGroup.bottomPadding
|
||||
radius: Theme.radiusMd
|
||||
color: Theme.panelBackground
|
||||
border.color: Theme.innerFrameBorder
|
||||
border.width: Theme.borderThin
|
||||
}
|
||||
|
||||
label: Label {
|
||||
text: settingsGroup.title
|
||||
color: Theme.panelTitleText
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
|
||||
component SettingsTextInput: TextField {
|
||||
id: textInput
|
||||
color: Theme.fieldText
|
||||
placeholderTextColor: Theme.fieldPlaceholder
|
||||
selectedTextColor: Theme.fieldBackground
|
||||
selectionColor: Theme.fieldText
|
||||
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusXs
|
||||
color: Theme.fieldBackground
|
||||
border.width: textInput.activeFocus ? Theme.borderStrong : Theme.borderThin
|
||||
border.color: textInput.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
|
||||
}
|
||||
}
|
||||
|
||||
component SettingsActionButton: Button {
|
||||
id: actionButton
|
||||
hoverEnabled: true
|
||||
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusSm
|
||||
color: actionButton.down ? Theme.buttonNeutralPressed : (actionButton.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground)
|
||||
border.width: Theme.borderThin
|
||||
border.color: Theme.fieldBorder
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: actionButton.text
|
||||
color: Theme.buttonNeutralText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
|
||||
component SettingsToggle: CheckBox {
|
||||
id: toggle
|
||||
indicator: Rectangle {
|
||||
implicitWidth: 20
|
||||
implicitHeight: 20
|
||||
x: toggle.leftPadding
|
||||
y: parent.height / 2 - height / 2
|
||||
radius: Theme.radiusXs
|
||||
color: toggle.checked ? Theme.primaryAccent : Theme.fieldBackground
|
||||
border.width: Theme.borderThin
|
||||
border.color: toggle.checked ? Theme.primaryAccent : Theme.fieldBorder
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: 9
|
||||
height: 9
|
||||
radius: Theme.radiusXs
|
||||
visible: toggle.checked
|
||||
color: Theme.fieldBackground
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: toggle.text
|
||||
color: Theme.checkboxText
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: toggle.indicator.width + toggle.spacing
|
||||
}
|
||||
}
|
||||
|
||||
// ===== File Picker Dialog =====
|
||||
FileDialog {
|
||||
id: masterPicker
|
||||
title: "Choose Master CSV"
|
||||
nameFilters: ["CSV files (*.csv)"]
|
||||
property string targetFamily: ""
|
||||
onAccepted: settingsModel.setMaster(targetFamily, String(selectedFile).replace(/^file:\/\//, ""))
|
||||
}
|
||||
|
||||
// ===== Settings Page Layout =====
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
ScrollView {
|
||||
id: settingsScroll
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.settingsOuterMargin
|
||||
|
||||
contentWidth: availableWidth
|
||||
contentHeight: container.implicitHeight
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
parent: settingsScroll
|
||||
anchors.top: settingsScroll.top
|
||||
anchors.right: settingsScroll.right
|
||||
anchors.bottom: settingsScroll.bottom
|
||||
policy: ScrollBar.AsNeeded
|
||||
|
||||
contentItem: Rectangle {
|
||||
implicitWidth: 8
|
||||
implicitHeight: 8
|
||||
radius: Theme.radiusSm
|
||||
color: Theme.trackBackground
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
implicitWidth: 8
|
||||
color: "transparent"
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: container
|
||||
width: Math.min(settingsScroll.availableWidth, Theme.settingsPanelMaxWidth)
|
||||
x: (settingsScroll.availableWidth - width) / 2
|
||||
spacing: Theme.settingsSectionSpacing
|
||||
|
||||
// ===== Page Title =====
|
||||
Label {
|
||||
text: "Settings"
|
||||
font.pixelSize: 30
|
||||
font.bold: true
|
||||
color: Theme.headingColor
|
||||
}
|
||||
|
||||
// ===== Chamber Settings =====
|
||||
SettingsGroupBox {
|
||||
title: "Chamber"
|
||||
Layout.fillWidth: true
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.settingsRowSpacing
|
||||
|
||||
Label {
|
||||
text: "Chamber ID"
|
||||
color: Theme.headingColor
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
SettingsTextInput {
|
||||
id: chamberField
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: Theme.settingsFieldMinWidth
|
||||
placeholderText: "Enter chamber ID"
|
||||
text: settingsModel.chamberId
|
||||
|
||||
Connections {
|
||||
target: settingsModel
|
||||
function onChamberIdChanged() {
|
||||
if (!chamberField.activeFocus) {
|
||||
chamberField.text = settingsModel.chamberId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsActionButton {
|
||||
text: "Set"
|
||||
Layout.preferredWidth: 90
|
||||
Layout.preferredHeight: Theme.settingsButtonHeight
|
||||
onClicked: {
|
||||
settingsModel.setChamberId(chamberField.text);
|
||||
settingsModel.saveSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Master CSV Mapping =====
|
||||
SettingsGroupBox {
|
||||
title: "Master Files"
|
||||
Layout.fillWidth: true
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.settingsGridSpacing
|
||||
|
||||
Repeater {
|
||||
model: root.masterFamilies
|
||||
|
||||
RowLayout {
|
||||
property string family: modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.settingsRowSpacing
|
||||
|
||||
Label {
|
||||
text: family
|
||||
font.bold: true
|
||||
color: Theme.headingColor
|
||||
Layout.preferredWidth: 24
|
||||
}
|
||||
|
||||
SettingsTextInput {
|
||||
Layout.fillWidth: true
|
||||
readOnly: true
|
||||
text: root.masterPath(family)
|
||||
placeholderText: "No master selected"
|
||||
color: Theme.bodyColor
|
||||
}
|
||||
|
||||
SettingsActionButton {
|
||||
text: "Browse"
|
||||
Layout.preferredHeight: Theme.settingsButtonHeight
|
||||
onClicked: {
|
||||
masterPicker.targetFamily = family;
|
||||
masterPicker.open();
|
||||
}
|
||||
}
|
||||
|
||||
SettingsActionButton {
|
||||
text: "Clear"
|
||||
Layout.preferredHeight: Theme.settingsButtonHeight
|
||||
onClicked: settingsModel.clearMaster(family)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Wafer Behavior =====
|
||||
SettingsGroupBox {
|
||||
title: "Wafer Behavior"
|
||||
Layout.fillWidth: true
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.settingsGridSpacing
|
||||
|
||||
SettingsToggle {
|
||||
text: "Reverse Z Wafer"
|
||||
checked: settingsModel.reverseZWafer
|
||||
onToggled: settingsModel.setReverseZWafer(checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Appearance =====
|
||||
SettingsGroupBox {
|
||||
title: "Appearance"
|
||||
Layout.fillWidth: true
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.settingsRowSpacing
|
||||
|
||||
// Custom pill toggle
|
||||
Rectangle {
|
||||
id: toggleTrack
|
||||
implicitWidth: 52
|
||||
implicitHeight: 28
|
||||
radius: height / 2
|
||||
color: Theme.trackBackground
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: implicitWidth
|
||||
Layout.preferredHeight: implicitHeight
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 250
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
// Thumb
|
||||
Rectangle {
|
||||
id: toggleThumb
|
||||
width: 22
|
||||
height: 22
|
||||
radius: height / 2
|
||||
color: "white"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: Theme.isDarkMode ? parent.width - width - 3 : 3
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: 250
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
// Drop shadow effect
|
||||
layer.enabled: true
|
||||
layer.effect: null // swap for DropShadow if Qt.labs.effects is available
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: Theme.isDarkMode = !Theme.isDarkMode
|
||||
}
|
||||
}
|
||||
|
||||
// Label
|
||||
Label {
|
||||
text: Theme.isDarkMode ? "Dark" : "Light"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 13
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 200
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Adjustment Entry Point =====
|
||||
RowLayout {
|
||||
id: adjustmentRow
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.settingsRowSpacing
|
||||
|
||||
SettingsActionButton {
|
||||
text: "Edit CSV Metadata"
|
||||
Layout.preferredWidth: Theme.settingsActionWidth
|
||||
Layout.preferredHeight: Theme.settingsButtonHeight
|
||||
onClicked: root.handleEditCsvMetadata()
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom spacer so last group isn't flush with scroll edge
|
||||
Item {
|
||||
Layout.preferredHeight: Theme.settingsSectionSpacing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Scroll hint overlay (fades out when scrolled) =====
|
||||
Rectangle {
|
||||
id: scrollHint
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 48
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0.0
|
||||
color: Theme.isDarkMode ? 'rgba(30,30,35,1)' : 'rgba(255,255,255,1)'
|
||||
}
|
||||
GradientStop {
|
||||
position: 1.0
|
||||
color: Qt.rgba(0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
text: "↓ Scroll for more"
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
color: Theme.bodyColor
|
||||
opacity: (settingsScroll.ScrollBar.vertical.position <= 0 && settingsScroll.contentHeight > settingsScroll.height) ? (scrollHintTimer.running ? 1 : 0) : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 400
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Auto-hide scroll hint after first interaction =====
|
||||
Timer {
|
||||
id: scrollHintTimer
|
||||
running: true
|
||||
repeat: false
|
||||
onTriggered: scrollHint.visible = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import ISC
|
||||
|
||||
// ===== Status Tab =====
|
||||
// Blank on startup. Fills in after Detect Wafer fires from the left rail.
|
||||
ColumnLayout {
|
||||
id: root
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.panelPadding
|
||||
spacing: Theme.rightPaneGap
|
||||
|
||||
property alias waferFamilyCode: waferInfoFamily.text
|
||||
property alias waferSerialNumber: waferSerial.text
|
||||
property alias waferSensorCount: waferSensors.text
|
||||
property alias waferRuntime: waferRuntime.text
|
||||
property alias waferCycles: waferCycles.text
|
||||
property bool waferDetected: false
|
||||
|
||||
// Latches true once any operation starts (or a previous session is
|
||||
// restored) and never resets, so the status panel stays visible even
|
||||
// when a detect finds no wafer.
|
||||
property bool statusActive: false
|
||||
|
||||
// Data summary after parse
|
||||
property int dataRows: 0
|
||||
property int dataCols: 0
|
||||
property string csvPath: ""
|
||||
property bool dataParsed: false
|
||||
|
||||
// ===== Empty state — nothing shown until detect fires =====
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
visible: !root.statusActive
|
||||
}
|
||||
|
||||
// ===== Results area (shown once detect/operation runs) =====
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
visible: root.statusActive
|
||||
spacing: Theme.rightPaneGap
|
||||
|
||||
// --- Connection Status ---
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 86
|
||||
color: {
|
||||
if (deviceController.connectionStatus === "Connected") return Theme.statusSuccessColor + "22"
|
||||
if (deviceController.connectionStatus === "Disconnected") return Theme.statusErrorColor + "22"
|
||||
return Theme.cardBackground
|
||||
}
|
||||
border.color: {
|
||||
if (deviceController.connectionStatus === "Connected") return Theme.statusSuccessColor
|
||||
if (deviceController.connectionStatus === "Disconnected") return Theme.statusErrorColor
|
||||
return Theme.cardBorder
|
||||
}
|
||||
radius: Theme.radiusSm
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.panelPadding
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
text: deviceController.connectionStatus
|
||||
color: {
|
||||
if (deviceController.connectionStatus === "Connected") return Theme.statusSuccessColor
|
||||
if (deviceController.connectionStatus === "Disconnected") return Theme.statusErrorColor
|
||||
return Theme.bodyColor
|
||||
}
|
||||
font.bold: true
|
||||
font.pixelSize: 16
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Port: " + (deviceController.selectedPort || "None selected")
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 14
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Save dir: " + deviceController.saveDataDir
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 12
|
||||
opacity: 0.7
|
||||
elide: Text.ElideMiddle
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Wafer Info ---
|
||||
GroupBox {
|
||||
title: "Wafer Information"
|
||||
visible: root.waferDetected
|
||||
Layout.fillWidth: true
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.panelPadding
|
||||
spacing: 8
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.rightPaneGap
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
Text { text: "Family Code"; color: Theme.subheadingColor; font.pixelSize: 12 }
|
||||
Text { id: waferInfoFamily; text: "—"; color: Theme.bodyColor; font.pixelSize: 16; font.bold: true }
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
Text { text: "Serial Number"; color: Theme.subheadingColor; font.pixelSize: 12 }
|
||||
Text { id: waferSerial; text: "—"; color: Theme.bodyColor; font.pixelSize: 16; font.bold: true }
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
Text { text: "Sensor Count"; color: Theme.subheadingColor; font.pixelSize: 12 }
|
||||
Text { id: waferSensors; text: "—"; color: Theme.bodyColor; font.pixelSize: 16; font.bold: true }
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.rightPaneGap
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
Text { text: "Runtime"; color: Theme.subheadingColor; font.pixelSize: 12 }
|
||||
Text { id: waferRuntime; text: "—"; color: Theme.bodyColor; font.pixelSize: 16; font.bold: true }
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
Text { text: "Cycles"; color: Theme.subheadingColor; font.pixelSize: 12 }
|
||||
Text { id: waferCycles; text: "—"; color: Theme.bodyColor; font.pixelSize: 16; font.bold: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Data Summary (shown after parse) ---
|
||||
GroupBox {
|
||||
title: "Data Summary"
|
||||
visible: root.dataParsed
|
||||
Layout.fillWidth: true
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.panelPadding
|
||||
spacing: Theme.rightPaneGap
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
Text { text: "Rows"; color: Theme.subheadingColor; font.pixelSize: 12 }
|
||||
Text { text: String(root.dataRows); color: Theme.bodyColor; font.pixelSize: 16; font.bold: true }
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
Text { text: "Sensors"; color: Theme.subheadingColor; font.pixelSize: 12 }
|
||||
Text { text: String(root.dataCols); color: Theme.bodyColor; font.pixelSize: 16; font.bold: true }
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
Text { text: "CSV"; color: Theme.subheadingColor; font.pixelSize: 12 }
|
||||
Text { text: root.csvPath; color: Theme.bodyColor; font.pixelSize: 12; elide: Text.ElideMiddle }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Activity Log ---
|
||||
GroupBox {
|
||||
title: "Activity Log"
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
TextArea {
|
||||
id: activityLog
|
||||
width: parent.width
|
||||
text: ""
|
||||
readOnly: true
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 12
|
||||
wrapMode: TextArea.WordWrap
|
||||
leftPadding: 4
|
||||
rightPadding: 4
|
||||
color: Theme.bodyColor
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: deviceController
|
||||
function onActivityLogUpdated(newLog) {
|
||||
activityLog.text = newLog
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Signal Handlers ---
|
||||
Connections {
|
||||
target: deviceController
|
||||
// Any operation start (detect/read/erase/debug) latches the panel on.
|
||||
function onPortsUpdated() {
|
||||
if (deviceController.operationInProgress)
|
||||
root.statusActive = true
|
||||
}
|
||||
|
||||
function onDetectResult(result) {
|
||||
root.statusActive = true
|
||||
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 onStatusRestored() {
|
||||
// Show restored wafer info if available
|
||||
var info = deviceController.lastWaferInfo
|
||||
if (info && info.length > 0) {
|
||||
root.statusActive = true
|
||||
root.waferDetected = true
|
||||
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.statusActive = true
|
||||
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 {
|
||||
target: deviceController
|
||||
function onReadResult(result) {
|
||||
root.dataParsed = false
|
||||
root.dataRows = 0
|
||||
root.dataCols = 0
|
||||
root.csvPath = ""
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: deviceController
|
||||
function onParsedDataReady(result) {
|
||||
if (result && result.success) {
|
||||
root.dataParsed = true
|
||||
root.dataRows = result.rows
|
||||
root.dataCols = result.cols
|
||||
root.csvPath = result.csv_path || ""
|
||||
} else {
|
||||
root.dataParsed = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# ===== ISC Tabs Module =====
|
||||
module ISC.Tabs
|
||||
|
||||
# ===== Tab Components =====
|
||||
SettingsTab 1.0 SettingsTab.qml
|
||||
Reference in New Issue
Block a user