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:
jack
2026-06-03 11:41:45 -07:00
parent af170666e8
commit 9779baa468
34 changed files with 130 additions and 36 deletions
+153
View File
@@ -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 }
}
}
}
}
}
+603
View File
@@ -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()
}
}
}
}
}
+443
View File
@@ -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
}
}
+290
View File
@@ -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
}
}
}
}
+5
View File
@@ -0,0 +1,5 @@
# ===== ISC Tabs Module =====
module ISC.Tabs
# ===== Tab Components =====
SettingsTab 1.0 SettingsTab.qml