Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d33da2eab | |||
| fbb04eb2f7 | |||
| 9f8c6e1a4c |
+63
-10
@@ -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
|
||||
|
||||
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
|
||||
text: "Main content area"
|
||||
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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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)
|
||||
|
||||
|
||||
+2
-1
@@ -4,4 +4,5 @@ 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"]
|
||||
∑
|
||||
Reference in New Issue
Block a user