Compare commits

...

3 Commits

Author SHA1 Message Date
jack 6d33da2eab Merge pull request 'SettingTab' (#1) from SettingTab into main
Reviewed-on: #1
2026-04-27 21:28:54 +00:00
Jack.Le fbb04eb2f7 Refactor main application entry and enhance UI with file browser integration
- Changed application entry point to use QApplication for better compatibility with widgets.
- Set up QML UI style to support custom button backgrounds.
- Introduced LocalSettingsModel and FileBrowser for managing application settings and file selection.
- Updated QML components to reflect new data models and improve layout consistency.
- Enhanced Theme.qml with detailed comments and improved color management for better readability.
2026-04-27 14:28:08 -07:00
Jack.Le 9f8c6e1a4c Enhance UI and add file selection functionality
- Updated  to include new files for the project.
- Modified  to improve layout and navigation, including new properties for tab and action management.
- Introduced  for file selection with metadata editing capabilities.
- Created  for managing application settings and integrating the file selection dialog.
2026-04-27 13:56:36 -07:00
7 changed files with 1476 additions and 69 deletions
+63 -10
View File
@@ -3,24 +3,33 @@ import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import ISC import ISC
// ===== Home Workspace Shell =====
Rectangle { Rectangle {
id: root id: root
anchors.fill: parent anchors.fill: parent
color: Theme.pageBackground color: Theme.pageBackground
clip: true clip: true
border.color: Theme.outerFrameBorder
border.width: Theme.borderStrong
// ===== Navigation Model =====
// Primary navigation shown in the left rail. // Primary navigation shown in the left rail.
property var sideActions: ["DETECT WAFER", "READ MEMORY", "OPEN CSV IN EXCEL", "ERASE MEMORY", "IMPORT DATA", "STORED DATA"] property var sideActions: ["DETECT WAFER", "READ MEMORY", "OPEN CSV IN EXCEL", "ERASE MEMORY", "IMPORT DATA", "STORED DATA"]
// ===== Footer Tab Model =====
// Footer tabs drive the active workspace section. // Footer tabs drive the active workspace section.
property var bottomTabs: ["Status", "Graph", "Data", "Wafer Map", "Compare", "Split", "Settings", "About"] property var bottomTabs: ["Status", "Graph", "Data", "Wafer Map", "Compare", "Split", "Settings", "About"]
// ===== View State =====
property int selectedTabIndex: 0 property int selectedTabIndex: 0
property int selectedSideActionIndex: 0
// ===== Main Two-Column Layout =====
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
spacing: 0 spacing: 0
// ===== Left Action Rail =====
// Left control rail. // Left control rail.
Rectangle { Rectangle {
id: sideRail id: sideRail
@@ -28,7 +37,7 @@ Rectangle {
Layout.fillHeight: true Layout.fillHeight: true
color: Theme.sideRailBackground color: Theme.sideRailBackground
border.color: Theme.workspaceBorder border.color: Theme.workspaceBorder
border.width: 1 border.width: Theme.borderThin
readonly property int actionCount: root.sideActions.length readonly property int actionCount: root.sideActions.length
readonly property real computedButtonHeight: Math.min(Theme.sideButtonHeight, (height - (Theme.panelPadding * 2) - (Theme.sideRailSpacing * Math.max(0, actionCount - 1))) / Math.max(1, actionCount)) readonly property real computedButtonHeight: Math.min(Theme.sideButtonHeight, (height - (Theme.panelPadding * 2) - (Theme.sideRailSpacing * Math.max(0, actionCount - 1))) / Math.max(1, actionCount))
@@ -44,20 +53,41 @@ Rectangle {
Button { Button {
id: control id: control
text: modelData text: modelData
property bool isActive: index === root.selectedSideActionIndex
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: Math.max(Theme.sideButtonMinHeight, sideRail.computedButtonHeight) Layout.preferredHeight: Math.max(Theme.sideButtonMinHeight, sideRail.computedButtonHeight)
hoverEnabled: true
onClicked: root.selectedSideActionIndex = index
background: Rectangle { background: Rectangle {
color: control.down ? Theme.buttonPressed : Theme.cardBackground color: {
border.color: Theme.cardBorder if (control.down) {
return Theme.buttonPressed;
}
if (control.isActive) {
return Theme.sideActiveBackground;
}
return "transparent";
}
border.color: control.isActive ? Theme.cardBorder : "transparent"
border.width: 1 border.width: 1
radius: 4 radius: Theme.radiusSm
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: 3
visible: control.isActive
color: Theme.primaryAccent
radius: Theme.radiusXs
}
} }
contentItem: Text { contentItem: Text {
text: control.text text: control.text
color: Theme.headingColor color: control.isActive ? Theme.headingColor : Theme.bodyColor
font.bold: true font.bold: control.isActive
font.pixelSize: 18 font.pixelSize: 18
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@@ -74,13 +104,14 @@ Rectangle {
Layout.fillHeight: true Layout.fillHeight: true
color: Theme.workspaceBackground color: Theme.workspaceBackground
border.color: Theme.workspaceBorder border.color: Theme.workspaceBorder
border.width: 1 border.width: Theme.borderThin
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.mainAreaPadding anchors.margins: Theme.mainAreaPadding
spacing: Theme.rightPaneGap spacing: Theme.rightPaneGap
// ===== Active Tab Content Area =====
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
@@ -88,23 +119,45 @@ Rectangle {
color: Theme.responseBackground color: Theme.responseBackground
border.color: Theme.responseBorder border.color: Theme.responseBorder
border.width: 1 border.width: 1
radius: 8 radius: Theme.radiusMd
StackLayout {
anchors.fill: parent
currentIndex: root.selectedTabIndex
// ===== Tab Content Routing =====
Repeater {
model: root.bottomTabs
Item {
property string tabName: modelData
Loader {
anchors.fill: parent
active: parent.tabName === "Settings"
source: parent.tabName === "Settings" ? "Tabs/SettingsTab.qml" : ""
}
Label { Label {
anchors.centerIn: parent anchors.centerIn: parent
text: "Main content area" visible: parent.tabName !== "Settings"
text: parent.tabName + " content"
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 20 font.pixelSize: 20
} }
} }
}
}
}
// Keep the tab strip evenly distributed across the footer. // Keep the tab strip evenly distributed across the footer.
// ===== Footer Tab Strip =====
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: Theme.tabBarHeight + (Theme.tabBarPadding * 2) Layout.preferredHeight: Theme.tabBarHeight + (Theme.tabBarPadding * 2)
color: Theme.tabBarBackground color: Theme.tabBarBackground
border.color: Theme.workspaceBorder border.color: Theme.workspaceBorder
border.width: 1 border.width: Theme.borderThin
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
+614
View File
@@ -0,0 +1,614 @@
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
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 ?? "";
notesField.text = rec.notes ?? "";
selectedField.checked = rec.selected ?? false;
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, selectedField.checked);
metadataEditDialog.close();
metadataEditRow = -1;
}
readonly property var headerTitles: ["", "Wafer", "Date", "Chamber", "Notes", "File", "Edit", "Save"]
readonly property var normalizedRows: (tableModel || []).map(function (row) {
row = row || {};
return {
select: row.selected ?? false,
wafer: row.wafer ?? "",
date: row.date ?? "",
chamber: row.chamber ?? "",
edit: "",
save: "",
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) {
// [select, wafer, date, chamber, notes, file (variable), edit, save]
const fixed = {
0: 40,
1: 90,
2: 170,
3: 120,
4: 180,
6: 56,
7: 56
};
if (fixed.hasOwnProperty(col))
return fixed[col];
// col 5 (File) takes remaining width
const w = Math.max(400, tableView.width);
const used = 40 + 90 + 170 + 120 + 180 + 56 + 56;
return Math.max(120, w - used);
}
model: TableModel {
TableModelColumn {
display: "select"
}
TableModelColumn {
display: "wafer"
}
TableModelColumn {
display: "date"
}
TableModelColumn {
display: "chamber"
}
TableModelColumn {
display: "notes"
}
TableModelColumn {
display: "fileName"
}
TableModelColumn {
display: "edit"
}
TableModelColumn {
display: "save"
}
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;
}
// Checkbox (col 0)
Loader {
anchors.fill: parent
active: column === 0
sourceComponent: CheckBox {
anchors.centerIn: parent
checked: root.tableModel[row]?.selected ?? false
onCheckedChanged: {
if (root.tableModel[row])
root.tableModel[row].selected = checked;
}
}
}
// 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
}
}
}
// Save button (col 7)
Loader {
anchors.fill: parent
anchors.margins: 4
active: column === 7
sourceComponent: Button {
hoverEnabled: true
text: "Save"
font.pixelSize: 12
onClicked: {
const record = root.tableModel[row];
if (!record)
return;
file_browser.saveMetadata(record.fileName ?? "", record.wafer ?? "", record.date ?? "", record.chamber ?? "", record.notes ?? "", record.selected ?? false);
}
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)
return record.fileName ?? "";
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 !== 0 && column !== 6 && column !== 7
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();
}
}
DialogActionButton {
text: "Cancel"
Layout.preferredWidth: 90
onClicked: 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 File Information")
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
}
Text {
id: filePathLabel
Layout.fillWidth: true
Layout.maximumHeight: 56
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
}
CheckBox {
id: selectedField
text: qsTr("Selected for processing")
}
RowLayout {
Layout.fillWidth: true
Layout.topMargin: 8
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()
}
}
}
}
}
+437
View File
@@ -0,0 +1,437 @@
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) {
console.log("Doing work with:", filePath);
// @todo: implement CSV metadata handling after file selection.
}
// ===== 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, selectedFile.toLocalFile())
}
// ===== 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
onEditingFinished: settingsModel.setChamberId(text)
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)
}
}
}
// ===== 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
}
}
}
}
// ===== 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.transparent
}
}
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
}
}
+104 -52
View File
@@ -2,44 +2,120 @@ pragma Singleton
import QtQuick import QtQuick
// ===== Shared Theme Tokens ===== // =============================================================================
// Theme — single source of truth for colors and sizing
// =============================================================================
// Sections:
// 1. Mode flag
// 2. Tone palette (raw tones; everything else derives from these)
// 3. Surfaces (page / card / panel / workspace backgrounds)
// 4. Borders
// 5. Text (headings, body, panel titles)
// 6. Controls (fields, buttons, checkbox)
// 7. Tracks (pill toggle + scrollbar)
// 8. Tabs (footer tab bar)
// 9. Side rail
// 10. Status (success / warning / error)
// 11. Geometry (radius / border width / spacing / sizing)
// =============================================================================
QtObject { QtObject {
// ===== Theme Mode ===== // ── 1. Mode ──────────────────────────────────────────────────────────────
// Toggle this to preview the same shell in light mode. property bool isDarkMode: false
property bool isDarkMode: true
// ===== Surface Colors ===== // ── 2. Tone palette (base values; consume via semantic tokens below) ─────
// Core application surfaces. readonly property color tone100: isDarkMode ? "#111111" : "#FAFAFA"
readonly property color pageBackground: isDarkMode ? "#11161c" : "#eef4f7" readonly property color tone200: isDarkMode ? "#1A1A1A" : "#F2F2F2"
readonly property color cardBackground: isDarkMode ? "#1b232d" : "#ffffff" readonly property color tone300: isDarkMode ? "#242424" : "#E8E8E8"
readonly property color workspaceBackground: isDarkMode ? "#121923" : "#ffffff" readonly property color toneText: isDarkMode ? "#F2F2F2" : "#111111"
readonly property color sideRailBackground: isDarkMode ? "#18202a" : "#f5f7fa" readonly property color toneMute: isDarkMode ? "#A8A8A8" : "#8A8A8A"
readonly property color responseBackground: isDarkMode ? "#18212a" : "#f9fbfc" readonly property color toneBorder: isDarkMode ? "#2B2B2B" : "#D8D8D8"
// Shared border and text colors. // ── 3. Surfaces ──────────────────────────────────────────────────────────
readonly property color cardBorder: isDarkMode ? "#334155" : "#cbd5e1" readonly property color pageBackground: tone100
readonly property color workspaceBorder: isDarkMode ? "#334155" : "#d7dde5" readonly property color cardBackground: tone200
readonly property color responseBorder: isDarkMode ? "#334155" : "#d5e0e8" readonly property color panelBackground: tone200
readonly property color workspaceBackground: tone200
readonly property color responseBackground: tone200
readonly property color subtleSectionBackground: tone300
readonly property color headingColor: isDarkMode ? "#e5eef7" : "#12324a" // ── 4. Borders ───────────────────────────────────────────────────────────
readonly property color bodyColor: isDarkMode ? "#afbdca" : "#415a6b" readonly property color cardBorder: toneBorder
readonly property color buttonPressed: isDarkMode ? "#214d60" : "#17576c" readonly property color responseBorder: toneBorder
readonly property color statusSuccessColor: isDarkMode ? "#7cd67e" : "#1f7a33" readonly property color workspaceBorder: toneBorder
readonly property color statusErrorColor: isDarkMode ? "#ff8a80" : "#c62828" readonly property color innerFrameBorder: toneBorder
readonly property color statusWarningColor: isDarkMode ? "#f3c677" : "#9a6700" readonly property color outerFrameBorder: isDarkMode ? "#343434" : "#CECECE"
readonly property color softBorder: isDarkMode ? "#222222" : "#E2E2E2"
// ===== Side Rail Sizing ===== // ── 5. Text ──────────────────────────────────────────────────────────────
readonly property color headingColor: toneText
readonly property color bodyColor: toneMute
readonly property color panelTitleText: toneMute
// ── 6. Controls ──────────────────────────────────────────────────────────
// Fields
readonly property color fieldBackground: tone100
readonly property color fieldText: toneText
readonly property color fieldPlaceholder: toneMute
readonly property color fieldBorder: toneBorder
readonly property color fieldBorderFocus: toneText
// Neutral buttons
readonly property color buttonNeutralBackground: tone300
readonly property color buttonNeutralHover: isDarkMode ? "#303030" : "#DDDDDD"
readonly property color buttonNeutralPressed: tone100
readonly property color buttonNeutralText: toneText
readonly property color buttonPressed: tone300
readonly property color primaryAccent: toneText
// Checkbox
readonly property color checkboxText: toneText
// ── 7. Tracks (pill toggle, scrollbar thumb) ─────────────────────────────
readonly property color trackBackground: isDarkMode ? "#25252B" : "#D1D5DB"
// ── 8. Tabs (footer tab bar) ─────────────────────────────────────────────
readonly property color tabBarBackground: tone200
readonly property color tabBackground: "transparent"
readonly property color tabActiveBackground: "transparent"
readonly property color tabHoverBackground: tone300
readonly property color tabBorder: "transparent"
readonly property color tabText: toneMute
readonly property color tabActiveText: toneText
// ── 9. Side rail ─────────────────────────────────────────────────────────
readonly property color sideRailBackground: tone200
readonly property color sideActiveBackground: tone300
// ── 10. Status ───────────────────────────────────────────────────────────
readonly property color statusSuccessColor: isDarkMode ? "#63D471" : "#2E9E44"
readonly property color statusWarningColor: isDarkMode ? "#F5C15C" : "#C88A18"
readonly property color statusErrorColor: isDarkMode ? "#FF6B6B" : "#D64545"
// ── 11. Geometry ─────────────────────────────────────────────────────────
// Radius
readonly property int radiusXs: 4 // fields, tight elements
readonly property int radiusSm: 6 // buttons
readonly property int radiusMd: 10 // cards, group boxes
readonly property int radiusLg: 14 // large panels / dialogs
// Border width
readonly property int borderThin: 1
readonly property int borderStrong: 2
// Main panel layout
readonly property int mainAreaPadding: 10
readonly property int rightPaneGap: 6
readonly property int panelPadding: 12
// Side rail layout
readonly property int sideRailWidth: 190 readonly property int sideRailWidth: 190
readonly property int sideRailSpacing: 14 readonly property int sideRailSpacing: 14
readonly property int sideButtonHeight: 146 readonly property int sideButtonHeight: 146
readonly property int sideButtonMinHeight: 88 readonly property int sideButtonMinHeight: 88
readonly property int panelPadding: 12 // Tab bar layout
// ===== Main Panel Spacing =====
// Main content spacing.
readonly property int mainAreaPadding: 10
readonly property int rightPaneGap: 6
readonly property int tabBarHeight: 34 readonly property int tabBarHeight: 34
readonly property int tabBarPadding: 6
readonly property int tabSpacing: 2
readonly property int tabRadius: 2
readonly property int tabFontSize: 12
readonly property int tabButtonMinWidth: 80
// Settings page layout
readonly property int settingsPanelMaxWidth: 760 readonly property int settingsPanelMaxWidth: 760
readonly property int settingsOuterMargin: 40 readonly property int settingsOuterMargin: 40
readonly property int settingsSectionSpacing: 20 readonly property int settingsSectionSpacing: 20
@@ -48,28 +124,4 @@ QtObject {
readonly property int settingsButtonHeight: 40 readonly property int settingsButtonHeight: 40
readonly property int settingsActionWidth: 200 readonly property int settingsActionWidth: 200
readonly property int settingsFieldMinWidth: 160 readonly property int settingsFieldMinWidth: 160
// ===== Footer Tab Theme =====
// Bottom tab styling.
readonly property color tabBarBackground: isDarkMode ? "#161d26" : "#f2f2f2"
readonly property color tabBackground: isDarkMode ? "#202833" : "#ececec"
readonly property color tabActiveBackground: isDarkMode ? "#2a3440" : "#ffffff"
readonly property color tabHoverBackground: isDarkMode ? "#263140" : "#f7f7f7"
readonly property color tabBorder: isDarkMode ? "#3a4758" : "#cfcfcf"
readonly property color tabText: isDarkMode ? "#c6d0da" : "#222222"
readonly property color tabActiveText: isDarkMode ? "#ffffff" : "#111111"
readonly property int tabButtonMinWidth: 80
readonly property int tabHorizontalPadding: 14
readonly property int tabBarPadding: 6
readonly property int tabSpacing: 2
readonly property int tabRadius: 2
readonly property int tabFontSize: 12
// ===== Form Control Theme =====
readonly property color inputBackground: isDarkMode ? "#202833" : "#ffffff"
readonly property color inputBorder: isDarkMode ? "#4a596d" : "#cfcfcf"
readonly property color buttonBackground: isDarkMode ? "#202833" : "#ffffff"
readonly property color buttonText: isDarkMode ? "#e5eef7" : "#111111"
readonly property color checkboxText: isDarkMode ? "#d6dfeb" : "#111111"
} }
+230
View File
@@ -0,0 +1,230 @@
from __future__ import annotations
import json
import re
from datetime import datetime
from pathlib import Path
from PySide6.QtCore import QObject, Property, QStandardPaths, Signal, Slot
from PySide6.QtWidgets import QFileDialog, QMessageBox
from backend.csv_file_metadata import CSVFileMetadata
from backend.zwafer_parser import ZWaferParser
# ===== File Browser Model =====
class FileBrowser(QObject):
# ===== Change Signals =====
filesChanged = Signal()
currentDirectoryChanged = Signal()
# ===== Lifecycle =====
def __init__(self, parent: QObject | None = None) -> None:
super().__init__(parent)
self._parser = ZWaferParser()
self._files: list[dict[str, object]] = []
self._current_directory = self._resolve_default_directory()
self._refresh_files(show_empty_message=False)
# ===== Exposed Properties =====
@Property("QVariantList", notify=filesChanged)
def files(self) -> list[dict[str, object]]:
return [dict(row) for row in self._files]
@Property(str, notify=currentDirectoryChanged)
def currentDirectory(self) -> str:
return str(self._current_directory)
# ===== Directory Selection =====
@Slot()
def chooseDirectory(self) -> None:
selected_dir = QFileDialog.getExistingDirectory(
None,
"Select CSV Folder",
self.currentDirectory,
)
if not selected_dir:
return
self._set_current_directory(Path(selected_dir))
self._refresh_files(show_empty_message=True)
@Slot()
def refreshFiles(self) -> None:
self._refresh_files(show_empty_message=False)
# ===== Metadata Persistence =====
@Slot(str, str, str, str, str, bool)
def saveMetadata(
self,
file_path: str,
wafer: str,
date: str,
chamber: str,
notes: str,
selected: bool,
) -> None:
csv_path = Path(file_path)
if not csv_path.exists():
self._show_error(f"CSV file was not found:\n{file_path}")
return
row = self._find_row(file_path)
if row is None:
self._show_error("The selected CSV row is no longer available.")
return
row["wafer"] = wafer
row["date"] = date
row["chamber"] = chamber
row["notes"] = notes
row["selected"] = selected
payload = {
"wafer": wafer,
"date": date,
"chamber": chamber,
"notes": notes,
}
try:
sidecar_path = self._sidecar_path(csv_path)
sidecar_path.write_text(json.dumps(payload, indent=4), encoding="utf-8")
except Exception as exc: # pragma: no cover - defensive UI path
self._show_error(f"Failed to save metadata:\n{exc}")
return
self.filesChanged.emit()
self._show_info(f"Saved metadata for:\n{csv_path.name}")
# ===== Internal Data Loading =====
def _refresh_files(self, show_empty_message: bool) -> None:
selected_state = {
str(row.get("fileName", "")): bool(row.get("selected", False))
for row in self._files
}
self._files = []
if not self._current_directory.exists():
self.filesChanged.emit()
return
for csv_path in sorted(self._current_directory.glob("*.csv")):
try:
metadata = self._load_metadata(csv_path)
self._files.append(
{
"selected": selected_state.get(str(csv_path), False),
"wafer": metadata.wafer,
"date": metadata.string_date_format(),
"chamber": metadata.chamber,
"notes": metadata.notes,
"fileName": str(csv_path),
"highlight": metadata.get_wafer_type() in {"A", "B", "C"},
}
)
except Exception:
self._files.append(
{
"selected": selected_state.get(str(csv_path), False),
"wafer": csv_path.stem,
"date": "",
"chamber": "",
"notes": "Unable to parse metadata",
"fileName": str(csv_path),
"highlight": False,
}
)
self.filesChanged.emit()
if show_empty_message and not self._files:
self._show_info("No CSV files were found in the selected folder.")
def _load_metadata(self, csv_path: Path) -> CSVFileMetadata:
sidecar_path = self._sidecar_path(csv_path)
if sidecar_path.exists():
try:
payload = json.loads(sidecar_path.read_text(encoding="utf-8"))
return CSVFileMetadata(
wafer=str(payload.get("wafer", "")),
date=str(payload.get("date", "")),
chamber=str(payload.get("chamber", "")),
notes=str(payload.get("notes", "")),
filename=str(csv_path),
)
except Exception:
# Fall back to CSV/header parsing if sidecar is malformed.
pass
parser_data, _ = self._parser.parse(str(csv_path))
if parser_data is not None:
wafer = parser_data.serial or ""
date_text = ""
if parser_data.date != datetime.min:
date_text = CSVFileMetadata.format_date(parser_data.date)
return CSVFileMetadata(
wafer=wafer,
date=date_text,
chamber="",
notes="",
filename=str(csv_path),
)
return self._metadata_from_filename(csv_path)
def _metadata_from_filename(self, csv_path: Path) -> CSVFileMetadata:
match = re.match(
r"^(?P<wafer>[^-]+)-(?P<date>\d{8})(?:_(?P<run>\d+))?$",
csv_path.stem,
)
if not match:
return CSVFileMetadata(filename=str(csv_path))
date_text = ""
try:
parsed = datetime.strptime(match.group("date"), "%Y%m%d")
date_text = CSVFileMetadata.format_date(parsed)
except ValueError:
date_text = ""
return CSVFileMetadata(
wafer=match.group("wafer"),
date=date_text,
filename=str(csv_path),
)
# ===== Internal Helpers =====
def _resolve_default_directory(self) -> Path:
documents_dir = QStandardPaths.writableLocation(
QStandardPaths.DocumentsLocation
)
base_dir = Path(documents_dir) if documents_dir else (Path.home() / "Documents")
return base_dir / "isc_data"
def _set_current_directory(self, directory: Path) -> None:
normalized = Path(directory)
if normalized == self._current_directory:
return
self._current_directory = normalized
self.currentDirectoryChanged.emit()
def _find_row(self, file_path: str) -> dict[str, object] | None:
for row in self._files:
if str(row.get("fileName", "")) == file_path:
return row
return None
def _sidecar_path(self, csv_path: Path) -> Path:
return csv_path.with_suffix(f"{csv_path.suffix}.json")
def _show_error(self, message: str) -> None:
QMessageBox.critical(None, "iSenseCloud", message)
def _show_info(self, message: str) -> None:
QMessageBox.information(None, "iSenseCloud", message)
# Backward-compatible alias while the QML dialog is still named SelectFileDialog.
SelectFileDialogModel = FileBrowser
+22 -2
View File
@@ -1,16 +1,36 @@
import sys import sys
from pathlib import Path from pathlib import Path
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtQuickControls2 import QQuickStyle
from PySide6.QtWidgets import QApplication
from backend.local_settings_model import LocalSettingsModel
from backend.file_browser import FileBrowser
# ===== Application Entry Point =====
if __name__ == "__main__": if __name__ == "__main__":
# ===== UI Style Setup =====
# Use a non-native controls style so our custom QML button backgrounds are supported.
QQuickStyle.setStyle("Basic")
# ===== Qt Bootstrap =====
# Minimal Qt bootstrap: load the ISC QML module and exit if it fails. # Minimal Qt bootstrap: load the ISC QML module and exit if it fails.
app = QGuiApplication(sys.argv) app = QApplication(sys.argv)
engine = QQmlApplicationEngine() engine = QQmlApplicationEngine()
# ===== Shared Models =====
settings_model = LocalSettingsModel()
select_file_dialog_model = FileBrowser()
settings_model.loadSettings()
engine.rootContext().setContextProperty("settingsModel", settings_model)
engine.rootContext().setContextProperty("file_browser", select_file_dialog_model)
# ===== QML Startup =====
engine.addImportPath(Path(__file__).parent) engine.addImportPath(Path(__file__).parent)
engine.loadFromModule("ISC", "Main") engine.loadFromModule("ISC", "Main")
# ===== Exit Handling =====
if not engine.rootObjects(): if not engine.rootObjects():
sys.exit(-1) sys.exit(-1)
+2 -1
View File
@@ -4,4 +4,5 @@ name = "PySide QtQuick Project"
# ===== PySide Build Inputs ===== # ===== PySide Build Inputs =====
[tool.pyside6-project] [tool.pyside6-project]
files = ["ISC/HomePage.qml", "ISC/Main.qml", "ISC/Theme.qml", "ISC/Tabs/SettingsTab.qml", "ISC/Tabs/qmldir", "ISC/qmldir", "main.py", "backend/local_settings.py", "backend/local_settings_model.py"] files = ["ISC/HomePage.qml", "ISC/Main.qml", "ISC/Tabs/SelectFileDialog.qml", "ISC/Tabs/SettingsTab.qml", "ISC/Tabs/qmldir", "ISC/Theme.qml", "ISC/qmldir", "backend/file_browser.py", "backend/local_settings.py", "backend/local_settings_model.py", "main.py"]