From 97ca58bfc27385602f869a70413b1d43106780c6 Mon Sep 17 00:00:00 2001 From: jack Date: Thu, 11 Jun 2026 11:57:24 -0700 Subject: [PATCH] feat: Add Wafer Map tab and associated components - Introduced a new Wafer Map tab in the HomePage.qml, which loads WaferMapTab.qml when selected. - Created WaferMapTab.qml to display wafer map with live data and controls. - Added ReadoutPanel.qml for displaying sensor statistics and thresholds. - Implemented SourcePanel.qml for file selection and filtering. - Developed TransportBar.qml for playback controls. - Added WaferMapView.qml to visualize wafer data with interactive features. - Created ReplaceSensorDialog.qml for sensor value overrides. - Updated Theme.qml for new color schemes and UI adjustments. - Modified qmldir files to include new components in the ISC.Tabs and ISC.Tabs.components modules. --- src/pygui/ISC/HomePage.qml | 8 +- src/pygui/ISC/Tabs/WaferMapTab.qml | 231 +++++++++++++++++ .../ISC/Tabs/components/ReadoutPanel.qml | 240 ++++++++++++++++++ .../Tabs/components/ReplaceSensorDialog.qml | 206 +++++++++++++++ src/pygui/ISC/Tabs/components/SourcePanel.qml | 229 +++++++++++++++++ .../ISC/Tabs/components/TransportBar.qml | 91 +++++++ .../ISC/Tabs/components/WaferMapView.qml | 43 ++++ src/pygui/ISC/Tabs/components/qmldir | 7 + src/pygui/ISC/Tabs/qmldir | 5 +- src/pygui/ISC/Theme.qml | 33 ++- 10 files changed, 1080 insertions(+), 13 deletions(-) create mode 100644 src/pygui/ISC/Tabs/WaferMapTab.qml create mode 100644 src/pygui/ISC/Tabs/components/ReadoutPanel.qml create mode 100644 src/pygui/ISC/Tabs/components/ReplaceSensorDialog.qml create mode 100644 src/pygui/ISC/Tabs/components/SourcePanel.qml create mode 100644 src/pygui/ISC/Tabs/components/TransportBar.qml create mode 100644 src/pygui/ISC/Tabs/components/WaferMapView.qml create mode 100644 src/pygui/ISC/Tabs/components/qmldir diff --git a/src/pygui/ISC/HomePage.qml b/src/pygui/ISC/HomePage.qml index 541a5a1..f2d3f77 100644 --- a/src/pygui/ISC/HomePage.qml +++ b/src/pygui/ISC/HomePage.qml @@ -158,11 +158,17 @@ Rectangle { Label { anchors.centerIn: parent - visible: parent.tabName !== "Settings" && parent.tabName !== "Status" && parent.tabName !== "Data" + visible: parent.tabName !== "Settings" && parent.tabName !== "Status" && parent.tabName !== "Data" && parent.tabName !== "Wafer Map" text: parent.tabName + " content" color: Theme.bodyColor font.pixelSize: 20 } + + Loader{ + anchors.fill: parent + active: parent.tabName === "Wafer Map" + source: parent.tabName === "Wafer Map" ? "Tabs/WaferMapTab.qml" : "" + } } } } diff --git a/src/pygui/ISC/Tabs/WaferMapTab.qml b/src/pygui/ISC/Tabs/WaferMapTab.qml new file mode 100644 index 0000000..206c4d7 --- /dev/null +++ b/src/pygui/ISC/Tabs/WaferMapTab.qml @@ -0,0 +1,231 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import ISC +import ISC.Tabs.components + +Item { + id: root + anchors.fill: parent + + // Live elapsed-time counter + property int _liveSecs: 0 + Timer { + id: liveTimer + interval: 1000 + repeat: true + running: streamController.mode === "live" && streamController.state !== "idle" + onTriggered: root._liveSecs++ + onRunningChanged: if (!running) root._liveSecs = 0 + } + function fmtTime(s) { + var m = Math.floor(s / 60) + var ss = s % 60 + return (m < 10 ? "0" : "") + m + ":" + (ss < 10 ? "0" : "") + ss + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Theme.panelPadding + spacing: Theme.rightPaneGap + + // ── Toolbar ─────────────────────────────────────────────────────── + RowLayout { + Layout.fillWidth: true + spacing: 10 + + // Mode toggle + TabBar { + id: modeBar + spacing: 2 + padding: 2 + background: Rectangle { + color: Theme.subtleSectionBackground + radius: Theme.radiusSm + border.color: Theme.cardBorder + border.width: Theme.borderThin + } + TabButton { + text: "Live" + implicitWidth: 58; implicitHeight: 28 + background: Rectangle { + color: parent.checked ? Theme.buttonNeutralBackground : "transparent" + radius: Theme.radiusSm - 1 + } + contentItem: Text { + text: parent.text + color: parent.checked ? Theme.headingColor : Theme.bodyColor + font.pixelSize: 11 + font.weight: parent.checked ? Font.Medium : Font.Normal + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + TabButton { + text: "Review" + implicitWidth: 64; implicitHeight: 28 + background: Rectangle { + color: parent.checked ? Theme.buttonNeutralBackground : "transparent" + radius: Theme.radiusSm - 1 + } + contentItem: Text { + text: parent.text + color: parent.checked ? Theme.headingColor : Theme.bodyColor + font.pixelSize: 11 + font.weight: parent.checked ? Font.Medium : Font.Normal + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + onCurrentIndexChanged: + streamController.setMode(currentIndex === 0 ? "live" : "review") + } + + // Live source info: WiFi icon + port · serial + RowLayout { + visible: streamController.mode === "live" + spacing: 5 + // WiFi icon (Unicode approximation; swap for SVG if available) + Label { + text: "◉" + color: streamController.state !== "idle" + ? Theme.liveColor : Theme.bodyColor + font.pixelSize: 13 + } + Label { + text: "Live stream" + color: Theme.bodyColor + font.pixelSize: 11 + } + } + + Item { Layout.fillWidth: true } + + // SET badge + avg temp + Rectangle { + visible: streamController.state === "set" + height: 22; radius: Theme.radiusSm + color: "transparent" + border.color: Theme.liveColor + border.width: 1 + implicitWidth: setLabel.implicitWidth + 16 + Label { + id: setLabel + anchors.centerIn: parent + text: "SET " + (streamController.stats.avg !== undefined + ? streamController.stats.avg + " avg" : "") + color: Theme.liveColor + font.pixelSize: 10 + font.weight: Font.Medium + font.letterSpacing: 0.8 + } + } + + // Record indicator + RowLayout { + spacing: 5 + visible: streamController.recording + Rectangle { + width: 7; height: 7; radius: 4 + color: Theme.recordColor + SequentialAnimation on opacity { + running: streamController.recording + loops: Animation.Infinite + NumberAnimation { to: 0.2; duration: 600 } + NumberAnimation { to: 1.0; duration: 600 } + } + } + Label { + text: "REC" + color: Theme.recordColor + font.pixelSize: 10 + font.weight: Font.Medium + font.letterSpacing: 1.2 + } + } + + // LIVE timer + RowLayout { + spacing: 5 + visible: streamController.mode === "live" && streamController.state !== "idle" + Rectangle { + width: 7; height: 7; radius: 4 + color: Theme.liveColor + } + Label { + text: "LIVE " + root.fmtTime(root._liveSecs) + color: Theme.liveColor + font.pixelSize: 10 + font.weight: Font.Medium + font.letterSpacing: 0.8 + } + } + + // IDLE label (review / not connected) + Label { + visible: streamController.mode === "review" || streamController.state === "idle" + text: streamController.state.toUpperCase() + color: Theme.bodyColor + font.pixelSize: 11 + font.weight: Font.Medium + font.letterSpacing: 1.2 + } + } + + // ── Body: 3 zones ───────────────────────────────────────────────── + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: Theme.rightPaneGap + + // Source panel — bordered card + Rectangle { + Layout.preferredWidth: 220 + Layout.fillHeight: true + color: Theme.cardBackground + border.color: Theme.cardBorder + border.width: 1 + radius: Theme.radiusMd + clip: true + + SourcePanel { + anchors.fill: parent + anchors.margins: 10 + } + } + + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 8 + + WaferMapView { + id: waferView + Layout.fillWidth: true + Layout.fillHeight: true + blend: readoutPanel.heatmapBlend + showLabels: readoutPanel.showLabels + } + + TransportBar { Layout.fillWidth: true } + } + + // Readout panel — bordered card + Rectangle { + Layout.preferredWidth: 220 + Layout.fillHeight: true + color: Theme.cardBackground + border.color: Theme.cardBorder + border.width: 1 + radius: Theme.radiusMd + clip: true + + ReadoutPanel { + id: readoutPanel + anchors.fill: parent + anchors.margins: 10 + } + } + } + } +} diff --git a/src/pygui/ISC/Tabs/components/ReadoutPanel.qml b/src/pygui/ISC/Tabs/components/ReadoutPanel.qml new file mode 100644 index 0000000..a8187ba --- /dev/null +++ b/src/pygui/ISC/Tabs/components/ReadoutPanel.qml @@ -0,0 +1,240 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import ISC + +ColumnLayout { + spacing: 0 + property var s: streamController.stats + property alias showLabels: labelsToggle.checked + property alias heatmapBlend: heatmapSlider.value + + // ── READOUT ─────────────────────────────────────────────────────────── + Label { + text: "READOUT" + color: Theme.bodyColor + font.pixelSize: 10 + font.letterSpacing: 1.8 + font.weight: Font.Medium + bottomPadding: 4 + } + + GridLayout { + columns: 2 + rowSpacing: 4 + columnSpacing: 4 + Layout.fillWidth: true + + // MIN TEMP + Rectangle { + Layout.fillWidth: true; height: 52 + color: Theme.subtleSectionBackground; radius: Theme.radiusSm + border.color: Theme.cardBorder; border.width: 1 + ColumnLayout { + anchors { fill: parent; margins: 7 } + spacing: 1 + Label { text: "MIN TEMP"; color: Theme.bodyColor; font.pixelSize: 9; font.letterSpacing: 1 } + Label { + text: s.min !== undefined ? s.min + " #" + s.minIndex : "—" + color: Theme.sensorLow + font.pixelSize: 15; font.weight: Font.Bold + } + } + } + + // MAX TEMP + Rectangle { + Layout.fillWidth: true; height: 52 + color: Theme.subtleSectionBackground; radius: Theme.radiusSm + border.color: Theme.cardBorder; border.width: 1 + ColumnLayout { + anchors { fill: parent; margins: 7 } + spacing: 1 + Label { text: "MAX TEMP"; color: Theme.bodyColor; font.pixelSize: 9; font.letterSpacing: 1 } + Label { + text: s.max !== undefined ? s.max + " #" + s.maxIndex : "—" + color: Theme.sensorHigh + font.pixelSize: 15; font.weight: Font.Bold + } + } + } + + // DIFFERENTIAL + Rectangle { + Layout.fillWidth: true; height: 52 + color: Theme.subtleSectionBackground; radius: Theme.radiusSm + border.color: Theme.cardBorder; border.width: 1 + ColumnLayout { + anchors { fill: parent; margins: 7 } + spacing: 1 + Label { text: "DIFFERENTIAL"; color: Theme.bodyColor; font.pixelSize: 9; font.letterSpacing: 1 } + Label { + text: s.diff !== undefined ? s.diff : "—" + color: Theme.headingColor + font.pixelSize: 15; font.weight: Font.Bold + } + } + } + + // AVERAGE + Rectangle { + Layout.fillWidth: true; height: 52 + color: Theme.subtleSectionBackground; radius: Theme.radiusSm + border.color: Theme.cardBorder; border.width: 1 + ColumnLayout { + anchors { fill: parent; margins: 7 } + spacing: 1 + Label { text: "AVERAGE"; color: Theme.bodyColor; font.pixelSize: 9; font.letterSpacing: 1 } + Label { + text: s.avg !== undefined ? s.avg : "—" + color: Theme.headingColor + font.pixelSize: 15; font.weight: Font.Bold + } + } + } + } + + // SIGMA — full-width + Rectangle { + Layout.fillWidth: true; height: 44 + color: Theme.subtleSectionBackground; radius: Theme.radiusSm + border.color: Theme.cardBorder; border.width: 1 + RowLayout { + anchors { fill: parent; leftMargin: 7; rightMargin: 7; topMargin: 4; bottomMargin: 4 } + ColumnLayout { + spacing: 1 + Label { text: "SIGMA (σ)"; color: Theme.bodyColor; font.pixelSize: 9; font.letterSpacing: 1 } + Label { text: "3σ"; color: Theme.bodyColor; font.pixelSize: 9; font.letterSpacing: 1 } + } + Item { Layout.fillWidth: true } + ColumnLayout { + spacing: 1 + Label { + text: s.sigma !== undefined ? s.sigma : "—" + color: Theme.headingColor + font.pixelSize: 15; font.weight: Font.Bold + Layout.alignment: Qt.AlignRight + } + Label { + text: s.threeSigma !== undefined ? s.threeSigma : "—" + color: Theme.bodyColor + font.pixelSize: 12; font.weight: Font.Medium + Layout.alignment: Qt.AlignRight + } + } + } + } + + Item { height: 8 } + Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder } + Item { height: 8 } + + // ── DISPLAY ─────────────────────────────────────────────────────────── + Label { + text: "DISPLAY" + color: Theme.bodyColor + font.pixelSize: 10 + font.letterSpacing: 1.8 + font.weight: Font.Medium + bottomPadding: 4 + } + + CheckBox { + id: labelsToggle + text: "Labels" + checked: true + font.pixelSize: 11 + } + + RowLayout { + Layout.fillWidth: true + spacing: 6 + Label { + text: "Heatmap" + color: Theme.bodyColor + font.pixelSize: 11 + Layout.preferredWidth: 52 + } + Slider { + id: heatmapSlider + from: 0; to: 1; value: 0 + Layout.fillWidth: true + ToolTip.visible: hovered + ToolTip.text: Math.round(value * 100) + "%" + } + } + + Item { height: 8 } + Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder } + Item { height: 8 } + + // ── THRESHOLDS ──────────────────────────────────────────────────────── + Label { + text: "THRESHOLDS" + color: Theme.bodyColor + font.pixelSize: 10 + font.letterSpacing: 1.8 + font.weight: Font.Medium + bottomPadding: 4 + } + + Label { text: "Set Point (°C)"; color: Theme.bodyColor; font.pixelSize: 11 } + TextField { + id: spField + Layout.fillWidth: true + text: "149.0" + font.pixelSize: 12 + inputMethodHints: Qt.ImhFormattedNumbersOnly + color: Theme.fieldText + placeholderTextColor: Theme.fieldPlaceholder + selectedTextColor: Theme.fieldBackground + selectionColor: Theme.fieldText + background: Rectangle { + radius: Theme.radiusXs + color: Theme.fieldBackground + border.width: spField.activeFocus ? Theme.borderStrong : Theme.borderThin + border.color: spField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder + } + onEditingFinished: pushThresholds() + } + + Item { height: 4 } + + Label { text: "Margin (±°C)"; color: Theme.bodyColor; font.pixelSize: 11 } + TextField { + id: mgField + Layout.fillWidth: true + text: "1.0" + font.pixelSize: 12 + inputMethodHints: Qt.ImhFormattedNumbersOnly + color: Theme.fieldText + placeholderTextColor: Theme.fieldPlaceholder + selectedTextColor: Theme.fieldBackground + selectionColor: Theme.fieldText + background: Rectangle { + radius: Theme.radiusXs + color: Theme.fieldBackground + border.width: mgField.activeFocus ? Theme.borderStrong : Theme.borderThin + border.color: mgField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder + } + onEditingFinished: pushThresholds() + } + + Item { height: 4 } + + CheckBox { + id: autoCheck + text: "Auto range (mean ± 1σ)" + checked: true + font.pixelSize: 11 + onCheckedChanged: pushThresholds() + } + + Item { Layout.fillHeight: true } + + function pushThresholds() { + var sp = parseFloat(spField.text) || 149.0 + var mg = parseFloat(mgField.text) || 1.0 + streamController.setThresholds(sp, mg, autoCheck.checked) + } +} diff --git a/src/pygui/ISC/Tabs/components/ReplaceSensorDialog.qml b/src/pygui/ISC/Tabs/components/ReplaceSensorDialog.qml new file mode 100644 index 0000000..89bf7e5 --- /dev/null +++ b/src/pygui/ISC/Tabs/components/ReplaceSensorDialog.qml @@ -0,0 +1,206 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import ISC + +Dialog { + id: root + title: "Sensor Override" + modal: true + standardButtons: Dialog.NoButton + width: 300 + + // called by WaferMapView's TapHandler + property int sensorIndex: -1 + property string sensorLabel: "" + property real currentValue: 0.0 + property bool hasOverride: streamController.overriddenSensors.indexOf(sensorIndex) >= 0 + + function openFor(index) { + sensorIndex = index + var dot = streamController.sensorDots[index] + if (!dot) return + sensorLabel = dot.label !== undefined ? String(dot.label) : String(index + 1) + currentValue = dot.value + valueField.text = "" + offsetField.text = "" + open() + } + + background: Rectangle { + color: Theme.cardBackground + radius: 8 + border.color: Theme.cardBorder + } + + ColumnLayout { + anchors.fill: parent + spacing: Theme.rightPaneGap + + // ── header ─────────────────────────────────────────────── + RowLayout { + Layout.fillWidth: true + Label { + text: "Sensor #" + (root.sensorIndex + 1) + + (root.sensorLabel ? " (" + root.sensorLabel + ")" : "") + color: Theme.headingColor + font.bold: true + font.pixelSize: 13 + Layout.fillWidth: true + } + Rectangle { + visible: root.hasOverride + width: overrideTag.implicitWidth + 12 + height: overrideTag.implicitHeight + 6 + radius: 4 + color: Theme.statusWarningColor + opacity: 0.85 + Label { + id: overrideTag + anchors.centerIn: parent + text: "OVERRIDE" + color: "white" + font.pixelSize: 10 + font.bold: true + font.letterSpacing: 1 + } + } + } + + Label { + text: "Live: " + root.currentValue.toFixed(2) + color: Theme.bodyColor + font.pixelSize: 12 + } + + Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder } + + // ── replace field ───────────────────────────────────────── + Label { text: "Replace with value"; color: Theme.bodyColor; font.pixelSize: 11 } + TextField { + id: valueField + Layout.fillWidth: true + placeholderText: "100" + inputMethodHints: Qt.ImhFormattedNumbersOnly + color: Theme.fieldText + placeholderTextColor: Theme.fieldPlaceholder + selectedTextColor: Theme.fieldBackground + selectionColor: Theme.fieldText + background: Rectangle { + radius: Theme.radiusXs + color: Theme.fieldBackground + border.width: valueField.activeFocus ? Theme.borderStrong : Theme.borderThin + border.color: valueField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder + } + } + + // ── offset field ────────────────────────────────────────── + Label { text: "Or add offset (± delta)"; color: Theme.bodyColor; font.pixelSize: 11 } + TextField { + id: offsetField + Layout.fillWidth: true + placeholderText: "e.g. +0.5 or -0.5 (blank = skip)" + inputMethodHints: Qt.ImhFormattedNumbersOnly + color: Theme.fieldText + placeholderTextColor: Theme.fieldPlaceholder + selectedTextColor: Theme.fieldBackground + selectionColor: Theme.fieldText + background: Rectangle { + radius: Theme.radiusXs + color: Theme.fieldBackground + border.width: offsetField.activeFocus ? Theme.borderStrong : Theme.borderThin + border.color: offsetField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder + } + } + + Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder } + + // ── buttons ─────────────────────────────────────────────── + RowLayout { + Layout.fillWidth: true + spacing: 8 + + Button { + id: applyBtn + text: "Apply" + Layout.fillWidth: true + hoverEnabled: true + enabled: valueField.text !== "" || offsetField.text !== "" + opacity: enabled ? 1.0 : 0.4 + background: Rectangle { + radius: Theme.radiusSm + color: applyBtn.down ? Theme.buttonNeutralPressed : applyBtn.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground + border.width: Theme.borderThin + border.color: Theme.fieldBorder + } + contentItem: Text { + text: applyBtn.text + color: Theme.buttonNeutralText + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.bold: true + font.pixelSize: 13 + } + onClicked: { + if (valueField.text !== "") { + var v = parseFloat(valueField.text) + if (!isNaN(v)) + streamController.replaceSensor(root.sensorIndex, v) + } + if (offsetField.text !== "") { + var d = parseFloat(offsetField.text) + if (!isNaN(d)) + streamController.offsetSensor(root.sensorIndex, d) + } + root.close() + } + } + Button { + id: clearBtn + text: "Clear" + Layout.fillWidth: true + hoverEnabled: true + enabled: root.hasOverride + opacity: enabled ? 1.0 : 0.4 + background: Rectangle { + radius: Theme.radiusSm + color: clearBtn.down ? Theme.buttonNeutralPressed : clearBtn.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground + border.width: Theme.borderThin + border.color: Theme.fieldBorder + } + contentItem: Text { + text: clearBtn.text + color: Theme.buttonNeutralText + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.bold: true + font.pixelSize: 13 + } + onClicked: { + streamController.clearSensorEdit(root.sensorIndex) + root.close() + } + } + Button { + id: cancelBtn + text: "Cancel" + hoverEnabled: true + background: Rectangle { + radius: Theme.radiusSm + color: cancelBtn.down ? Theme.buttonNeutralPressed : cancelBtn.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground + border.width: Theme.borderThin + border.color: Theme.fieldBorder + } + contentItem: Text { + text: cancelBtn.text + color: Theme.buttonNeutralText + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.bold: true + font.pixelSize: 13 + } + onClicked: root.close() + } + } + } +} \ No newline at end of file diff --git a/src/pygui/ISC/Tabs/components/SourcePanel.qml b/src/pygui/ISC/Tabs/components/SourcePanel.qml new file mode 100644 index 0000000..8b6bd20 --- /dev/null +++ b/src/pygui/ISC/Tabs/components/SourcePanel.qml @@ -0,0 +1,229 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import ISC + +ColumnLayout { + spacing: 6 + + // ── Section label ───────────────────────────────────────────────────── + Label { + text: "SOURCE" + color: Theme.bodyColor + font.pixelSize: 10 + font.letterSpacing: 1.8 + font.weight: Font.Medium + } + + // ── Directory button ────────────────────────────────────────────────── + Button { + id: dirBtn + Layout.fillWidth: true + hoverEnabled: true + implicitHeight: 32 + leftPadding: 8 + rightPadding: 8 + background: Rectangle { + radius: Theme.radiusSm + color: dirBtn.pressed ? Theme.buttonNeutralPressed + : dirBtn.hovered ? Theme.buttonNeutralHover + : Theme.buttonNeutralBackground + border.width: Theme.borderThin + border.color: Theme.fieldBorder + } + contentItem: RowLayout { + spacing: 6 + Label { + text: "▤" + color: Theme.bodyColor + font.pixelSize: 12 + } + Label { + text: file_browser.currentDirectory.split("/").pop() || file_browser.currentDirectory + color: Theme.headingColor + font.pixelSize: 11 + elide: Text.ElideMiddle + Layout.fillWidth: true + } + } + onClicked: file_browser.chooseDirectory() + } + + // ── Filter ──────────────────────────────────────────────────────────── + TextField { + id: filter + Layout.fillWidth: true + placeholderText: "Filter…" + font.pixelSize: 11 + color: Theme.fieldText + placeholderTextColor: Theme.fieldPlaceholder + selectedTextColor: Theme.fieldBackground + selectionColor: Theme.fieldText + background: Rectangle { + radius: Theme.radiusXs + color: Theme.fieldBackground + border.width: filter.activeFocus ? Theme.borderStrong : Theme.borderThin + border.color: filter.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder + } + } + + // ── File list ───────────────────────────────────────────────────────── + ListView { + id: fileList + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + spacing: 2 + highlight: null + highlightFollowsCurrentItem: false + + model: file_browser.files + + delegate: ItemDelegate { + id: fileItem + width: ListView.view.width + padding: 0 + highlighted: false + focusPolicy: Qt.NoFocus + + readonly property bool matchesFilter: { + if (filter.text === "") return true + var q = filter.text.toLowerCase() + return (modelData.baseName + modelData.waferType + modelData.date) + .toLowerCase().indexOf(q) >= 0 + } + + // Active only in review mode; no highlight during live streaming + readonly property bool isActive: + streamController.mode === "review" && + streamController.loadedFile !== "" && + modelData.fileName === streamController.loadedFile + + visible: matchesFilter + height: matchesFilter ? implicitHeight : 0 + + onClicked: streamController.loadFile(modelData.fileName) + + background: Rectangle { + color: fileItem.isActive + ? Qt.rgba(1, 1, 1, 0.06) + : fileItem.hovered ? Qt.rgba(1, 1, 1, 0.04) : "transparent" + radius: Theme.radiusSm + + // Left accent bar — shown only when active + Rectangle { + visible: fileItem.isActive + width: 3 + radius: 1.5 + anchors { + top: parent.top; bottom: parent.bottom; left: parent.left + topMargin: 5; bottomMargin: 5 + } + color: { + var t = modelData.waferType + if (t === "A" || t === "E" || t === "P") return "#3B82F6" + if (t === "B" || t === "C" || t === "D") return "#10B981" + if (t === "Z") return "#8B5CF6" + return Theme.headingColor + } + } + } + + contentItem: RowLayout { + spacing: 8 + anchors.margins: 8 + + // Wafer-type avatar (circle) + Rectangle { + width: 28; height: 28 + radius: 14 + color: { + var t = modelData.waferType + if (t === "A" || t === "E" || t === "P") return "#1D4ED8" + if (t === "B" || t === "C" || t === "D") return "#065F46" + if (t === "Z") return "#7C3AED" + return "#374151" + } + Label { + anchors.centerIn: parent + text: modelData.waferType || "?" + font.pixelSize: 12 + font.weight: Font.Bold + color: "#FFFFFF" + } + } + + // Typography hierarchy + ColumnLayout { + spacing: 1 + Layout.fillWidth: true + + // Primary: serial number + Label { + text: modelData.serialNumber || modelData.baseName + color: Theme.headingColor + font.pixelSize: 13 + font.weight: Font.Bold + elide: Text.ElideRight + Layout.fillWidth: true + } + + // Secondary: date · time + RowLayout { + spacing: 3 + Layout.fillWidth: true + Label { + text: { + var d = modelData.date || "" + return d.length >= 10 ? d.substring(0, 10) : (d || "—") + } + color: Theme.bodyColor + font.pixelSize: 10 + } + Label { + visible: (modelData.timeStr || "") !== "" + text: "·" + color: Theme.bodyColor + font.pixelSize: 10 + opacity: 0.45 + } + Label { + visible: (modelData.timeStr || "") !== "" + text: modelData.timeStr || "" + color: Theme.bodyColor + font.pixelSize: 10 + opacity: 0.65 + } + } + + // Tertiary: edited metadata (chamber · notes) if any + Label { + visible: { + var c = modelData.chamber || "" + var n = modelData.notes || "" + return c !== "" || n !== "" + } + text: { + var parts = [] + if (modelData.chamber) parts.push(modelData.chamber) + if (modelData.notes) parts.push(modelData.notes) + return parts.join(" · ") + } + color: Theme.bodyColor + font.pixelSize: 9 + opacity: 0.7 + elide: Text.ElideRight + Layout.fillWidth: true + } + } + } + } + } + + // ── Footer ──────────────────────────────────────────────────────────── + Label { + text: file_browser.files.length + " files · CSV only" + color: Theme.bodyColor + font.pixelSize: 10 + } +} diff --git a/src/pygui/ISC/Tabs/components/TransportBar.qml b/src/pygui/ISC/Tabs/components/TransportBar.qml new file mode 100644 index 0000000..a544353 --- /dev/null +++ b/src/pygui/ISC/Tabs/components/TransportBar.qml @@ -0,0 +1,91 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import ISC + +RowLayout { + spacing: 2 + + component TBtn: Button { + implicitWidth: 44 + implicitHeight: 32 + hoverEnabled: true + font.pixelSize: 13 + background: Rectangle { + color: parent.pressed ? Theme.transportButtonHover + : parent.hovered ? Theme.transportButtonBg + : Theme.transportBackground + radius: Theme.radiusSm + } + contentItem: Text { + text: parent.text + color: Theme.headingColor + font: parent.font + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + + // Left fill — centers the button cluster + Item { Layout.fillWidth: true } + + TBtn { text: "⏮"; onClicked: streamController.step(-1) } + TBtn { text: "⏹"; onClicked: streamController.stop() } + TBtn { text: "⏸"; onClicked: streamController.pause() } + // Play: slightly wider; leftPadding nudge compensates for ▶ optical offset + TBtn { + text: "▶" + implicitWidth: 52 + contentItem: Text { + text: parent.text + color: Theme.headingColor + font: parent.font + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + leftPadding: 2 + } + onClicked: streamController.play() + } + TBtn { text: "⏭"; onClicked: streamController.step(1) } + + // Right fill — balances centering + Item { Layout.fillWidth: true } + + // Speed cycle + Button { + id: speedBtn + property var speeds: [1, 2, 5] + property int idx: 0 + implicitWidth: 44 + implicitHeight: 32 + hoverEnabled: true + text: speeds[idx] + "×" + font.pixelSize: 11 + font.weight: Font.Medium + background: Rectangle { + color: parent.pressed ? Theme.transportButtonHover + : parent.hovered ? Theme.transportButtonBg + : Theme.transportBackground + radius: Theme.radiusSm + } + contentItem: Text { + text: parent.text + color: Theme.bodyColor + font: parent.font + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + onClicked: { + idx = (idx + 1) % speeds.length + streamController.setSpeed(speeds[idx]) + } + } + + Item { width: 8 } + + Label { + text: "frame " + (streamController.frameIndex + 1) + " / " + streamController.frameTotal + color: Theme.bodyColor + font.pixelSize: 11 + } +} diff --git a/src/pygui/ISC/Tabs/components/WaferMapView.qml b/src/pygui/ISC/Tabs/components/WaferMapView.qml new file mode 100644 index 0000000..4a7bc4b --- /dev/null +++ b/src/pygui/ISC/Tabs/components/WaferMapView.qml @@ -0,0 +1,43 @@ +import QtQuick +import QtQuick.Controls +import ISC +import ISC.Wafer + + +Item { + id: root + property real blend: 0.0 + property bool showLabels: true + + WaferMapItem { + id: map + anchors.fill: parent + sensors: streamController.sensorLayout // [{label,x,y}] + values: streamController.sensorValues // [float] + bands: streamController.sensorBands // ["in_range"|"high"|"low"] + target: streamController.target + margin: streamController.margin + blend: root.blend + showLabels: root.showLabels + // Bind to Theme so colors update on dark/light mode switch + ringColor: Theme.waferRingColor + axisColor: Theme.waferAxisColor + lowColor: Theme.sensorLow + inRangeColor: Theme.sensorInRange + highColor: Theme.sensorHigh + textColor: Theme.headingColor + + + TapHandler { + onTapped: (ev) => { + var idx = map.which_marker(ev.position.x, ev.position.y) + if (idx >= 0) replaceDialog.openFor(idx) + } + } + } + + ReplaceSensorDialog { id: replaceDialog} + + + +} \ No newline at end of file diff --git a/src/pygui/ISC/Tabs/components/qmldir b/src/pygui/ISC/Tabs/components/qmldir new file mode 100644 index 0000000..ec6da6e --- /dev/null +++ b/src/pygui/ISC/Tabs/components/qmldir @@ -0,0 +1,7 @@ +# ===== ISC Tabs Components Module ===== +module ISC.Tabs.components +SourcePanel 1.0 SourcePanel.qml +ReadoutPanel 1.0 ReadoutPanel.qml +TransportBar 1.0 TransportBar.qml +WaferMapView 1.0 WaferMapView.qml +ReplaceSensorDialog 1.0 ReplaceSensorDialog.qml diff --git a/src/pygui/ISC/Tabs/qmldir b/src/pygui/ISC/Tabs/qmldir index ad1389f..4e00d78 100644 --- a/src/pygui/ISC/Tabs/qmldir +++ b/src/pygui/ISC/Tabs/qmldir @@ -1,5 +1,8 @@ # ===== ISC Tabs Module ===== module ISC.Tabs -# ===== Tab Components ===== +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 diff --git a/src/pygui/ISC/Theme.qml b/src/pygui/ISC/Theme.qml index e554b93..fd5f317 100644 --- a/src/pygui/ISC/Theme.qml +++ b/src/pygui/ISC/Theme.qml @@ -21,15 +21,18 @@ import QtQuick QtObject { // ── 1. Mode ────────────────────────────────────────────────────────────── - property bool isDarkMode: false + property bool isDarkMode: true // ── 2. Tone palette (base values; consume via semantic tokens below) ───── readonly property color tone100: isDarkMode ? "#111111" : "#FAFAFA" - readonly property color tone200: isDarkMode ? "#1A1A1A" : "#F2F2F2" - readonly property color tone300: isDarkMode ? "#242424" : "#E8E8E8" + readonly property color tone150: isDarkMode ? "#161616" : "#F6F6F6" + readonly property color tone200: isDarkMode ? "#1A1A1A" : "#F0F0F0" + readonly property color tone250: isDarkMode ? "#212121" : "#EAEAEA" + readonly property color tone300: isDarkMode ? "#282828" : "#E2E2E2" + readonly property color tone350: isDarkMode ? "#303030" : "#D6D6D6" readonly property color toneText: isDarkMode ? "#F2F2F2" : "#111111" readonly property color toneMute: isDarkMode ? "#A8A8A8" : "#8A8A8A" - readonly property color toneBorder: isDarkMode ? "#2B2B2B" : "#D8D8D8" + readonly property color toneBorder: isDarkMode ? "#2E2E2E" : "#D4D4D4" // ── 3. Surfaces ────────────────────────────────────────────────────────── readonly property color pageBackground: tone100 @@ -41,11 +44,12 @@ QtObject { // ── 4. Borders ─────────────────────────────────────────────────────────── readonly property color cardBorder: toneBorder + readonly property color cardSurfaceBorder: isDarkMode ? "#3C3C3C" : "#C8C8C8" readonly property color responseBorder: toneBorder readonly property color workspaceBorder: toneBorder readonly property color innerFrameBorder: toneBorder - readonly property color outerFrameBorder: isDarkMode ? "#343434" : "#CECECE" - readonly property color softBorder: isDarkMode ? "#222222" : "#E2E2E2" + readonly property color outerFrameBorder: isDarkMode ? "#383838" : "#CACACA" + readonly property color softBorder: isDarkMode ? "#252525" : "#E0E0E0" // ── 5. Text ────────────────────────────────────────────────────────────── readonly property color headingColor: toneText @@ -87,6 +91,13 @@ QtObject { readonly property color sideRailBackground: tone200 readonly property color sideActiveBackground: tone300 + // ── 10a. Transport / toolbar surfaces ──────────────────────────────────── + readonly property color transportBackground: isDarkMode ? "#0D0D0D" : "#E8E8E8" + readonly property color transportButtonBg: isDarkMode ? "#2A2A2A" : "#D4D4D4" + readonly property color transportButtonHover: isDarkMode ? "#3A3A3A" : "#C4C4C4" + readonly property color liveColor: isDarkMode ? "#22C55E" : "#16A34A" + readonly property color recordColor: isDarkMode ? "#EF4444" : "#DC2626" + // ── 10. Status ─────────────────────────────────────────────────────────── readonly property color statusSuccessColor: isDarkMode ? "#63D471" : "#2E9E44" readonly property color statusWarningColor: isDarkMode ? "#F5C15C" : "#C88A18" @@ -95,16 +106,16 @@ QtObject { // -- 10b. Sensor bands (wafer map dots) readonly property color sensorInRange: statusSuccessColor readonly property color sensorHigh: statusErrorColor - readonly property color sensorlow: isDarkMode ? "#589DF5" : "#2F6FE0" + readonly property color sensorLow: isDarkMode ? "#5B9DF5" : "#2F6FE0" readonly property color waferRingColor: toneBorder readonly property color waferAxisColor: softBorder // ── 11. Geometry ───────────────────────────────────────────────────────── // Radius - readonly property int radiusXs: 4 // fields, tight elements - readonly property int radiusSm: 6 // buttons - readonly property int radiusMd: 10 // cards, group boxes - readonly property int radiusLg: 14 // large panels / dialogs + readonly property int radiusXs: 6 // fields, tight elements + readonly property int radiusSm: 8 // buttons + readonly property int radiusMd: 12 // cards, group boxes + readonly property int radiusLg: 18 // large panels / dialogs // Border width readonly property int borderThin: 1 readonly property int borderStrong: 2