From ca158a794636351f9a2d296a2aef9f5f6e4d52c3 Mon Sep 17 00:00:00 2001 From: jack Date: Thu, 9 Jul 2026 12:49:10 -0700 Subject: [PATCH] refactor(qml): extract sidebar panels and data tab comparison cards to modular components --- src/pygui/ISC/HomePage.qml | 366 +---------- src/pygui/ISC/Tabs/SelectFileDialog.qml | 619 ------------------ src/pygui/ISC/Tabs/components/AppToolTip.qml | 25 + .../Tabs/components/ComparisonChartCard.qml | 260 ++++++++ .../Tabs/components/ComparisonSidePanel.qml | 239 +++++++ .../ISC/Tabs/components/ConnectionFooter.qml | 102 +++ .../ISC/Tabs/components/EditableScrubStat.qml | 113 ++++ .../ISC/Tabs/components/OverlapMapCard.qml | 86 +++ src/pygui/ISC/Tabs/components/PanelBox.qml | 10 + .../ISC/Tabs/components/PanelCheckBox.qml | 43 ++ src/pygui/ISC/Tabs/components/PanelSlider.qml | 19 + .../ISC/Tabs/components/SectionTitle.qml | 12 + .../Tabs/components/StatusActionsPanel.qml | 103 +++ .../ISC/Tabs/components/UtilityFooter.qml | 112 ++++ src/pygui/ISC/Tabs/components/qmldir | 16 +- src/pygui/ISC/Tabs/qmldir | 1 - 16 files changed, 1156 insertions(+), 970 deletions(-) delete mode 100644 src/pygui/ISC/Tabs/SelectFileDialog.qml create mode 100644 src/pygui/ISC/Tabs/components/AppToolTip.qml create mode 100644 src/pygui/ISC/Tabs/components/ComparisonChartCard.qml create mode 100644 src/pygui/ISC/Tabs/components/ComparisonSidePanel.qml create mode 100644 src/pygui/ISC/Tabs/components/ConnectionFooter.qml create mode 100644 src/pygui/ISC/Tabs/components/EditableScrubStat.qml create mode 100644 src/pygui/ISC/Tabs/components/OverlapMapCard.qml create mode 100644 src/pygui/ISC/Tabs/components/PanelBox.qml create mode 100644 src/pygui/ISC/Tabs/components/PanelCheckBox.qml create mode 100644 src/pygui/ISC/Tabs/components/PanelSlider.qml create mode 100644 src/pygui/ISC/Tabs/components/SectionTitle.qml create mode 100644 src/pygui/ISC/Tabs/components/StatusActionsPanel.qml create mode 100644 src/pygui/ISC/Tabs/components/UtilityFooter.qml diff --git a/src/pygui/ISC/HomePage.qml b/src/pygui/ISC/HomePage.qml index 3bcde4b..62c238e 100644 --- a/src/pygui/ISC/HomePage.qml +++ b/src/pygui/ISC/HomePage.qml @@ -17,20 +17,6 @@ Rectangle { border.color: Theme.outerFrameBorder border.width: Theme.borderStrong - // ===== Global ToolTip Styling ===== - ToolTip.toolTip.background: Rectangle { - color: Theme.cardBackground - border.color: Theme.sideBorder - border.width: 1 - radius: Theme.radiusXs - } - ToolTip.toolTip.contentItem: Text { - text: ToolTip.toolTip.text - color: Theme.headingColor - font.pixelSize: Theme.fontXs - font.family: Theme.uiFontFamily - } - // ===== View State ===== property int selectedTabIndex: 0 onSelectedTabIndexChanged: { @@ -420,43 +406,6 @@ Rectangle { } } - component PanelCheckBox: CheckBox { - id: toggle - indicator: Rectangle { - implicitWidth: 18 - implicitHeight: 18 - x: toggle.leftPadding - y: parent.height / 2 - height / 2 - radius: Theme.radiusXs - color: toggle.checked ? Theme.primaryAccent : "transparent" - border.width: Theme.borderThin - border.color: toggle.checked ? Theme.primaryAccent : Theme.fieldBorder - - Behavior on color { ColorAnimation { duration: Theme.durationFast } } - Behavior on border.color { ColorAnimation { duration: Theme.durationFast } } - - IconImage { - anchors.centerIn: parent - source: "Tabs/icons/check.svg" - width: 12; height: 12 - sourceSize.width: 12 - sourceSize.height: 12 - color: Theme.panelBackground - opacity: toggle.checked ? 1.0 : 0.0 - Behavior on opacity { NumberAnimation { duration: Theme.durationFast } } - } - } - - contentItem: Text { - text: toggle.text - color: Theme.headingColor - font.family: Theme.uiFontFamily - verticalAlignment: Text.AlignVCenter - leftPadding: toggle.indicator.width + toggle.spacing - font.pixelSize: toggle.font.pixelSize || Theme.fontXs - } - } - ColumnLayout { id: runCompCol anchors.fill: parent @@ -498,8 +447,10 @@ Rectangle { color: Theme.bodyColor anchors.centerIn: parent } - ToolTip.visible: resetRunBtn.hovered - ToolTip.text: "Reset comparison" + AppToolTip { + visible: resetRunBtn.hovered + text: "Reset comparison" + } } } @@ -562,11 +513,13 @@ Rectangle { readonly property bool sameFile: !!dt && fileA !== "" && fileA === dt.compareFileB readonly property string gateError: !!dt ? dt.familyGateError : "" enabled: !!dt && fileA !== "" && dt.compareFileB !== "" && !sameFile && !dt.comparing && !masterMissing && gateError === "" - ToolTip.visible: (sameFile || masterMissing || gateError !== "") && hovered - ToolTip.text: masterMissing - ? "No master file registered for this wafer type (Settings → Master Files)" - : gateError !== "" ? gateError - : "Choose two different runs to compare" + AppToolTip { + visible: (compareRunBtn.sameFile || compareRunBtn.masterMissing || compareRunBtn.gateError !== "") && compareRunBtn.hovered + text: compareRunBtn.masterMissing + ? "No master file registered for this wafer type (Settings → Master Files)" + : compareRunBtn.gateError !== "" ? compareRunBtn.gateError + : "Choose two different runs to compare" + } onClicked: { dt.comparing = true dt.clearResults() @@ -617,100 +570,9 @@ Rectangle { currentIndex: root.selectedTabIndex === 0 ? 0 : 1 // ── Status tab: Hardware Actions + Log Actions ────────── - ColumnLayout { - spacing: Theme.sideRailSpacing - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: hwActionsCol.implicitHeight + 28 - radius: Theme.sidePanelRadius - border.color: Theme.sideBorder - border.width: 1 - color: Theme.sidePanelBackground - - ColumnLayout { - id: hwActionsCol - anchors.fill: parent - anchors.margins: 14 - spacing: 6 - - Label { - text: "HARDWARE ACTIONS" - color: Theme.sideMutedText - font.family: Theme.uiFontFamily - font.pixelSize: Theme.fontSm - font.letterSpacing: 1.5 - font.bold: true - } - - RailActionButton { - label: "DETECT WAFER" - iconSource: "../icons/detect.svg" - Layout.fillWidth: true - enabled: !deviceController.operationInProgress - onClicked: root._doDetect() - } - - RailActionButton { - label: "READ MEMORY" - iconSource: "../icons/read.svg" - Layout.fillWidth: true - enabled: root.waferDetected - onClicked: deviceController.readMemoryAsync() - } - - RailActionButton { - label: "ERASE MEMORY" - iconSource: "../icons/erase.svg" - Layout.fillWidth: true - destructive: true - enabled: root.waferDetected - onClicked: deviceController.eraseMemory( - deviceController.selectedPort) - } - } - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: logActionsCol.implicitHeight + 28 - radius: Theme.sidePanelRadius - border.color: Theme.sideBorder - border.width: 1 - color: Theme.sidePanelBackground - - ColumnLayout { - id: logActionsCol - anchors.fill: parent - anchors.margins: 14 - spacing: 6 - - Label { - text: "LOG ACTIONS" - color: Theme.sideMutedText - font.family: Theme.uiFontFamily - font.pixelSize: Theme.fontSm - font.letterSpacing: 1.5 - font.bold: true - } - - RailActionButton { - label: "REFRESH SESSION" - iconSource: "../icons/refresh.svg" - Layout.fillWidth: true - onClicked: deviceController.clearSession() - } - - RailActionButton { - label: "CLEAR LOG" - iconSource: "../icons/x.svg" - Layout.fillWidth: true - onClicked: deviceController.clearActivityLog() - } - } - } - - Item { Layout.fillHeight: true } + StatusActionsPanel { + Layout.fillWidth: true + Layout.fillHeight: true } // ── Map tab: Source file browser ────────────── @@ -728,208 +590,14 @@ Rectangle { } // ── BOX 3: Hardware Status Footer ────────────────────────── - // Not relevant on the Data tab — Run Comparison takes its place above. - Rectangle { + ConnectionFooter { Layout.fillWidth: true - Layout.preferredHeight: Theme.sideFooterConnection - visible: root.selectedTabIndex !== 1 - radius: Theme.sidePanelRadius - border.color: Theme.sideBorder - border.width: 1 - color: Theme.sidePanelBackground - clip: true - - ColumnLayout { - anchors.fill: parent - anchors.margins: 14 - spacing: 6 - - // Row 1: Title - Label { - text: "CONNECTION FLOW" - color: Theme.sideMutedText - font.family: Theme.uiFontFamily - font.pixelSize: Theme.fontSm - font.letterSpacing: 1.5 - font.bold: true - Layout.fillWidth: true - elide: Text.ElideRight - } - - // Row 2: Port / descriptive text - Label { - Layout.fillWidth: true - text: { - if (deviceController.connectionStatus === "Connected") - return deviceController.selectedPort || "Connected" - if (deviceController.selectedPort) - return deviceController.selectedPort - return "No port selected" - } - color: Theme.bodyColor - font.family: Theme.uiFontFamily - font.pixelSize: Theme.fontSm - elide: Text.ElideRight - } - - Item { Layout.fillHeight: true } - - // Row 3: Mode chip left, status badge bottom-right - RowLayout { - spacing: 6 - Layout.fillWidth: true - - Rectangle { - width: modeChipLabel.implicitWidth + 12 - height: 18 - radius: 4 - color: Theme.fieldBackground - border.color: Theme.sideBorder - border.width: 1 - - Label { - id: modeChipLabel - anchors.centerIn: parent - text: streamController.mode - ? streamController.mode.toUpperCase() : "REVIEW" - color: Theme.sideMutedText - font.family: Theme.uiFontFamily - font.pixelSize: Theme.fontXs - font.bold: true - font.letterSpacing: 0.8 - } - } - - Item { Layout.fillWidth: true } - - Rectangle { - height: 18 - width: badgeLabel.implicitWidth + 14 - radius: 9 - color: { - var s = deviceController.connectionStatus - if (s === "Connected") return Theme.statusSuccessColor - if (s === "Disconnected") return Theme.statusErrorColor - return Theme.statusWarningColor - } - - Label { - id: badgeLabel - anchors.centerIn: parent - text: deviceController.connectionStatus || "Disconnected" - color: Theme.statusBadgeText - font.pixelSize: Theme.fontXs - font.bold: true - } - } - } - } } // ── BOX 4: Utility Buttons ───────────────────────────────── - Rectangle { + UtilityFooter { + id: utilFooter Layout.fillWidth: true - Layout.preferredHeight: Theme.sideFooterUtility - radius: Theme.sidePanelRadius - border.color: Theme.sideBorder - border.width: 1 - color: Theme.sidePanelBackground - - RowLayout { - anchors.fill: parent - anchors.margins: 4 - spacing: 4 - - Button { - id: settingsBtn - flat: true - Layout.fillWidth: true - Layout.fillHeight: true - icon.source: "Tabs/icons/settings.svg" - onClicked: settingsPopup.open() - - background: Rectangle { - radius: 8 - color: settingsBtn.hovered - ? Theme.sideActiveBackground : "transparent" - } - - contentItem: Row { - spacing: 6 - anchors.centerIn: parent - - IconImage { - anchors.verticalCenter: parent.verticalCenter - width: 16 - height: 16 - source: "Tabs/icons/settings.svg" - sourceSize.width: 16 - sourceSize.height: 16 - color: Theme.headingColor - opacity: 0.75 - } - - Label { - anchors.verticalCenter: parent.verticalCenter - text: "SETTINGS" - color: Theme.headingColor - font.family: Theme.uiFontFamily - font.pixelSize: Theme.fontXs - font.bold: true - font.letterSpacing: 1.2 - opacity: 0.8 - } - } - } - - Rectangle { - width: 1 - height: 24 - color: Theme.sideBorder - } - - Button { - id: aboutBtn - flat: true - Layout.fillWidth: true - Layout.fillHeight: true - icon.source: "Tabs/icons/about.svg" - onClicked: aboutDialog.open() - - background: Rectangle { - radius: 8 - color: aboutBtn.hovered - ? Theme.sideActiveBackground : "transparent" - } - - contentItem: Row { - spacing: 6 - anchors.centerIn: parent - - IconImage { - anchors.verticalCenter: parent.verticalCenter - width: 16 - height: 16 - source: "Tabs/icons/about.svg" - sourceSize.width: 16 - sourceSize.height: 16 - color: Theme.headingColor - opacity: 0.75 - } - - Label { - anchors.verticalCenter: parent.verticalCenter - text: "ABOUT" - color: Theme.headingColor - font.family: Theme.uiFontFamily - font.pixelSize: Theme.fontXs - font.bold: true - font.letterSpacing: 1.2 - opacity: 0.8 - } - } - } - } } } } diff --git a/src/pygui/ISC/Tabs/SelectFileDialog.qml b/src/pygui/ISC/Tabs/SelectFileDialog.qml deleted file mode 100644 index 4928be5..0000000 --- a/src/pygui/ISC/Tabs/SelectFileDialog.qml +++ /dev/null @@ -1,619 +0,0 @@ -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() - } - } - } - } -} diff --git a/src/pygui/ISC/Tabs/components/AppToolTip.qml b/src/pygui/ISC/Tabs/components/AppToolTip.qml new file mode 100644 index 0000000..cdc669d --- /dev/null +++ b/src/pygui/ISC/Tabs/components/AppToolTip.qml @@ -0,0 +1,25 @@ +import QtQuick +import QtQuick.Controls +import ISC + +// Single seam for tooltip chrome — every hover tip in the app instantiates +// this instead of styling QtQuick.Controls ToolTip.background/contentItem +// inline at each call site. +ToolTip { + id: root + delay: 400 + font.pixelSize: Theme.fontXs + font.family: Theme.uiFontFamily + + contentItem: Text { + text: root.text + color: Theme.headingColor + font: root.font + } + background: Rectangle { + color: Theme.cardBackground + border.color: Theme.sideBorder + border.width: 1 + radius: Theme.radiusXs + } +} diff --git a/src/pygui/ISC/Tabs/components/ComparisonChartCard.qml b/src/pygui/ISC/Tabs/components/ComparisonChartCard.qml new file mode 100644 index 0000000..cdff0d1 --- /dev/null +++ b/src/pygui/ISC/Tabs/components/ComparisonChartCard.qml @@ -0,0 +1,260 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.impl +import QtQuick.Layouts +import ISC + +// Curve overlay chart card with the alignment timeline scrubber. +PanelBox { + id: card + + required property var trendDataA + required property var trendDataB + required property int seriesLen + required property bool hasSeries + required property bool comparing + + // Scrubber position — the internal Slider is private; consumers + // read/write frameIndex and call focusScrubber(). + property alias frameIndex: scrubber.value + + function focusScrubber() { scrubber.forceActiveFocus() } + + implicitHeight: chartCol.implicitHeight + 24 + + ColumnLayout { + id: chartCol + anchors.fill: parent + anchors.margins: 12 + spacing: 8 + + RowLayout { + Layout.fillWidth: true + SectionTitle { + text: "CURVE OVERLAY" + Layout.fillWidth: true + } + Row { + spacing: 16 + visible: card.hasSeries + Row { + spacing: 4 + Rectangle { width: 12; height: 3; radius: 1.5; color: Theme.primaryAccent; anchors.verticalCenter: parent.verticalCenter } + Label { text: "Run A"; font.pixelSize: Theme.fontXs; color: Theme.bodyColor } + + } + Row { + spacing: 4 + Rectangle { width: 12; height: 3; radius: 1.5; color: Theme.themeSkill; anchors.verticalCenter: parent.verticalCenter } + Label { text: "Run B"; font.pixelSize: Theme.fontXs; color: Theme.bodyColor } + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 200 + color: Theme.fieldBackground + border.color: Theme.cardBorder + border.width: 1 + radius: Theme.radiusSm + clip: true + + Canvas { + id: overlayCanvas + anchors.fill: parent + visible: card.hasSeries + + readonly property real padLeft: 50 + readonly property real padRight: 16 + readonly property real padTop: 24 + readonly property real padBottom: 36 + + // Nice tick step (1/2/5 × 10^k) — same scheme as TrendChartItem, + // so labels land on round numbers instead of raw data min/max. + function niceStep(rawStep) { + var mag = Math.pow(10, Math.floor(Math.log10(rawStep))) + return [1, 2, 5, 10].map(function (s) { return s * mag }) + .find(function (s) { return s >= rawStep }) + } + + function drawGrid(ctx, minV, maxV, yStep, dataLen) { + var plotW = width - padLeft - padRight + var plotH = height - padTop - padBottom + var range = (maxV - minV) || 1 + + ctx.strokeStyle = Theme.chartGridLine + ctx.lineWidth = 1 + ctx.fillStyle = Theme.chartAxisText + ctx.font = "10px " + Theme.uiFontFamily + ctx.textAlign = "right" + + var ySteps = Math.max(1, Math.round(range / yStep)) + var yDec = Math.max(0, -Math.floor(Math.log10(yStep))) + for (var yi = 0; yi <= ySteps; yi++) { + var yFrac = yi / ySteps + var yPx = padTop + plotH * yFrac + var yVal = maxV - yFrac * range + + ctx.beginPath() + ctx.moveTo(padLeft, yPx) + ctx.lineTo(padLeft + plotW, yPx) + ctx.stroke() + + ctx.fillText(yVal.toFixed(yDec), padLeft - 6, yPx + 3) + } + + ctx.textAlign = "center" + var xMax = dataLen - 1 + if (xMax > 0) { + var xStep = niceStep(xMax / 5 || 1) + for (var xIdx = 0; xIdx <= xMax; xIdx += xStep) { + var xPx = padLeft + (xIdx / xMax) * plotW + ctx.fillText(xIdx.toString(), xPx, height - 20) + } + } + + ctx.save() + ctx.translate(14, padTop + plotH / 2) + ctx.rotate(-Math.PI / 2) + ctx.textAlign = "center" + ctx.font = "12px " + Theme.uiFontFamily + ctx.fillText("Celcius (°C)", 0, 0) + ctx.restore() + + ctx.textAlign = "center" + ctx.font = "12px " + Theme.uiFontFamily + ctx.fillText("Frame", padLeft + plotW / 2, height - 4) + } + + function drawSeries(ctx, series, minV, range, color, dataLen) { + // x is mapped against the shared frame domain (dataLen), not the + // series' own length — otherwise a shorter run's line stretches to + // fill the whole plot instead of stopping where its data ends. + if (series.length < 2) return + var plotW = width - padLeft - padRight + var plotH = height - padTop - padBottom + + ctx.strokeStyle = color + ctx.lineWidth = 2 + ctx.beginPath() + for (var i = 0; i < series.length; i++) { + var x = padLeft + (i / (dataLen - 1)) * plotW + var y = padTop + plotH - ((series[i] - minV) / range) * plotH + if (i === 0) ctx.moveTo(x, y) + else ctx.lineTo(x, y) + } + ctx.stroke() + } + + function drawScrubMarker(ctx) { + if (card.seriesLen < 2) return + var plotW = width - padLeft - padRight + var x = padLeft + (scrubber.value / (card.seriesLen - 1)) * plotW + ctx.strokeStyle = Theme.primaryAccent + ctx.lineWidth = 1.5 + ctx.setLineDash([3, 3]) + ctx.beginPath() + ctx.moveTo(x, padTop) + ctx.lineTo(x, height - padBottom) + ctx.stroke() + ctx.setLineDash([]) + } + + onPaint: { + var ctx = getContext("2d") + ctx.reset() + if (!card.hasSeries) return + var all = card.trendDataA.concat(card.trendDataB) + var minV = Math.min.apply(null, all) + var maxV = Math.max.apply(null, all) + var padding = (maxV - minV) * 0.05 + minV -= padding + maxV += padding + // Snap bounds to nice-step multiples -> stable round-number axis + var yStep = niceStep((maxV - minV) / 5 || 1) + minV = Math.floor(minV / yStep) * yStep + maxV = Math.ceil(maxV / yStep) * yStep + var range = (maxV - minV) || 1 + + drawGrid(ctx, minV, maxV, yStep, card.seriesLen) + drawSeries(ctx, card.trendDataA, minV, range, Theme.primaryAccent, card.seriesLen) + drawSeries(ctx, card.trendDataB, minV, range, Theme.themeSkill, card.seriesLen) + drawScrubMarker(ctx) + } + + Connections { + target: card + function onTrendDataAChanged() { overlayCanvas.requestPaint() } + function onTrendDataBChanged() { overlayCanvas.requestPaint() } + } + Connections { + target: scrubber + function onValueChanged() { overlayCanvas.requestPaint() } + } + } + + // Empty state + ColumnLayout { + anchors.centerIn: parent + visible: !card.hasSeries + spacing: 8 + + IconImage { + Layout.alignment: Qt.AlignHCenter + visible: !card.comparing + source: "../icons/bar-chart.svg" + width: 32; height: 32 + sourceSize.width: 32 + sourceSize.height: 32 + color: Theme.sideMutedText + } + Label { + Layout.alignment: Qt.AlignHCenter + text: card.comparing ? "Running DTW comparison…" : "Select two runs and run comparison" + color: Theme.bodyColor + font.pixelSize: Theme.fontSm + } + Label { + Layout.alignment: Qt.AlignHCenter + visible: !card.comparing + text: "Click a run box on the right, then select a file from the left panel." + color: Theme.sideMutedText + font.pixelSize: Theme.fontXs + } + } + } + + // ── Alignment Timeline Scrubber ──────────── + ColumnLayout { + Layout.fillWidth: true + spacing: 4 + visible: card.hasSeries + + Rectangle { + Layout.fillWidth: true + height: 1 + color: Theme.cardBorder + } + + SectionTitle { + text: "TIMELINE SCRUBBER" + Layout.fillWidth: true + Layout.topMargin: 8 + } + + Slider { + id: scrubber + Layout.fillWidth: true + leftPadding: overlayCanvas.padLeft + rightPadding: overlayCanvas.padRight + from: 0 + to: Math.max(1, card.seriesLen - 1) + stepSize: 1 + value: 0 + } + + + } + } +} diff --git a/src/pygui/ISC/Tabs/components/ComparisonSidePanel.qml b/src/pygui/ISC/Tabs/components/ComparisonSidePanel.qml new file mode 100644 index 0000000..fb5e25b --- /dev/null +++ b/src/pygui/ISC/Tabs/components/ComparisonSidePanel.qml @@ -0,0 +1,239 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import ISC + +// Right-column bento stack: DTW readout, overlap settings, frame/time +// controls, and scrubber readout. Pure view — all comparison state lives +// on DataTab; scrubber writes go up via frameIndexRequested. +ColumnLayout { + id: panel + + required property real warpingDistance + required property int frameOffset + required property real frameOffsetSeconds + required property real maxSensorDeviation + required property int seriesLen + required property bool hasSeries + required property var trendDataA + required property var trendDataB + required property var timeDataA + required property var timeDataB + required property var frameForTime // function(seconds) -> frame index + required property real frameIndex // bound from ComparisonChartCard.frameIndex + + // Overlap settings outputs (consumed by OverlapMapCard via DataTab) + property alias overlapEnabled: enableOverlapCheck.checked + property alias blendAmount: blendSlider.value + + signal frameIndexRequested(real idx) + signal scrubberFocusRequested() + + spacing: 12 + + // Bento 3: DTW Comparison Readout + PanelBox { + Layout.fillWidth: true + Layout.preferredHeight: readoutCol.implicitHeight + 24 + + ColumnLayout { + id: readoutCol + anchors.fill: parent + anchors.margins: 12 + spacing: 8 + + SectionTitle { text: "DTW READOUT" } + + ReadoutStat { + label: "DTW ALIGNMENT DISTANCE" + value: panel.warpingDistance >= 0 ? panel.warpingDistance.toFixed(2) : "--" + valueColor: panel.warpingDistance >= 0 + ? (panel.warpingDistance < 50 ? Theme.metricGood : panel.warpingDistance < 100 ? Theme.metricWarn : Theme.metricBad) + : Theme.sideMutedText + } + ReadoutStat { + label: "TEMPORAL FRAME OFFSET" + value: panel.warpingDistance >= 0 + ? ((panel.frameOffset >= 0 ? "+" : "") + panel.frameOffset + " frames / " + + (panel.frameOffsetSeconds >= 0 ? "+" : "") + panel.frameOffsetSeconds.toFixed(1) + "s") + : "--" + valueColor: panel.warpingDistance >= 0 ? Theme.headingColor : Theme.sideMutedText + } + ReadoutStat { + label: "MAX SENSOR DEVIATION" + value: panel.maxSensorDeviation >= 0 ? panel.maxSensorDeviation.toFixed(2) + "°C" : "--" + valueColor: panel.maxSensorDeviation >= 0 + ? (panel.maxSensorDeviation < 1.0 ? Theme.metricGood : panel.maxSensorDeviation < 3.0 ? Theme.metricWarn : Theme.metricBad) + : Theme.sideMutedText + } + } + } + + // Bento 2: Overlap Map Settings + PanelBox { + Layout.fillWidth: true + Layout.preferredHeight: overlapCol.implicitHeight + 24 + + ColumnLayout { + id: overlapCol + anchors.fill: parent + anchors.margins: 12 + spacing: 8 + + SectionTitle { text: "OVERLAP SETTINGS" } + + PanelCheckBox { + id: enableOverlapCheck + Layout.fillWidth: true + checked: true + text: "Enable Overlap Map" + font.pixelSize: Theme.fontSm + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 6 + visible: enableOverlapCheck.checked + + Rectangle { + Layout.fillWidth: true + height: 1 + color: Theme.cardBorder + } + + RowLayout { + Layout.fillWidth: true + spacing: 6 + Label { + text: "Blend" + font.pixelSize: Theme.fontSm + color: Theme.bodyColor + Layout.preferredWidth: 40 + } + PanelSlider { + id: blendSlider + Layout.fillWidth: true + from: 0; to: 100; value: 50 + stepSize: 1 + } + Label { + text: Math.round(blendSlider.value) + "%" + font.pixelSize: Theme.fontSm + color: Theme.sideMutedText + Layout.preferredWidth: 30 + } + } + + // Diff color legend + Row { + Layout.fillWidth: true + spacing: 10 + Row { + spacing: 3 + Rectangle { width: 8; height: 8; radius: 4; color: Theme.sensorLow; anchors.verticalCenter: parent.verticalCenter } + Label { text: "B cooler"; font.pixelSize: Theme.fontSm; color: Theme.sideMutedText } + } + Row { + spacing: 3 + Rectangle { width: 8; height: 8; radius: 4; color: Theme.sensorInRange; anchors.verticalCenter: parent.verticalCenter } + Label { text: "Match"; font.pixelSize: Theme.fontSm; color: Theme.sideMutedText } + } + Row { + spacing: 3 + Rectangle { width: 8; height: 8; radius: 4; color: Theme.sensorHigh; anchors.verticalCenter: parent.verticalCenter } + Label { text: "B hotter"; font.pixelSize: Theme.fontSm; color: Theme.sideMutedText } + } + } + } + } + } + + // Bento: Frame / Time Controls + PanelBox { + Layout.fillWidth: true + Layout.preferredHeight: frameTimeCol.implicitHeight + 32 + + ColumnLayout { + id: frameTimeCol + anchors.fill: parent + anchors.margins: 16 + spacing: 20 + + SectionTitle { text: "FRAME / TIME" } + + EditableScrubStat { + label: "Frame" + value: scrubReadout.scrubIdx + unit: "/ " + Math.max(0, panel.seriesLen - 1) + (Math.max(0, panel.seriesLen - 1) <= 1 ? " frame" : " frames") + editable: panel.hasSeries + onValueEdited: (newValue) => { + var maxVal = Math.max(0, panel.seriesLen - 1) + var val = Math.max(0, Math.min(newValue, maxVal)) + panel.frameIndexRequested(val) + } + onEditingDone: panel.scrubberFocusRequested() + } + + EditableScrubStat { + label: "Time" + value: scrubReadout.scrubTime + unit: "/ " + scrubReadout.maxTime.toFixed(1) + "s" + editable: panel.hasSeries + onValueEdited: (newValue) => { + var closestIdx = panel.frameForTime(newValue) + panel.frameIndexRequested(closestIdx) + } + onEditingDone: panel.scrubberFocusRequested() + } + } + } + + // Bento: Scrub Readout + PanelBox { + Layout.fillWidth: true + Layout.preferredHeight: scrubReadout.implicitHeight + 24 + + ColumnLayout { + id: scrubReadout + anchors.fill: parent + anchors.margins: 12 + spacing: 8 + + readonly property int scrubIdx: Math.round(panel.frameIndex) + readonly property bool hasA: scrubIdx < panel.trendDataA.length + readonly property bool hasB: scrubIdx < panel.trendDataB.length + readonly property real tempA: hasA ? panel.trendDataA[scrubIdx] : 0 + readonly property real tempB: hasB ? panel.trendDataB[scrubIdx] : 0 + readonly property real scrubTime: { + var t = scrubIdx < panel.timeDataA.length ? panel.timeDataA[scrubIdx] + : (scrubIdx < panel.timeDataB.length ? panel.timeDataB[scrubIdx] : 0) + return t + } + readonly property real maxTime: { + var ta = panel.timeDataA.length > 0 ? panel.timeDataA[panel.timeDataA.length - 1] : 0 + var tb = panel.timeDataB.length > 0 ? panel.timeDataB[panel.timeDataB.length - 1] : 0 + return Math.max(ta, tb) + } + + SectionTitle { text: "SCRUBBER READOUT" } + + ReadoutStat { + label: "Temp (Run A)" + value: scrubReadout.hasA ? scrubReadout.tempA.toFixed(1) + "°C" : "none" + valueColor: Theme.primaryAccent + } + ReadoutStat { + label: "Temp (Run B)" + value: scrubReadout.hasB ? scrubReadout.tempB.toFixed(1) + "°C" : "none" + valueColor: Theme.themeSkill + } + ReadoutStat { + label: "Difference" + value: (scrubReadout.hasA && scrubReadout.hasB) + ? Math.abs(scrubReadout.tempA - scrubReadout.tempB).toFixed(1) + "°C" : "none" + } + } + } + + Item { Layout.fillHeight: true } +} diff --git a/src/pygui/ISC/Tabs/components/ConnectionFooter.qml b/src/pygui/ISC/Tabs/components/ConnectionFooter.qml new file mode 100644 index 0000000..dabd8ca --- /dev/null +++ b/src/pygui/ISC/Tabs/components/ConnectionFooter.qml @@ -0,0 +1,102 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import ISC + +// ── BOX 3: Hardware Status Footer ────────────────────────── +Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: Theme.sideFooterConnection + visible: root.selectedTabIndex !== 1 + radius: Theme.sidePanelRadius + border.color: Theme.sideBorder + border.width: 1 + color: Theme.sidePanelBackground + clip: true + + ColumnLayout { + anchors.fill: parent + anchors.margins: 14 + spacing: 6 + + // Row 1: Title + Label { + text: "CONNECTION FLOW" + color: Theme.sideMutedText + font.family: Theme.uiFontFamily + font.pixelSize: Theme.fontSm + font.letterSpacing: 1.5 + font.bold: true + Layout.fillWidth: true + elide: Text.ElideRight + } + + // Row 2: Port / descriptive text + Label { + Layout.fillWidth: true + text: { + if (deviceController.connectionStatus === "Connected") + return deviceController.selectedPort || "Connected" + if (deviceController.selectedPort) + return deviceController.selectedPort + return "No port selected" + } + color: Theme.bodyColor + font.family: Theme.uiFontFamily + font.pixelSize: Theme.fontSm + elide: Text.ElideRight + } + + Item { Layout.fillHeight: true } + + // Row 3: Mode chip left, status badge bottom-right + RowLayout { + spacing: 6 + Layout.fillWidth: true + + Rectangle { + width: modeChipLabel.implicitWidth + 12 + height: 18 + radius: 4 + color: Theme.fieldBackground + border.color: Theme.sideBorder + border.width: 1 + + Label { + id: modeChipLabel + anchors.centerIn: parent + text: streamController.mode + ? streamController.mode.toUpperCase() : "REVIEW" + color: Theme.sideMutedText + font.family: Theme.uiFontFamily + font.pixelSize: Theme.fontXs + font.bold: true + font.letterSpacing: 0.8 + } + } + + Item { Layout.fillWidth: true } + + Rectangle { + height: 18 + width: badgeLabel.implicitWidth + 14 + radius: 9 + color: { + var s = deviceController.connectionStatus + if (s === "Connected") return Theme.statusSuccessColor + if (s === "Disconnected") return Theme.statusErrorColor + return Theme.statusWarningColor + } + + Label { + id: badgeLabel + anchors.centerIn: parent + text: deviceController.connectionStatus || "Disconnected" + color: Theme.statusBadgeText + font.pixelSize: Theme.fontXs + font.bold: true + } + } + } + } +} diff --git a/src/pygui/ISC/Tabs/components/EditableScrubStat.qml b/src/pygui/ISC/Tabs/components/EditableScrubStat.qml new file mode 100644 index 0000000..b003d16 --- /dev/null +++ b/src/pygui/ISC/Tabs/components/EditableScrubStat.qml @@ -0,0 +1,113 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import ISC + +// Editable stat box for scrubber readout (styled to match DTW ReadoutCard) +Rectangle { + id: edStat + property string label + property real value + property string unit + property string extraText: "" + property bool editable: true + signal valueEdited(real newValue) + // Fires when the input field commits (Enter/focus loss), even if the + // entered text was invalid — callers use it to hand focus back to the + // timeline scrubber, matching the pre-extraction behavior. + signal editingDone() + + Layout.fillWidth: true + implicitHeight: edStatRow.implicitHeight + 16 + radius: Theme.radiusSm + color: Theme.subtleSectionBackground + border.color: (edStat.editable && inputField.activeFocus) ? Theme.fieldBorderFocus : Theme.cardBorder + border.width: (edStat.editable && inputField.activeFocus) ? Theme.borderStrong : Theme.borderThin + + RowLayout { + id: edStatRow + anchors.fill: parent + anchors.margins: 8 + spacing: 6 + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + Label { + text: edStat.label.toUpperCase() + font.pixelSize: Theme.fontXs + font.bold: true + font.letterSpacing: 0.2 + color: Theme.sideMutedText + } + RowLayout { + spacing: 4 + TextField { + id: inputField + readOnly: !edStat.editable + hoverEnabled: edStat.editable + font.pixelSize: Theme.fontLg + font.bold: true + font.family: Theme.codeFontFamily + color: edStat.editable ? Theme.headingColor : Theme.sideMutedText + verticalAlignment: TextInput.AlignVCenter + horizontalAlignment: TextInput.AlignHCenter + background: Rectangle { + visible: edStat.editable + color: Theme.fieldBackground + radius: Theme.radiusXs + border.color: inputField.activeFocus ? Theme.fieldBorderFocus : (inputField.hovered ? Theme.fieldBorderFocus : Theme.fieldBorder) + border.width: 1 + } + padding: 0 + leftPadding: edStat.editable ? 6 : 0 + rightPadding: edStat.editable ? 6 : 0 + topPadding: edStat.editable ? 5 : 0 + bottomPadding: edStat.editable ? 5 : 0 + selectByMouse: edStat.editable + inputMethodHints: edStat.label === "Time" ? Qt.ImhFormattedNumbersOnly : Qt.ImhDigitsOnly + Layout.preferredWidth: edStat.editable ? Math.max(36, contentWidth + 12) : contentWidth + Layout.preferredHeight: edStat.editable ? 32 : implicitHeight + + text: edStat.value.toFixed(edStat.label === "Time" ? 1 : 0) + + onEditingFinished: { + var val = parseFloat(text) + if (!isNaN(val)) { + edStat.valueEdited(val) + } + text = edStat.value.toFixed(edStat.label === "Time" ? 1 : 0) + edStat.editingDone() + } + + Connections { + target: edStat + function onValueChanged() { + if (!inputField.activeFocus) { + inputField.text = edStat.value.toFixed(edStat.label === "Time" ? 1 : 0) + } + } + } + } + Label { + text: edStat.unit + font.pixelSize: Theme.fontLg + font.bold: true + font.family: Theme.codeFontFamily + color: edStat.editable ? Theme.headingColor : Theme.sideMutedText + Layout.alignment: Qt.AlignVCenter + } + } + } + + Label { + visible: edStat.extraText !== "" + text: edStat.extraText + font.pixelSize: Theme.fontLg + font.bold: true + font.family: Theme.codeFontFamily + color: Theme.headingColor + Layout.alignment: Qt.AlignVCenter + } + } +} diff --git a/src/pygui/ISC/Tabs/components/OverlapMapCard.qml b/src/pygui/ISC/Tabs/components/OverlapMapCard.qml new file mode 100644 index 0000000..a27ef34 --- /dev/null +++ b/src/pygui/ISC/Tabs/components/OverlapMapCard.qml @@ -0,0 +1,86 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.impl +import QtQuick.Layouts +import ISC +import ISC.Wafer + +// Wafer overlap map card — renders per-sensor Run A/Run B temperature +// differences at the current scrub position. +PanelBox { + id: card + + required property var sensorLayout + required property var diffValues + required property var diffBands // function(values) -> band thresholds + required property string waferShape + required property real waferSize + required property bool overlapEnabled + required property real blendAmount // 0–100, percent + + ColumnLayout { + anchors.fill: parent + anchors.margins: 12 + spacing: 8 + + RowLayout { + Layout.fillWidth: true + SectionTitle { + text: "WAFER OVERLAP" + Layout.fillWidth: true + } + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: Theme.fieldBackground + border.color: Theme.cardBorder + border.width: 1 + radius: Theme.radiusSm + clip: true + + WaferMapItem { + id: overlapMapItem + anchors.fill: parent + visible: card.sensorLayout.length > 0 && card.overlapEnabled + sensors: card.sensorLayout + values: card.diffValues + bands: card.diffBands(overlapMapItem.values) + shape: card.waferShape + size: card.waferSize + target: 0 + margin: 1.0 + blend: card.blendAmount / 100 + showLabels: true + ringColor: Theme.waferRingColor + axisColor: Theme.waferAxisColor + lowColor: Theme.sensorLow + inRangeColor: Theme.sensorInRange + highColor: Theme.sensorHigh + textColor: Theme.headingColor + } + + ColumnLayout { + anchors.centerIn: parent + visible: card.sensorLayout.length === 0 || !card.overlapEnabled + spacing: 4 + IconImage { + Layout.alignment: Qt.AlignHCenter + source: "../icons/map.svg" + width: 24; height: 24 + sourceSize.width: 24 + sourceSize.height: 24 + color: Theme.sideMutedText + } + Label { + Layout.alignment: Qt.AlignHCenter + text: !card.overlapEnabled ? "Overlap map disabled" + : "No sensor map available" + color: Theme.bodyColor + font.pixelSize: Theme.fontXs + } + } + } + } +} diff --git a/src/pygui/ISC/Tabs/components/PanelBox.qml b/src/pygui/ISC/Tabs/components/PanelBox.qml new file mode 100644 index 0000000..b80ac1d --- /dev/null +++ b/src/pygui/ISC/Tabs/components/PanelBox.qml @@ -0,0 +1,10 @@ +import QtQuick +import ISC + +// Card container for the Data tab bento boxes. +Rectangle { + color: Theme.sidePanelBackground + border.color: Theme.sideBorder + border.width: 1 + radius: Theme.sidePanelRadius +} diff --git a/src/pygui/ISC/Tabs/components/PanelCheckBox.qml b/src/pygui/ISC/Tabs/components/PanelCheckBox.qml new file mode 100644 index 0000000..ae76910 --- /dev/null +++ b/src/pygui/ISC/Tabs/components/PanelCheckBox.qml @@ -0,0 +1,43 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.impl +import ISC + +// Checkbox styled to match MapTab's right-rail ReadoutPanel (square accent +// indicator + check icon) instead of the platform-default CheckBox look. +CheckBox { + id: toggle + indicator: Rectangle { + implicitWidth: 18 + implicitHeight: 18 + x: toggle.leftPadding + y: parent.height / 2 - height / 2 + radius: Theme.radiusXs + color: toggle.checked ? Theme.primaryAccent : "transparent" + border.width: Theme.borderThin + border.color: toggle.checked ? Theme.primaryAccent : Theme.fieldBorder + + Behavior on color { ColorAnimation { duration: Theme.durationFast } } + Behavior on border.color { ColorAnimation { duration: Theme.durationFast } } + + IconImage { + anchors.centerIn: parent + source: "../icons/check.svg" + width: 12; height: 12 + sourceSize.width: 12 + sourceSize.height: 12 + color: Theme.panelBackground + opacity: toggle.checked ? 1.0 : 0.0 + Behavior on opacity { NumberAnimation { duration: Theme.durationFast } } + } + } + + contentItem: Text { + text: toggle.text + color: Theme.headingColor + font.family: Theme.uiFontFamily + verticalAlignment: Text.AlignVCenter + leftPadding: toggle.indicator.width + toggle.spacing + font.pixelSize: toggle.font.pixelSize || Theme.fontXs + } +} diff --git a/src/pygui/ISC/Tabs/components/PanelSlider.qml b/src/pygui/ISC/Tabs/components/PanelSlider.qml new file mode 100644 index 0000000..cf4bd2c --- /dev/null +++ b/src/pygui/ISC/Tabs/components/PanelSlider.qml @@ -0,0 +1,19 @@ +import QtQuick +import QtQuick.Controls +import ISC + +// Slider styled to match MapTab's right-rail heatmap slider (circular +// accent handle that grows slightly on press). +Slider { + id: panelSlider + handle: Rectangle { + x: panelSlider.leftPadding + panelSlider.visualPosition * (panelSlider.availableWidth - width) + y: panelSlider.topPadding + panelSlider.availableHeight / 2 - height / 2 + implicitWidth: 18 + implicitHeight: 18 + radius: 9 + color: Theme.primaryAccent + scale: panelSlider.pressed ? 1.15 : 1.0 + Behavior on scale { NumberAnimation { duration: Theme.durationFast; easing.type: Theme.easeStandard } } + } +} diff --git a/src/pygui/ISC/Tabs/components/SectionTitle.qml b/src/pygui/ISC/Tabs/components/SectionTitle.qml new file mode 100644 index 0000000..a37569f --- /dev/null +++ b/src/pygui/ISC/Tabs/components/SectionTitle.qml @@ -0,0 +1,12 @@ +import QtQuick +import QtQuick.Controls +import ISC + +// Uppercase card heading — shared by the Data tab cards. +Label { + font.family: Theme.uiFontFamily + font.pixelSize: Theme.fontSm + font.letterSpacing: 1.5 + font.bold: true + color: Theme.sideMutedText +} diff --git a/src/pygui/ISC/Tabs/components/StatusActionsPanel.qml b/src/pygui/ISC/Tabs/components/StatusActionsPanel.qml new file mode 100644 index 0000000..74fdd85 --- /dev/null +++ b/src/pygui/ISC/Tabs/components/StatusActionsPanel.qml @@ -0,0 +1,103 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.impl +import QtQuick.Layouts +import ISC +import ISC.Tabs.components + +// ── Status tab: Hardware Actions + Log Actions ────────── +ColumnLayout { + spacing: Theme.sideRailSpacing + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: hwActionsCol.implicitHeight + 28 + radius: Theme.sidePanelRadius + border.color: Theme.sideBorder + border.width: 1 + color: Theme.sidePanelBackground + + ColumnLayout { + id: hwActionsCol + anchors.fill: parent + anchors.margins: 14 + spacing: 6 + + Label { + text: "HARDWARE ACTIONS" + color: Theme.sideMutedText + font.family: Theme.uiFontFamily + font.pixelSize: Theme.fontSm + font.letterSpacing: 1.5 + font.bold: true + } + + RailActionButton { + label: "DETECT WAFER" + iconSource: "../icons/detect.svg" + Layout.fillWidth: true + enabled: !deviceController.operationInProgress + onClicked: root._doDetect() + } + + RailActionButton { + label: "READ MEMORY" + iconSource: "../icons/read.svg" + Layout.fillWidth: true + enabled: root.waferDetected + onClicked: deviceController.readMemoryAsync() + } + + RailActionButton { + label: "ERASE MEMORY" + iconSource: "../icons/erase.svg" + Layout.fillWidth: true + destructive: true + enabled: root.waferDetected + onClicked: deviceController.eraseMemory( + deviceController.selectedPort) + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: logActionsCol.implicitHeight + 28 + radius: Theme.sidePanelRadius + border.color: Theme.sideBorder + border.width: 1 + color: Theme.sidePanelBackground + + ColumnLayout { + id: logActionsCol + anchors.fill: parent + anchors.margins: 14 + spacing: 6 + + Label { + text: "LOG ACTIONS" + color: Theme.sideMutedText + font.family: Theme.uiFontFamily + font.pixelSize: Theme.fontSm + font.letterSpacing: 1.5 + font.bold: true + } + + RailActionButton { + label: "REFRESH SESSION" + iconSource: "../icons/refresh.svg" + Layout.fillWidth: true + onClicked: deviceController.clearSession() + } + + RailActionButton { + label: "CLEAR LOG" + iconSource: "../icons/x.svg" + Layout.fillWidth: true + onClicked: deviceController.clearActivityLog() + } + } + } + + Item { Layout.fillHeight: true } +} diff --git a/src/pygui/ISC/Tabs/components/UtilityFooter.qml b/src/pygui/ISC/Tabs/components/UtilityFooter.qml new file mode 100644 index 0000000..560729a --- /dev/null +++ b/src/pygui/ISC/Tabs/components/UtilityFooter.qml @@ -0,0 +1,112 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.impl +import QtQuick.Layouts +import ISC + +// ── BOX 4: Utility Buttons ───────────────────────────────── +Rectangle { + id: utilFooter + Layout.fillWidth: true + Layout.preferredHeight: Theme.sideFooterUtility + radius: Theme.sidePanelRadius + border.color: Theme.sideBorder + border.width: 1 + color: Theme.sidePanelBackground + + RowLayout { + anchors.fill: parent + anchors.margins: 4 + spacing: 4 + + Button { + id: settingsBtn + flat: true + Layout.fillWidth: true + Layout.fillHeight: true + icon.source: "../icons/settings.svg" + onClicked: utilFooter.parent.settingsPopup.open() + + background: Rectangle { + radius: 8 + color: settingsBtn.hovered + ? Theme.sideActiveBackground : "transparent" + } + + contentItem: Row { + spacing: 6 + anchors.centerIn: parent + + IconImage { + anchors.verticalCenter: parent.verticalCenter + width: 16 + height: 16 + source: "../icons/settings.svg" + sourceSize.width: 16 + sourceSize.height: 16 + color: Theme.headingColor + opacity: 0.75 + } + + Label { + anchors.verticalCenter: parent.verticalCenter + text: "SETTINGS" + color: Theme.headingColor + font.family: Theme.uiFontFamily + font.pixelSize: Theme.fontXs + font.bold: true + font.letterSpacing: 1.2 + opacity: 0.8 + } + } + } + + Rectangle { + width: 1 + height: 24 + color: Theme.sideBorder + } + + Button { + id: aboutBtn + flat: true + Layout.fillWidth: true + Layout.fillHeight: true + icon.source: "../icons/about.svg" + onClicked: utilFooter.parent.aboutDialog.open() + + background: Rectangle { + radius: 8 + color: aboutBtn.hovered + ? Theme.sideActiveBackground : "transparent" + } + + contentItem: Row { + spacing: 6 + anchors.centerIn: parent + + IconImage { + anchors.verticalCenter: parent.verticalCenter + width: 16 + height: 16 + source: "../icons/about.svg" + sourceSize.width: 16 + sourceSize.height: 16 + color: Theme.headingColor + opacity: 0.75 + } + + Label { + anchors.verticalCenter: parent.verticalCenter + text: "ABOUT" + color: Theme.headingColor + font.family: Theme.uiFontFamily + font.pixelSize: Theme.fontXs + font.bold: true + font.letterSpacing: 1.2 + opacity: 0.8 + } + } + } + } +} diff --git a/src/pygui/ISC/Tabs/components/qmldir b/src/pygui/ISC/Tabs/components/qmldir index b8fac2a..c77654d 100644 --- a/src/pygui/ISC/Tabs/components/qmldir +++ b/src/pygui/ISC/Tabs/components/qmldir @@ -6,5 +6,19 @@ TransportBar 1.0 TransportBar.qml WaferMapView 1.0 WaferMapView.qml ReplaceSensorDialog 1.0 ReplaceSensorDialog.qml RailActionButton 1.0 RailActionButton.qml +SelectFileDialog 1.0 SelectFileDialog.qml SplitDialog 1.0 SplitDialog.qml -StreamStatsDialog 1.0 StreamStatsDialog.qml \ No newline at end of file +StreamStatsDialog 1.0 StreamStatsDialog.qml +StatusActionsPanel 1.0 StatusActionsPanel.qml +ConnectionFooter 1.0 ConnectionFooter.qml +UtilityFooter 1.0 UtilityFooter.qml +PanelCheckBox 1.0 PanelCheckBox.qml +ReadoutStat 1.0 ReadoutStat.qml +AppToolTip 1.0 AppToolTip.qml +SectionTitle 1.0 SectionTitle.qml +PanelBox 1.0 PanelBox.qml +PanelSlider 1.0 PanelSlider.qml +EditableScrubStat 1.0 EditableScrubStat.qml +ComparisonChartCard 1.0 ComparisonChartCard.qml +OverlapMapCard 1.0 OverlapMapCard.qml +ComparisonSidePanel 1.0 ComparisonSidePanel.qml \ No newline at end of file diff --git a/src/pygui/ISC/Tabs/qmldir b/src/pygui/ISC/Tabs/qmldir index 4e00d78..2c433ae 100644 --- a/src/pygui/ISC/Tabs/qmldir +++ b/src/pygui/ISC/Tabs/qmldir @@ -5,4 +5,3 @@ WaferMapTab 1.0 WaferMapTab.qml DataTab 1.0 DataTab.qml SettingsTab 1.0 SettingsTab.qml StatusTab 1.0 StatusTab.qml -SelectFileDialog 1.0 SelectFileDialog.qml