620 lines
20 KiB
QML
620 lines
20 KiB
QML
import QtQuick
|
|
import QtQuick.Controls
|
|
import QtQuick.Controls.impl
|
|
import QtQuick.Layouts
|
|
import Qt.labs.qmlmodels
|
|
import ISC
|
|
|
|
Dialog {
|
|
id: root
|
|
title: "Select File"
|
|
width: 980
|
|
height: 640
|
|
modal: true
|
|
padding: 16
|
|
topPadding: 0
|
|
parent: Overlay.overlay
|
|
x: Math.round(((parent ? parent.width : width) - width) / 2)
|
|
y: Math.round(((parent ? parent.height : height) - height) / 2)
|
|
|
|
signal fileChosen(string path)
|
|
|
|
// ===== Dialog State =====
|
|
property string selectedPath: ""
|
|
property var tableModel: []
|
|
property int selectedRow: -1
|
|
property int metadataEditRow: -1
|
|
readonly property string masterTypeFieldText: {
|
|
if (metadataEditRow < 0)
|
|
return "";
|
|
const rec = tableModel[metadataEditRow];
|
|
if (!rec)
|
|
return "";
|
|
return rec.masterType ?? "";
|
|
}
|
|
|
|
function openMetadataEditor(row) {
|
|
metadataEditRow = row;
|
|
const rec = tableModel[row];
|
|
if (!rec) {
|
|
metadataEditDialog.close();
|
|
return;
|
|
}
|
|
waferField.text = rec.wafer ?? "";
|
|
dateField.text = rec.date ?? "";
|
|
chamberField.text = rec.chamber ?? "";
|
|
if (!chamberField.text && settingsModel.chamberId)
|
|
chamberField.text = settingsModel.chamberId;
|
|
notesField.text = rec.notes ?? "";
|
|
filePathLabel.text = rec.fileName ?? "";
|
|
metadataEditDialog.open();
|
|
}
|
|
|
|
function applyMetadataFromEditor() {
|
|
if (metadataEditRow < 0)
|
|
return;
|
|
const rec = tableModel[metadataEditRow];
|
|
if (!rec) {
|
|
metadataEditDialog.close();
|
|
return;
|
|
}
|
|
file_browser.saveMetadata(rec.fileName ?? "", waferField.text, dateField.text, chamberField.text, notesField.text, false, "");
|
|
metadataEditDialog.close();
|
|
metadataEditRow = -1;
|
|
}
|
|
|
|
readonly property var headerTitles: ["Master", "Wafer", "Date", "Chamber", "Notes", "File", "Edit"]
|
|
// Build a path→family reverse map from settingsModel.masters.
|
|
// settingsModel is a QML context property; accessing it here makes the
|
|
// binding reactive — any mastersChanged signal re-evaluates normalizedRows.
|
|
function masterFamilyForPath(filePath) {
|
|
const m = settingsModel.masters;
|
|
for (var fam in m) {
|
|
if (m[fam] && m[fam] === filePath)
|
|
return fam;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
readonly property var normalizedRows: {
|
|
// Touch settingsModel.masters so QML tracks this dependency.
|
|
const _ = settingsModel.masters;
|
|
return (tableModel || []).map(function (row) {
|
|
row = row || {};
|
|
// settingsModel is the authoritative source; fall back to sidecar masterType.
|
|
const masterType = root.masterFamilyForPath(row.fileName ?? "") || row.masterType || "";
|
|
return {
|
|
masterType: masterType,
|
|
wafer: row.wafer ?? "",
|
|
date: row.date ?? "",
|
|
chamber: row.chamber ?? "",
|
|
edit: "",
|
|
notes: row.notes ?? "",
|
|
fileName: row.fileName ?? "",
|
|
highlight: row.highlight ?? false
|
|
};
|
|
});
|
|
}
|
|
|
|
// ===== Reusable Controls =====
|
|
component DialogActionButton: Button {
|
|
id: dialogButton
|
|
hoverEnabled: true
|
|
implicitHeight: 36
|
|
|
|
background: Rectangle {
|
|
radius: Theme.radiusSm
|
|
color: dialogButton.down ? Theme.buttonNeutralPressed : dialogButton.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground
|
|
border.width: Theme.borderThin
|
|
border.color: Theme.fieldBorder
|
|
}
|
|
|
|
contentItem: Text {
|
|
text: dialogButton.text
|
|
color: Theme.buttonNeutralText
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
elide: Text.ElideRight
|
|
font.bold: true
|
|
font.pixelSize: Theme.fontSm
|
|
}
|
|
}
|
|
|
|
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
|
|
Behavior on color {
|
|
ColorAnimation { duration: Theme.durationFast }
|
|
}
|
|
Behavior on border.color {
|
|
ColorAnimation { duration: Theme.durationFast }
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===== 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: Theme.fontLg
|
|
font.bold: true
|
|
color: Theme.headingColor
|
|
}
|
|
|
|
// Close button
|
|
DialogActionButton {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 12
|
|
implicitWidth: 32
|
|
implicitHeight: 32
|
|
onClicked: root.close()
|
|
contentItem: IconImage {
|
|
source: "icons/x.svg"
|
|
width: 14; height: 14
|
|
sourceSize.width: 14
|
|
sourceSize.height: 14
|
|
color: Theme.buttonNeutralText
|
|
anchors.centerIn: parent
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===== 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: Theme.fontSm
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===== 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: Theme.fontSm
|
|
color: Theme.headingColor
|
|
}
|
|
}
|
|
}
|
|
|
|
TableView {
|
|
id: tableView
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
clip: true
|
|
columnSpacing: 0
|
|
rowSpacing: 0
|
|
flickableDirection: Flickable.VerticalFlick
|
|
|
|
columnWidthProvider: function (col) {
|
|
// [master, wafer, date, chamber, notes, file (variable), edit]
|
|
const fixed = {
|
|
0: 48,
|
|
1: 90,
|
|
2: 170,
|
|
3: 120,
|
|
4: 180,
|
|
6: 56
|
|
};
|
|
if (col in fixed)
|
|
return fixed[col];
|
|
// col 5 (File) takes remaining width
|
|
const w = Math.max(400, tableView.width);
|
|
const used = 48 + 90 + 170 + 120 + 180 + 56;
|
|
return Math.max(120, w - used);
|
|
}
|
|
|
|
model: TableModel {
|
|
TableModelColumn {
|
|
display: "masterType"
|
|
}
|
|
TableModelColumn {
|
|
display: "wafer"
|
|
}
|
|
TableModelColumn {
|
|
display: "date"
|
|
}
|
|
TableModelColumn {
|
|
display: "chamber"
|
|
}
|
|
TableModelColumn {
|
|
display: "notes"
|
|
}
|
|
TableModelColumn {
|
|
display: "fileName"
|
|
}
|
|
TableModelColumn {
|
|
display: "edit"
|
|
}
|
|
rows: root.normalizedRows
|
|
}
|
|
|
|
delegate: Rectangle {
|
|
required property bool current
|
|
required property int row
|
|
required property int column
|
|
|
|
implicitHeight: 40
|
|
border.color: Theme.softBorder
|
|
border.width: Theme.borderThin
|
|
color: {
|
|
if (root.selectedRow === row)
|
|
return Theme.sideActiveBackground;
|
|
if (root.tableModel[row]?.highlight)
|
|
return Theme.subtleSectionBackground;
|
|
return row % 2 === 0 ? Theme.cardBackground : Theme.tone100;
|
|
}
|
|
|
|
// Master type indicator (col 0)
|
|
// Reads normalizedRows (which merges settingsModel.masters + sidecar).
|
|
Loader {
|
|
anchors.fill: parent
|
|
active: column === 0
|
|
sourceComponent: Text {
|
|
anchors.fill: parent
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
text: {
|
|
const mt = root.normalizedRows[row]?.masterType ?? "";
|
|
return mt || "—";
|
|
}
|
|
font.pixelSize: Theme.fontMd
|
|
font.bold: true
|
|
color: {
|
|
const mt = root.normalizedRows[row]?.masterType ?? "";
|
|
return mt ? Theme.primaryAccent : Theme.fieldPlaceholder;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Edit button (col 6)
|
|
Loader {
|
|
anchors.fill: parent
|
|
anchors.margins: 4
|
|
active: column === 6
|
|
sourceComponent: Button {
|
|
hoverEnabled: true
|
|
text: "Edit…"
|
|
font.pixelSize: Theme.fontSm
|
|
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: Theme.fontSm
|
|
}
|
|
}
|
|
}
|
|
|
|
// Text columns: Wafer (1), Date (2), File (5)
|
|
Loader {
|
|
anchors.fill: parent
|
|
active: column === 1 || column === 2 || column === 5
|
|
sourceComponent: Text {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 8
|
|
anchors.rightMargin: 8
|
|
verticalAlignment: Text.AlignVCenter
|
|
text: {
|
|
const record = root.tableModel[row] || {};
|
|
if (column === 1)
|
|
return record.wafer ?? "";
|
|
if (column === 2)
|
|
return record.date ?? "";
|
|
if (column === 5) {
|
|
const p = record.fileName ?? "";
|
|
return p.split("/").pop();
|
|
}
|
|
return "";
|
|
}
|
|
elide: Text.ElideRight
|
|
font.pixelSize: Theme.fontSm
|
|
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: Theme.fontSm
|
|
color: Theme.bodyColor
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
enabled: column !== 6
|
|
onClicked: {
|
|
root.selectedRow = row;
|
|
root.selectedPath = root.tableModel[row]?.fileName ?? "";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===== Bottom Bar =====
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 8
|
|
|
|
DialogActionButton {
|
|
text: "OK"
|
|
Layout.preferredWidth: 90
|
|
onClicked: {
|
|
root.fileChosen(root.selectedPath);
|
|
root.close();
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 32
|
|
Layout.minimumWidth: 120
|
|
clip: true
|
|
radius: Theme.radiusXs
|
|
color: Theme.fieldBackground
|
|
border.color: Theme.fieldBorder
|
|
border.width: Theme.borderThin
|
|
|
|
Text {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 10
|
|
anchors.rightMargin: 10
|
|
verticalAlignment: Text.AlignVCenter
|
|
text: root.selectedPath
|
|
elide: Text.ElideLeft
|
|
font.pixelSize: Theme.fontSm
|
|
color: root.selectedPath ? Theme.fieldText : Theme.fieldPlaceholder
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===== Metadata Editor Dialog =====
|
|
Dialog {
|
|
id: metadataEditDialog
|
|
parent: Overlay.overlay
|
|
modal: true
|
|
title: qsTr("Edit Metadata")
|
|
width: 520
|
|
height: 520
|
|
padding: 16
|
|
topPadding: 0
|
|
x: Math.round(((parent ? parent.width : width) - width) / 2)
|
|
y: Math.round(((parent ? parent.height : height) - height) / 2)
|
|
|
|
background: Rectangle {
|
|
radius: Theme.radiusLg
|
|
color: Theme.panelBackground
|
|
border.color: Theme.outerFrameBorder
|
|
border.width: Theme.borderStrong
|
|
}
|
|
|
|
header: Rectangle {
|
|
width: metadataEditDialog.width
|
|
height: 48
|
|
radius: Theme.radiusLg
|
|
color: Theme.subtleSectionBackground
|
|
border.color: Theme.innerFrameBorder
|
|
border.width: Theme.borderThin
|
|
|
|
Rectangle {
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.bottom: parent.bottom
|
|
height: parent.radius
|
|
color: parent.color
|
|
}
|
|
|
|
Label {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: 16
|
|
text: metadataEditDialog.title
|
|
font.pixelSize: Theme.fontLg
|
|
font.bold: true
|
|
color: Theme.headingColor
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
spacing: Theme.settingsGridSpacing
|
|
|
|
Label {
|
|
text: qsTr("File")
|
|
font.bold: true
|
|
color: Theme.headingColor
|
|
Layout.topMargin: 8
|
|
}
|
|
|
|
Text {
|
|
id: filePathLabel
|
|
Layout.fillWidth: true
|
|
Layout.maximumHeight: 56
|
|
Layout.bottomMargin: 16
|
|
wrapMode: Text.WrapAnywhere
|
|
maximumLineCount: 3
|
|
elide: Text.ElideRight
|
|
color: Theme.bodyColor
|
|
font.pixelSize: Theme.fontSm
|
|
}
|
|
|
|
Label {
|
|
text: qsTr("Wafer")
|
|
color: Theme.headingColor
|
|
}
|
|
DialogTextInput {
|
|
id: waferField
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 36
|
|
}
|
|
|
|
Label {
|
|
text: qsTr("Date")
|
|
color: Theme.headingColor
|
|
}
|
|
DialogTextInput {
|
|
id: dateField
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 36
|
|
}
|
|
|
|
Label {
|
|
text: qsTr("Chamber")
|
|
color: Theme.headingColor
|
|
}
|
|
DialogTextInput {
|
|
id: chamberField
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 36
|
|
}
|
|
|
|
Label {
|
|
text: qsTr("Notes")
|
|
color: Theme.headingColor
|
|
}
|
|
DialogTextInput {
|
|
id: notesField
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 36
|
|
}
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: 16
|
|
Layout.bottomMargin: 16
|
|
spacing: 8
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
DialogActionButton {
|
|
text: qsTr("Cancel")
|
|
Layout.preferredWidth: 90
|
|
onClicked: {
|
|
root.metadataEditRow = -1;
|
|
metadataEditDialog.close();
|
|
}
|
|
}
|
|
|
|
DialogActionButton {
|
|
text: qsTr("Save")
|
|
Layout.preferredWidth: 90
|
|
onClicked: root.applyMetadataFromEditor()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|