From 3b919f576fa6c7c4eefede99d31438140ce7ecea Mon Sep 17 00:00:00 2001 From: jack Date: Mon, 6 Jul 2026 13:12:58 -0700 Subject: [PATCH] fix: comparison results never reached QML; restore click-box run selection --- src/pygui/ISC/Tabs/DataTab.qml | 232 +++++++----------- .../backend/controllers/session_controller.py | 17 +- tests/test_session_controller.py | 37 ++- 3 files changed, 137 insertions(+), 149 deletions(-) diff --git a/src/pygui/ISC/Tabs/DataTab.qml b/src/pygui/ISC/Tabs/DataTab.qml index b489931..2dd862e 100644 --- a/src/pygui/ISC/Tabs/DataTab.qml +++ b/src/pygui/ISC/Tabs/DataTab.qml @@ -16,6 +16,7 @@ Item { anchors.fill: parent // Properties for DTW Comparison + property int activeBox: 1 // 1 for Reference (Run A), 2 for Session (Run B), 0 for inactive property string compareFileA: "" property string compareFileB: "" property real warpingDistance: -1 @@ -43,17 +44,6 @@ Item { return path ? path.split("/").pop() : "" } - // Badge colors per wafer type, matching the mockup's file-row chips - function badgeFg(t) { - return t === "Z" ? "#a78bfa" : t === "B" ? "#93c5fd" : t === "A" ? "#6ee7b7" : "#fde047" - } - function badgeBg(t) { - return t === "Z" ? Qt.rgba(139 / 255, 92 / 255, 246 / 255, 0.15) - : t === "B" ? Qt.rgba(59 / 255, 130 / 255, 246 / 255, 0.15) - : t === "A" ? Qt.rgba(16 / 255, 185 / 255, 129 / 255, 0.15) - : Qt.rgba(245 / 255, 158 / 255, 11 / 255, 0.15) - } - function clearResults() { root.warpingDistance = -1 root.frameOffset = 0 @@ -69,15 +59,26 @@ Item { root.compareFileA = "" root.compareFileB = "" root.comparing = false - runACombo.currentIndex = -1 - runBCombo.currentIndex = -1 + root.activeBox = 1 clearResults() } Component.onCompleted: file_browser.refreshFiles() + // Clicking a file in the left rail's browser populates the armed run slot Connections { target: streamController + function onLoadedFileChanged() { + if (streamController.loadedFile && root.activeBox > 0) { + if (root.activeBox === 1) { + root.compareFileA = streamController.loadedFile + root.activeBox = 2 // Auto-advance to box 2 + } else if (root.activeBox === 2) { + root.compareFileB = streamController.loadedFile + root.activeBox = 0 // Finished selecting + } + } + } function onComparisonResult(result) { root.comparing = false if (result && result.success) { @@ -183,124 +184,79 @@ Item { } } - // Run selector dropdown fed by the left-rail file browser model - component RunSelector: ComboBox { - id: selector - property string placeholder: "Select CSV…" + // Run slot card: click to arm, then pick a file in the left rail's browser + component RunSlot: Rectangle { + id: slotBox + property int boxIndex: 1 + property string title + property string filePath: "" + property color accent: Theme.primaryAccent + signal cleared() Layout.fillWidth: true - implicitHeight: 36 - model: file_browser.files - currentIndex: -1 + implicitHeight: 58 + radius: Theme.radiusSm + color: Theme.fieldBackground + border.width: root.activeBox === boxIndex ? 2 : 1 + border.color: root.activeBox === boxIndex ? accent : Theme.cardBorder - readonly property var currentFile: (currentIndex >= 0 && currentIndex < file_browser.files.length) - ? file_browser.files[currentIndex] : null + Behavior on border.color { ColorAnimation { duration: Theme.durationFast } } - background: Rectangle { - radius: Theme.radiusXs - color: Theme.fieldBackground - border.color: selector.popup.visible ? Theme.fieldBorderFocus : Theme.fieldBorder - border.width: 1 + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.activeBox = slotBox.boxIndex } - contentItem: RowLayout { + RowLayout { + anchors.fill: parent + anchors.margins: 10 spacing: 8 Rectangle { - Layout.leftMargin: 8 - width: 16; height: 16; radius: 4 - visible: selector.currentFile !== null - color: root.badgeBg(selector.currentFile ? selector.currentFile.waferType : "") + width: 8; height: 8; radius: 4 + color: slotBox.accent + Layout.alignment: Qt.AlignVCenter + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 Label { - anchors.centerIn: parent - text: selector.currentFile ? selector.currentFile.waferType : "" + text: slotBox.title font.pixelSize: 9 font.bold: true - color: root.badgeFg(selector.currentFile ? selector.currentFile.waferType : "") + font.letterSpacing: 1.0 + color: root.activeBox === slotBox.boxIndex ? slotBox.accent : Theme.sideMutedText } - } - Label { - Layout.fillWidth: true - Layout.leftMargin: selector.currentFile === null ? 8 : 0 - text: selector.currentFile ? selector.currentFile.baseName : selector.placeholder - font.pixelSize: Theme.fontSm - font.weight: selector.currentFile ? Font.Medium : Font.Normal - color: selector.currentFile ? Theme.fieldText : Theme.fieldPlaceholder - elide: Text.ElideMiddle - } - } - - indicator: Label { - x: selector.width - width - 8 - y: (selector.height - height) / 2 - text: "▾" - font.pixelSize: Theme.fontSm - color: Theme.sideMutedText - } - - delegate: ItemDelegate { - id: fileDelegate - width: selector.width - highlighted: selector.highlightedIndex === index - - background: Rectangle { - radius: Theme.radiusXs - color: fileDelegate.highlighted ? Theme.buttonNeutralHover : "transparent" - } - contentItem: RowLayout { - spacing: 8 - Rectangle { - width: 20; height: 20; radius: 4 - color: root.badgeBg(modelData.waferType) - Label { - anchors.centerIn: parent - text: modelData.waferType - font.pixelSize: Theme.fontXs - font.bold: true - color: root.badgeFg(modelData.waferType) - } - } - ColumnLayout { + Label { + text: slotBox.filePath !== "" + ? root.shortName(slotBox.filePath) + : (root.activeBox === slotBox.boxIndex ? "← Select file from left panel…" : "Click to select") + font.pixelSize: Theme.fontXs + color: slotBox.filePath !== "" ? Theme.headingColor : Theme.bodyColor + opacity: slotBox.filePath !== "" ? 1.0 : 0.6 + elide: Text.ElideMiddle Layout.fillWidth: true - spacing: 0 - Label { - Layout.fillWidth: true - text: modelData.baseName - font.pixelSize: Theme.fontSm - font.weight: Font.Medium - color: Theme.headingColor - elide: Text.ElideMiddle - } - Label { - Layout.fillWidth: true - text: modelData.date || modelData.chamber || "" - visible: text !== "" - font.pixelSize: 9 - color: Theme.sideMutedText - elide: Text.ElideRight - } } } - } - popup: Popup { - y: selector.height + 4 - width: selector.width - implicitHeight: Math.min(contentItem.implicitHeight + 8, 220) - padding: 4 - - background: Rectangle { - radius: Theme.radiusXs - color: Theme.cardBackground - border.color: Theme.cardBorder - border.width: 1 - } - contentItem: ListView { - clip: true - implicitHeight: contentHeight - model: selector.popup.visible ? selector.delegateModel : null - currentIndex: selector.highlightedIndex - ScrollIndicator.vertical: ScrollIndicator {} + Button { + visible: slotBox.filePath !== "" + flat: true + implicitWidth: 24; implicitHeight: 24 + onClicked: slotBox.cleared() + background: Rectangle { + radius: Theme.radiusSm + color: parent.hovered ? Qt.rgba(1, 1, 1, 0.08) : "transparent" + } + contentItem: Label { + text: "✕" + color: Theme.bodyColor + font.pixelSize: Theme.fontSm + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } } } } @@ -565,7 +521,7 @@ Item { Label { Layout.alignment: Qt.AlignHCenter visible: !root.comparing - text: "Pick Run A and Run B in the panel on the right." + text: "Click a run box on the right, then select a file from the left panel." color: Theme.sideMutedText font.pixelSize: Theme.fontXs } @@ -739,37 +695,27 @@ Item { SectionTitle { text: "RUN SELECTION" } - ColumnLayout { - Layout.fillWidth: true - spacing: 4 - Label { - text: "Reference CSV (Run A)" - font.pixelSize: 9 - color: Theme.sideMutedText - } - RunSelector { - id: runACombo - onActivated: function (index) { - root.compareFileA = file_browser.files[index].fileName - root.clearResults() - } + RunSlot { + title: "REFERENCE (RUN A)" + boxIndex: 1 + accent: Theme.primaryAccent + filePath: root.compareFileA + onCleared: { + root.compareFileA = "" + root.activeBox = 1 + root.clearResults() } } - ColumnLayout { - Layout.fillWidth: true - spacing: 4 - Label { - text: "Session CSV (Run B)" - font.pixelSize: 9 - color: Theme.sideMutedText - } - RunSelector { - id: runBCombo - onActivated: function (index) { - root.compareFileB = file_browser.files[index].fileName - root.clearResults() - } + RunSlot { + title: "SESSION (RUN B)" + boxIndex: 2 + accent: Theme.themeSkill + filePath: root.compareFileB + onCleared: { + root.compareFileB = "" + root.activeBox = 2 + root.clearResults() } } diff --git a/src/pygui/backend/controllers/session_controller.py b/src/pygui/backend/controllers/session_controller.py index 2477350..ba7ab04 100644 --- a/src/pygui/backend/controllers/session_controller.py +++ b/src/pygui/backend/controllers/session_controller.py @@ -40,8 +40,10 @@ class SessionController(QObject): liveStatsChanged = Signal() # emitted when receivedCount or errorCount change # trend: per-frame avg for live graph trendData = Signal(str) # JSON array of floats - comparisonResult = Signal(object) # dict with success, distance, warping_path, max_sensor_deviation, series_a, series_b - splitResult = Signal(object) # dict with success, segments + # NOTE: dict (QVariantMap), not object — Signal(object) delivers Python + # dicts to QML as an opaque empty wrapper with no accessible fields. + comparisonResult = Signal(dict) # dict with success, distance, warping_path, max_sensor_deviation, series_a, series_b + splitResult = Signal(dict) # dict with success, segments # private: marshal a worker-thread frame onto the main thread _liveFrame = Signal(object) # Frame _liveError = Signal() # worker-thread error ping @@ -357,10 +359,11 @@ class SessionController(QObject): result = compare_runs(series_a, series_b) - # Mean temporal shif along the DTW path: positive means run B - # reaches the same profile features lather than run A. + # Mean temporal shift along the DTW path: positive means run B + # reaches the same profile features later than run A. path = result["warping_path"] frame_offset = round(sum(j - i for i, j in path) / len(path)) if path else 0 + # Max sensor deviation: walk the DTW-aligned frame pairs and take # the largest per-sensor abs difference across all sensors, not # just the sensor[0] series used for alignment. @@ -422,7 +425,8 @@ class SessionController(QObject): self.comparisonResult.emit({ "success": True, "distance": result["distance"], - "warping_path": result["warping_path"][:50], # limit path size + # Lists, not tuples — tuples don't survive QVariant conversion + "warping_path": [list(p) for p in result["warping_path"][:50]], "frame_offset": frame_offset, "max_sensor_deviation": max_sensor_deviation, "series_a": series_a, @@ -709,6 +713,9 @@ class SessionController(QObject): self._trend_timestamps.pop(0) self._trend_buffer.pop(0) + + + def _flush_repaint(self) -> None: if not self._dirty: return diff --git a/tests/test_session_controller.py b/tests/test_session_controller.py index efa1ecd..954c1a2 100644 --- a/tests/test_session_controller.py +++ b/tests/test_session_controller.py @@ -64,7 +64,42 @@ def test_compare_files_integration(controller): assert "series_a" in args assert "series_b" in args assert args["frame_offset"] == 0 - + +def test_comparison_result_reaches_qml(qapp, controller): + """The result dict must arrive in QML as a real JS object with fields. + + Signal(object) delivers Python dicts to QML as an opaque empty wrapper; + the signal must be declared with a dict/QVariantMap signature. + """ + from PySide6.QtQml import QQmlApplicationEngine + + engine = QQmlApplicationEngine() + engine.rootContext().setContextProperty("streamController", controller) + engine.loadData(b""" +import QtQuick +Item { + id: probe + property bool gotSuccess: false + property real gotDistance: -1 + Connections { + target: streamController + function onComparisonResult(result) { + probe.gotSuccess = result.success === true + probe.gotDistance = result.distance !== undefined ? result.distance : -1 + } + } +} +""") + assert engine.rootObjects(), "probe QML failed to load" + root = engine.rootObjects()[0] + + csv_path = "tests/fixtures/sample_stream.csv" + controller.compareFiles(csv_path, csv_path) + qapp.processEvents() + + assert root.property("gotSuccess") is True + assert root.property("gotDistance") >= 0 + def test_recording_toggle(controller, tmp_path): csv_path = str(tmp_path / "rec" / "live_test.csv") controller.startRecording(csv_path, "SN123")