From 9f8c6e1a4ca875003eaf80df48f5843b318a1792 Mon Sep 17 00:00:00 2001 From: "Jack.Le" Date: Mon, 27 Apr 2026 13:56:36 -0700 Subject: [PATCH 1/2] 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. --- ISC/HomePage.qml | 81 ++++- ISC/Tabs/SelectFileDialog.qml | 553 ++++++++++++++++++++++++++++++++++ ISC/Tabs/SettingsTab.qml | 436 +++++++++++++++++++++++++++ pyproject.toml | 2 +- 4 files changed, 1057 insertions(+), 15 deletions(-) create mode 100644 ISC/Tabs/SelectFileDialog.qml create mode 100644 ISC/Tabs/SettingsTab.qml diff --git a/ISC/HomePage.qml b/ISC/HomePage.qml index 7731f5d..de31cfe 100644 --- a/ISC/HomePage.qml +++ b/ISC/HomePage.qml @@ -3,24 +3,33 @@ import QtQuick.Controls import QtQuick.Layouts import ISC +// ===== Home Workspace Shell ===== Rectangle { id: root anchors.fill: parent color: Theme.pageBackground clip: true + border.color: Theme.outerFrameBorder + border.width: Theme.borderStrong + // ===== Navigation Model ===== // Primary navigation shown in the left rail. 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. property var bottomTabs: ["Status", "Graph", "Data", "Wafer Map", "Compare", "Split", "Settings", "About"] + // ===== View State ===== property int selectedTabIndex: 0 + property int selectedSideActionIndex: 0 + // ===== Main Two-Column Layout ===== RowLayout { anchors.fill: parent spacing: 0 + // ===== Left Action Rail ===== // Left control rail. Rectangle { id: sideRail @@ -28,7 +37,7 @@ Rectangle { Layout.fillHeight: true color: Theme.sideRailBackground border.color: Theme.workspaceBorder - border.width: 1 + border.width: Theme.borderThin 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)) @@ -44,20 +53,41 @@ Rectangle { Button { id: control text: modelData + property bool isActive: index === root.selectedSideActionIndex Layout.fillWidth: true Layout.preferredHeight: Math.max(Theme.sideButtonMinHeight, sideRail.computedButtonHeight) + hoverEnabled: true + onClicked: root.selectedSideActionIndex = index background: Rectangle { - color: control.down ? Theme.buttonPressed : Theme.cardBackground - border.color: Theme.cardBorder + color: { + if (control.down) { + return Theme.buttonPressed; + } + if (control.isActive) { + return Theme.sideActiveBackground; + } + return "transparent"; + } + border.color: control.isActive ? Theme.cardBorder : "transparent" 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 { text: control.text - color: Theme.headingColor - font.bold: true + color: control.isActive ? Theme.headingColor : Theme.bodyColor + font.bold: control.isActive font.pixelSize: 18 wrapMode: Text.WordWrap horizontalAlignment: Text.AlignHCenter @@ -74,13 +104,14 @@ Rectangle { Layout.fillHeight: true color: Theme.workspaceBackground border.color: Theme.workspaceBorder - border.width: 1 + border.width: Theme.borderThin ColumnLayout { anchors.fill: parent anchors.margins: Theme.mainAreaPadding spacing: Theme.rightPaneGap + // ===== Active Tab Content Area ===== Rectangle { Layout.fillWidth: true Layout.fillHeight: true @@ -88,23 +119,45 @@ Rectangle { color: Theme.responseBackground border.color: Theme.responseBorder border.width: 1 - radius: 8 + radius: Theme.radiusMd - Label { - anchors.centerIn: parent - text: "Main content area" - color: Theme.bodyColor - font.pixelSize: 20 + 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 { + anchors.centerIn: parent + visible: parent.tabName !== "Settings" + text: parent.tabName + " content" + color: Theme.bodyColor + font.pixelSize: 20 + } + } + } } } // Keep the tab strip evenly distributed across the footer. + // ===== Footer Tab Strip ===== Rectangle { Layout.fillWidth: true Layout.preferredHeight: Theme.tabBarHeight + (Theme.tabBarPadding * 2) color: Theme.tabBarBackground border.color: Theme.workspaceBorder - border.width: 1 + border.width: Theme.borderThin RowLayout { anchors.fill: parent diff --git a/ISC/Tabs/SelectFileDialog.qml b/ISC/Tabs/SelectFileDialog.qml new file mode 100644 index 0000000..fe06231 --- /dev/null +++ b/ISC/Tabs/SelectFileDialog.qml @@ -0,0 +1,553 @@ +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() + } + } + } + } +} diff --git a/ISC/Tabs/SettingsTab.qml b/ISC/Tabs/SettingsTab.qml new file mode 100644 index 0000000..caa1ff4 --- /dev/null +++ b/ISC/Tabs/SettingsTab.qml @@ -0,0 +1,436 @@ +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 + } + +} diff --git a/pyproject.toml b/pyproject.toml index 65d13df..27a3e10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,4 +4,4 @@ name = "PySide QtQuick Project" # ===== PySide Build Inputs ===== [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"] From fbb04eb2f7aa340a7749c9bfade29878483defa0 Mon Sep 17 00:00:00 2001 From: "Jack.Le" Date: Mon, 27 Apr 2026 14:28:08 -0700 Subject: [PATCH 2/2] 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. --- ISC/Tabs/SelectFileDialog.qml | 151 +++++++++++++++------- ISC/Tabs/SettingsTab.qml | 5 +- ISC/Theme.qml | 156 +++++++++++++++-------- backend/file_browser.py | 230 ++++++++++++++++++++++++++++++++++ main.py | 24 +++- pyproject.toml | 1 + 6 files changed, 466 insertions(+), 101 deletions(-) create mode 100644 backend/file_browser.py diff --git a/ISC/Tabs/SelectFileDialog.qml b/ISC/Tabs/SelectFileDialog.qml index fe06231..e4cb828 100644 --- a/ISC/Tabs/SelectFileDialog.qml +++ b/ISC/Tabs/SelectFileDialog.qml @@ -41,7 +41,8 @@ Dialog { } function applyMetadataFromEditor() { - if (metadataEditRow < 0) return; + if (metadataEditRow < 0) + return; const rec = tableModel[metadataEditRow]; if (!rec) { metadataEditDialog.close(); @@ -56,14 +57,14 @@ Dialog { 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 ?? "", + 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 }; }) @@ -76,9 +77,7 @@ Dialog { background: Rectangle { radius: Theme.radiusSm - color: dialogButton.down ? Theme.buttonNeutralPressed - : dialogButton.hovered ? Theme.buttonNeutralHover - : Theme.buttonNeutralBackground + color: dialogButton.down ? Theme.buttonNeutralPressed : dialogButton.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground border.width: Theme.borderThin border.color: Theme.fieldBorder } @@ -236,8 +235,17 @@ Dialog { 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]; + 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; @@ -245,14 +253,30 @@ Dialog { } 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" } + 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 } @@ -265,8 +289,10 @@ Dialog { border.color: Theme.softBorder border.width: Theme.borderThin color: { - if (root.selectedRow === row) return Theme.sideActiveBackground; - if (root.tableModel[row]?.highlight) return Theme.subtleSectionBackground; + if (root.selectedRow === row) + return Theme.sideActiveBackground; + if (root.tableModel[row]?.highlight) + return Theme.subtleSectionBackground; return row % 2 === 0 ? Theme.cardBackground : Theme.tone100; } @@ -297,9 +323,7 @@ Dialog { background: Rectangle { radius: Theme.radiusXs - color: parent.down ? Theme.buttonNeutralPressed - : parent.hovered ? Theme.buttonNeutralHover - : Theme.buttonNeutralBackground + color: parent.down ? Theme.buttonNeutralPressed : parent.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground border.width: Theme.borderThin border.color: Theme.fieldBorder } @@ -325,15 +349,14 @@ Dialog { font.pixelSize: 12 onClicked: { const record = root.tableModel[row]; - if (!record) return; + 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 + color: parent.down ? Theme.buttonNeutralPressed : parent.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground border.width: Theme.borderThin border.color: Theme.fieldBorder } @@ -359,9 +382,12 @@ Dialog { 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 ?? ""; + if (column === 1) + return record.wafer ?? ""; + if (column === 2) + return record.date ?? ""; + if (column === 5) + return record.fileName ?? ""; return ""; } elide: Text.ElideRight @@ -381,7 +407,8 @@ Dialog { verticalAlignment: Text.AlignVCenter text: { const record = root.tableModel[row] || {}; - if (column === 3) return record.chamber ?? ""; + if (column === 3) + return record.chamber ?? ""; return record.notes ?? ""; } elide: Text.ElideRight @@ -496,7 +523,11 @@ Dialog { anchors.fill: parent spacing: Theme.settingsGridSpacing - Label { text: qsTr("File"); font.bold: true; color: Theme.headingColor } + Label { + text: qsTr("File") + font.bold: true + color: Theme.headingColor + } Text { id: filePathLabel @@ -509,17 +540,45 @@ Dialog { font.pixelSize: 12 } - Label { text: qsTr("Wafer"); color: Theme.headingColor } - DialogTextInput { id: waferField; Layout.fillWidth: true; Layout.preferredHeight: 36 } + 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("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("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 } + Label { + text: qsTr("Notes") + color: Theme.headingColor + } + DialogTextInput { + id: notesField + Layout.fillWidth: true + Layout.preferredHeight: 36 + } CheckBox { id: selectedField @@ -531,7 +590,9 @@ Dialog { Layout.topMargin: 8 spacing: 8 - Item { Layout.fillWidth: true } + Item { + Layout.fillWidth: true + } DialogActionButton { text: qsTr("Cancel") diff --git a/ISC/Tabs/SettingsTab.qml b/ISC/Tabs/SettingsTab.qml index caa1ff4..4ad4158 100644 --- a/ISC/Tabs/SettingsTab.qml +++ b/ISC/Tabs/SettingsTab.qml @@ -419,7 +419,9 @@ Item { opacity: (settingsScroll.ScrollBar.vertical.position <= 0 && settingsScroll.contentHeight > settingsScroll.height) ? (scrollHintTimer.running ? 1 : 0) : 0 Behavior on opacity { - NumberAnimation { duration: 400 } + NumberAnimation { + duration: 400 + } } } } @@ -432,5 +434,4 @@ Item { repeat: false onTriggered: scrollHint.visible = false } - } diff --git a/ISC/Theme.qml b/ISC/Theme.qml index 8ef58fe..6dd5b0a 100644 --- a/ISC/Theme.qml +++ b/ISC/Theme.qml @@ -2,44 +2,120 @@ pragma Singleton 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 { - // ===== Theme Mode ===== - // Toggle this to preview the same shell in light mode. - property bool isDarkMode: true + // ── 1. Mode ────────────────────────────────────────────────────────────── + property bool isDarkMode: false - // ===== Surface Colors ===== - // Core application surfaces. - readonly property color pageBackground: isDarkMode ? "#11161c" : "#eef4f7" - readonly property color cardBackground: isDarkMode ? "#1b232d" : "#ffffff" - readonly property color workspaceBackground: isDarkMode ? "#121923" : "#ffffff" - readonly property color sideRailBackground: isDarkMode ? "#18202a" : "#f5f7fa" - readonly property color responseBackground: isDarkMode ? "#18212a" : "#f9fbfc" + // ── 2. Tone palette (base values; consume via semantic tokens below) ───── + readonly property color tone100: isDarkMode ? "#111111" : "#FAFAFA" + readonly property color tone200: isDarkMode ? "#1A1A1A" : "#F2F2F2" + readonly property color tone300: isDarkMode ? "#242424" : "#E8E8E8" + readonly property color toneText: isDarkMode ? "#F2F2F2" : "#111111" + readonly property color toneMute: isDarkMode ? "#A8A8A8" : "#8A8A8A" + readonly property color toneBorder: isDarkMode ? "#2B2B2B" : "#D8D8D8" - // Shared border and text colors. - readonly property color cardBorder: isDarkMode ? "#334155" : "#cbd5e1" - readonly property color workspaceBorder: isDarkMode ? "#334155" : "#d7dde5" - readonly property color responseBorder: isDarkMode ? "#334155" : "#d5e0e8" + // ── 3. Surfaces ────────────────────────────────────────────────────────── + readonly property color pageBackground: tone100 + readonly property color cardBackground: tone200 + 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" - readonly property color bodyColor: isDarkMode ? "#afbdca" : "#415a6b" - readonly property color buttonPressed: isDarkMode ? "#214d60" : "#17576c" - readonly property color statusSuccessColor: isDarkMode ? "#7cd67e" : "#1f7a33" - readonly property color statusErrorColor: isDarkMode ? "#ff8a80" : "#c62828" - readonly property color statusWarningColor: isDarkMode ? "#f3c677" : "#9a6700" + // ── 4. Borders ─────────────────────────────────────────────────────────── + readonly property color cardBorder: toneBorder + readonly property color responseBorder: toneBorder + readonly property color workspaceBorder: toneBorder + readonly property color innerFrameBorder: toneBorder + 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 sideRailSpacing: 14 readonly property int sideButtonHeight: 146 readonly property int sideButtonMinHeight: 88 - readonly property int panelPadding: 12 - - // ===== Main Panel Spacing ===== - // Main content spacing. - readonly property int mainAreaPadding: 10 - readonly property int rightPaneGap: 6 + // Tab bar layout 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 settingsOuterMargin: 40 readonly property int settingsSectionSpacing: 20 @@ -48,28 +124,4 @@ QtObject { readonly property int settingsButtonHeight: 40 readonly property int settingsActionWidth: 200 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" } diff --git a/backend/file_browser.py b/backend/file_browser.py new file mode 100644 index 0000000..d057558 --- /dev/null +++ b/backend/file_browser.py @@ -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[^-]+)-(?P\d{8})(?:_(?P\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 diff --git a/main.py b/main.py index b9401f7..7a54800 100644 --- a/main.py +++ b/main.py @@ -1,16 +1,36 @@ import sys from pathlib import Path -from PySide6.QtGui import QGuiApplication 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__": + # ===== 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. - app = QGuiApplication(sys.argv) + app = QApplication(sys.argv) 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.loadFromModule("ISC", "Main") + # ===== Exit Handling ===== if not engine.rootObjects(): sys.exit(-1) diff --git a/pyproject.toml b/pyproject.toml index 27a3e10..8e30281 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,3 +5,4 @@ name = "PySide QtQuick Project" # ===== PySide Build Inputs ===== [tool.pyside6-project] 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"] +∑ \ No newline at end of file