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.
This commit is contained in:
@@ -41,7 +41,8 @@ Dialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyMetadataFromEditor() {
|
function applyMetadataFromEditor() {
|
||||||
if (metadataEditRow < 0) return;
|
if (metadataEditRow < 0)
|
||||||
|
return;
|
||||||
const rec = tableModel[metadataEditRow];
|
const rec = tableModel[metadataEditRow];
|
||||||
if (!rec) {
|
if (!rec) {
|
||||||
metadataEditDialog.close();
|
metadataEditDialog.close();
|
||||||
@@ -76,9 +77,7 @@ Dialog {
|
|||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
radius: Theme.radiusSm
|
radius: Theme.radiusSm
|
||||||
color: dialogButton.down ? Theme.buttonNeutralPressed
|
color: dialogButton.down ? Theme.buttonNeutralPressed : dialogButton.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground
|
||||||
: dialogButton.hovered ? Theme.buttonNeutralHover
|
|
||||||
: Theme.buttonNeutralBackground
|
|
||||||
border.width: Theme.borderThin
|
border.width: Theme.borderThin
|
||||||
border.color: Theme.fieldBorder
|
border.color: Theme.fieldBorder
|
||||||
}
|
}
|
||||||
@@ -236,8 +235,17 @@ Dialog {
|
|||||||
|
|
||||||
columnWidthProvider: function (col) {
|
columnWidthProvider: function (col) {
|
||||||
// [select, wafer, date, chamber, notes, file (variable), edit, save]
|
// [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 };
|
const fixed = {
|
||||||
if (fixed.hasOwnProperty(col)) return fixed[col];
|
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
|
// col 5 (File) takes remaining width
|
||||||
const w = Math.max(400, tableView.width);
|
const w = Math.max(400, tableView.width);
|
||||||
const used = 40 + 90 + 170 + 120 + 180 + 56 + 56;
|
const used = 40 + 90 + 170 + 120 + 180 + 56 + 56;
|
||||||
@@ -245,14 +253,30 @@ Dialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model: TableModel {
|
model: TableModel {
|
||||||
TableModelColumn { display: "select" }
|
TableModelColumn {
|
||||||
TableModelColumn { display: "wafer" }
|
display: "select"
|
||||||
TableModelColumn { display: "date" }
|
}
|
||||||
TableModelColumn { display: "chamber" }
|
TableModelColumn {
|
||||||
TableModelColumn { display: "notes" }
|
display: "wafer"
|
||||||
TableModelColumn { display: "fileName" }
|
}
|
||||||
TableModelColumn { display: "edit" }
|
TableModelColumn {
|
||||||
TableModelColumn { display: "save" }
|
display: "date"
|
||||||
|
}
|
||||||
|
TableModelColumn {
|
||||||
|
display: "chamber"
|
||||||
|
}
|
||||||
|
TableModelColumn {
|
||||||
|
display: "notes"
|
||||||
|
}
|
||||||
|
TableModelColumn {
|
||||||
|
display: "fileName"
|
||||||
|
}
|
||||||
|
TableModelColumn {
|
||||||
|
display: "edit"
|
||||||
|
}
|
||||||
|
TableModelColumn {
|
||||||
|
display: "save"
|
||||||
|
}
|
||||||
rows: root.normalizedRows
|
rows: root.normalizedRows
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,8 +289,10 @@ Dialog {
|
|||||||
border.color: Theme.softBorder
|
border.color: Theme.softBorder
|
||||||
border.width: Theme.borderThin
|
border.width: Theme.borderThin
|
||||||
color: {
|
color: {
|
||||||
if (root.selectedRow === row) return Theme.sideActiveBackground;
|
if (root.selectedRow === row)
|
||||||
if (root.tableModel[row]?.highlight) return Theme.subtleSectionBackground;
|
return Theme.sideActiveBackground;
|
||||||
|
if (root.tableModel[row]?.highlight)
|
||||||
|
return Theme.subtleSectionBackground;
|
||||||
return row % 2 === 0 ? Theme.cardBackground : Theme.tone100;
|
return row % 2 === 0 ? Theme.cardBackground : Theme.tone100;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,9 +323,7 @@ Dialog {
|
|||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
radius: Theme.radiusXs
|
radius: Theme.radiusXs
|
||||||
color: parent.down ? Theme.buttonNeutralPressed
|
color: parent.down ? Theme.buttonNeutralPressed : parent.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground
|
||||||
: parent.hovered ? Theme.buttonNeutralHover
|
|
||||||
: Theme.buttonNeutralBackground
|
|
||||||
border.width: Theme.borderThin
|
border.width: Theme.borderThin
|
||||||
border.color: Theme.fieldBorder
|
border.color: Theme.fieldBorder
|
||||||
}
|
}
|
||||||
@@ -325,15 +349,14 @@ Dialog {
|
|||||||
font.pixelSize: 12
|
font.pixelSize: 12
|
||||||
onClicked: {
|
onClicked: {
|
||||||
const record = root.tableModel[row];
|
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);
|
file_browser.saveMetadata(record.fileName ?? "", record.wafer ?? "", record.date ?? "", record.chamber ?? "", record.notes ?? "", record.selected ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
radius: Theme.radiusXs
|
radius: Theme.radiusXs
|
||||||
color: parent.down ? Theme.buttonNeutralPressed
|
color: parent.down ? Theme.buttonNeutralPressed : parent.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground
|
||||||
: parent.hovered ? Theme.buttonNeutralHover
|
|
||||||
: Theme.buttonNeutralBackground
|
|
||||||
border.width: Theme.borderThin
|
border.width: Theme.borderThin
|
||||||
border.color: Theme.fieldBorder
|
border.color: Theme.fieldBorder
|
||||||
}
|
}
|
||||||
@@ -359,9 +382,12 @@ Dialog {
|
|||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
text: {
|
text: {
|
||||||
const record = root.tableModel[row] || {};
|
const record = root.tableModel[row] || {};
|
||||||
if (column === 1) return record.wafer ?? "";
|
if (column === 1)
|
||||||
if (column === 2) return record.date ?? "";
|
return record.wafer ?? "";
|
||||||
if (column === 5) return record.fileName ?? "";
|
if (column === 2)
|
||||||
|
return record.date ?? "";
|
||||||
|
if (column === 5)
|
||||||
|
return record.fileName ?? "";
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
@@ -381,7 +407,8 @@ Dialog {
|
|||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
text: {
|
text: {
|
||||||
const record = root.tableModel[row] || {};
|
const record = root.tableModel[row] || {};
|
||||||
if (column === 3) return record.chamber ?? "";
|
if (column === 3)
|
||||||
|
return record.chamber ?? "";
|
||||||
return record.notes ?? "";
|
return record.notes ?? "";
|
||||||
}
|
}
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
@@ -496,7 +523,11 @@ Dialog {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: Theme.settingsGridSpacing
|
spacing: Theme.settingsGridSpacing
|
||||||
|
|
||||||
Label { text: qsTr("File"); font.bold: true; color: Theme.headingColor }
|
Label {
|
||||||
|
text: qsTr("File")
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.headingColor
|
||||||
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: filePathLabel
|
id: filePathLabel
|
||||||
@@ -509,17 +540,45 @@ Dialog {
|
|||||||
font.pixelSize: 12
|
font.pixelSize: 12
|
||||||
}
|
}
|
||||||
|
|
||||||
Label { text: qsTr("Wafer"); color: Theme.headingColor }
|
Label {
|
||||||
DialogTextInput { id: waferField; Layout.fillWidth: true; Layout.preferredHeight: 36 }
|
text: qsTr("Wafer")
|
||||||
|
color: Theme.headingColor
|
||||||
|
}
|
||||||
|
DialogTextInput {
|
||||||
|
id: waferField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 36
|
||||||
|
}
|
||||||
|
|
||||||
Label { text: qsTr("Date"); color: Theme.headingColor }
|
Label {
|
||||||
DialogTextInput { id: dateField; Layout.fillWidth: true; Layout.preferredHeight: 36 }
|
text: qsTr("Date")
|
||||||
|
color: Theme.headingColor
|
||||||
|
}
|
||||||
|
DialogTextInput {
|
||||||
|
id: dateField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 36
|
||||||
|
}
|
||||||
|
|
||||||
Label { text: qsTr("Chamber"); color: Theme.headingColor }
|
Label {
|
||||||
DialogTextInput { id: chamberField; Layout.fillWidth: true; Layout.preferredHeight: 36 }
|
text: qsTr("Chamber")
|
||||||
|
color: Theme.headingColor
|
||||||
|
}
|
||||||
|
DialogTextInput {
|
||||||
|
id: chamberField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 36
|
||||||
|
}
|
||||||
|
|
||||||
Label { text: qsTr("Notes"); color: Theme.headingColor }
|
Label {
|
||||||
DialogTextInput { id: notesField; Layout.fillWidth: true; Layout.preferredHeight: 36 }
|
text: qsTr("Notes")
|
||||||
|
color: Theme.headingColor
|
||||||
|
}
|
||||||
|
DialogTextInput {
|
||||||
|
id: notesField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 36
|
||||||
|
}
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: selectedField
|
id: selectedField
|
||||||
@@ -531,7 +590,9 @@ Dialog {
|
|||||||
Layout.topMargin: 8
|
Layout.topMargin: 8
|
||||||
spacing: 8
|
spacing: 8
|
||||||
|
|
||||||
Item { Layout.fillWidth: true }
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
DialogActionButton {
|
DialogActionButton {
|
||||||
text: qsTr("Cancel")
|
text: qsTr("Cancel")
|
||||||
|
|||||||
@@ -419,7 +419,9 @@ Item {
|
|||||||
opacity: (settingsScroll.ScrollBar.vertical.position <= 0 && settingsScroll.contentHeight > settingsScroll.height) ? (scrollHintTimer.running ? 1 : 0) : 0
|
opacity: (settingsScroll.ScrollBar.vertical.position <= 0 && settingsScroll.contentHeight > settingsScroll.height) ? (scrollHintTimer.running ? 1 : 0) : 0
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
NumberAnimation { duration: 400 }
|
NumberAnimation {
|
||||||
|
duration: 400
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -432,5 +434,4 @@ Item {
|
|||||||
repeat: false
|
repeat: false
|
||||||
onTriggered: scrollHint.visible = false
|
onTriggered: scrollHint.visible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+104
-52
@@ -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"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ name = "PySide QtQuick Project"
|
|||||||
# ===== PySide Build Inputs =====
|
# ===== PySide Build Inputs =====
|
||||||
[tool.pyside6-project]
|
[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"]
|
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"]
|
||||||
|
∑
|
||||||
Reference in New Issue
Block a user