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 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: 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) { // [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: 14 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: 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 } } } // 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: 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 !== 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: 13 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: 15 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: 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 } 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() } } } } }