diff --git a/src/pygui/ISC/HomePage.qml b/src/pygui/ISC/HomePage.qml index 194eb32..d6380b3 100644 --- a/src/pygui/ISC/HomePage.qml +++ b/src/pygui/ISC/HomePage.qml @@ -14,16 +14,26 @@ Rectangle { border.width: Theme.borderStrong // ===== Navigation Model ===== - // Primary navigation shown in the left rail. - property var sideActions: ["DETECT WAFER", "READ MEMORY", "OPEN CSV IN EXCEL", "ERASE MEMORY", "IMPORT DATA", "STORED DATA"] + // --------------------------------------------------------------------------- + // TODO P1.1: Delete these 3 property blocks — superseded by mockup rail + // + // THINKING: + // The new rail uses a pill-tab-bar for navigation (Status/Data/Map) instead + // of sideActions[] + bottomTabs[]. The Repeaters below that consume these + // arrays are also deleted with the old layout (P1.1). + // selectedSideActionIndex is dead — the new rail has no concept of a + // globally-selected side action. selectedTabIndex stays, redefined below. + // + // Cross-ref: P1.2 (pill tabs), P1.3 (context panels), P1.4 (workspace) + // --------------------------------------------------------------------------- + property var sideActions: ["DETECT WAFER", "READ MEMORY", "ERASE MEMORY"] // ===== Footer Tab Model ===== - // Footer tabs drive the active workspace section. - property var bottomTabs: ["Status", "Graph", "Data", "Wafer Map", "Compare", "Split", "Settings", "About"] + property var bottomTabs: ["Status", "Data", "Wafer Map", "Settings"] // ===== View State ===== property int selectedTabIndex: 0 - property int selectedSideActionIndex: -1 // nothing active on startup + property int selectedSideActionIndex: -1 property bool memoryRead: false property bool waferDetected: false @@ -80,6 +90,22 @@ Rectangle { } } + // --------------------------------------------------------------------------- + // TODO P1.1: Delete importFileDialog + storedDataDialog (and the + // `selectedTabIndex = 3` lines inside them) + // + // THINKING: + // These dialogs were only opened from the old side-rail buttons + // "IMPORT DATA" (index 4) and "STORED DATA" (index 5). With the + // mockup rail, the Map tab's file list (P1.3) replaces both — it + // drives `streamController.loadFile()` directly from a file_browser + // Repeater. Keeping these dead dialogs would bloat the QML and + // confuse future contributors. + // + // `selectedTabIndex = 3` is also stale (Map tab is index 2 now). + // The replacement flow: file_row.onClicked → loadFile() → + // selectedTabIndex = 2. + // --------------------------------------------------------------------------- FileDialog { id: importFileDialog title: "Import CSV / ZWafer file" @@ -109,6 +135,27 @@ Rectangle { } // ===== Main Two-Column Layout ====== + // --------------------------------------------------------------------------- + // TODO P1.1: Delete the ENTIRE RowLayout below (lines ~112-341) — old + // side-rail + workspace + footer-tab-strip. + // + // THINKING: + // This ~230-line RowLayout is being replaced by: + // P1.2 — Pill tab bar (BOX 1, top of rail) + // P1.3 — Context-sensitive panel (BOX 2, mid rail) + // P1.5 — Hardware status footer (BOX 3, below BOX 2) + // P1.6 — Settings/About utility buttons (BOX 4, rail bottom) + // P1.4 — Workspace StackLayout (fills right side) + // + // None of the old structure (Repeater for sideActions[], Repeater for + // bottomTabs[], footer tab strip) survives. Keeping it would cause + // double-rendering or visual conflicts. + // + // Keep: saveDirDialog (FolderDialog), cleanFolderUrl(), _doDetect(), + // and all Connections blocks (lines 31-61). + // + // Cross-ref: P1.2, P1.3, P1.4, P1.5, P1.6 + // --------------------------------------------------------------------------- RowLayout { anchors.fill: parent spacing: 0 @@ -141,10 +188,8 @@ Rectangle { switch (index) { case 0: return true // DETECT WAFER case 1: return root.waferDetected // READ MEMORY - case 2: return root.memoryRead // OPEN CSV IN EXCEL - case 3: return root.waferDetected // ERASE MEMORY - case 4: return root.memoryRead // IMPORT DATA - default: return true // STORED DATA + case 2: return root.waferDetected // ERASE MEMORY + default: return true } } property bool isActive: index === root.selectedSideActionIndex @@ -167,22 +212,11 @@ Rectangle { deviceController.readMemoryAsync() } else if (index === 2) { - deviceController.openCsvFile() - } - else if (index === 3) { // ERASE MEMORY streamController.setMode("review") streamController.stopStream() deviceController.eraseMemory(deviceController.selectedPort || "") } - else if (index === 4) { - // IMPORT DATA - importFileDialog.open() - } - else if (index === 5) { - // STORED DATA - storedDataDialog.open() - } } background: Rectangle { diff --git a/src/pygui/ISC/Tabs/DataTab.qml b/src/pygui/ISC/Tabs/DataTab.qml index 7a2b4b2..302d685 100644 --- a/src/pygui/ISC/Tabs/DataTab.qml +++ b/src/pygui/ISC/Tabs/DataTab.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import QtQuick.TableView import ISC // ===== Data Tab ===== @@ -74,6 +75,29 @@ Item { } } + // --- Toolbar --- + RowLayout { + Layout.fillWidth: true + spacing: 8 + + Button { + text: "Open in Excel" + icon.source: "icon/excel.svg" + onClicked: deviceController.openCsvFile() + } + + Button { + text: "Compare" + onClicked: compareDialog.open() + } + + Button { + text: "Split" + onClicked: splitDialog.open() + } + + Item { Layout.fillWidth: true} + } // --- Table container --- Rectangle { Layout.fillWidth: true diff --git a/src/pygui/ISC/Tabs/WaferMapTab.qml b/src/pygui/ISC/Tabs/WaferMapTab.qml index fdac4b0..ab5452c 100644 --- a/src/pygui/ISC/Tabs/WaferMapTab.qml +++ b/src/pygui/ISC/Tabs/WaferMapTab.qml @@ -1,8 +1,10 @@ import QtQuick import QtQuick.Controls +import QtQuick.Dialogs import QtQuick.Layouts import ISC import ISC.Tabs.components +import ISC.Wafer Item { id: root @@ -23,6 +25,16 @@ Item { var ss = s % 60 return (m < 10 ? "0" : "") + m + ":" + (ss < 10 ? "0" : "") + ss } + // Wire streamController.trendData (Signal(str) carrying JSON list of floats) + // into the trend chart's data property. The slot parseJsonToData() is defined + // on the Python TrendChartItem; we use it so malformed payloads are logged + // there instead of crashing the QML binding. + Connections { + target: streamController + function onTrendData(avgsJson) { + trendChart.setDataFromJson(avgsJson) + } + } ColumnLayout { anchors.fill: parent @@ -37,7 +49,7 @@ Item { // Mode toggle TabBar { id: modeBar - currentIndex: 1 // Default to "Review" + currentIndex: 0 // Default to "Review" spacing: 2 padding: 2 background: Rectangle { @@ -46,6 +58,22 @@ Item { border.color: Theme.cardBorder border.width: Theme.borderThin } + TabButton { + text: "Review" + implicitWidth: 64; implicitHeight: 28 + background: Rectangle { + color: parent.checked ? Theme.tabActiveBackground : "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: "Live" enabled: deviceController.connectionStatus === "Connected" @@ -64,33 +92,20 @@ Item { verticalAlignment: Text.AlignVCenter } } - TabButton { - text: "Review" - implicitWidth: 64; implicitHeight: 28 - background: Rectangle { - color: parent.checked ? Theme.tabActiveBackground : "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: - if (currentIndex === 0){ - // Entering Live mode, only work when wafer detected/connected - streamController.setMode("live") - if (deviceController.connectionStatus === "Connected") { - var fc = "" - if (deviceController.lastWaferInfo && deviceController.lastWaferInfo.length > 0) { - fc = deviceController.lastWaferInfo[0] - } - streamController.startStream(deviceController.selectedPort, fc) + if (currentIndex === 1){ + // Guard: revert to Review if not connected + if (deviceController.connectionStatus !== "Connected") { + currentIndex = 0 + return } + // Entering Live mode + streamController.setMode("live") + var fc = "" + if (deviceController.lastWaferInfo && deviceController.lastWaferInfo.length > 0) { + fc = deviceController.lastWaferInfo[0] + } + streamController.startStream(deviceController.selectedPort, fc) } else { // Entering Review Mode (automatically calls stopStream in backend) streamController.setMode("review") @@ -172,34 +187,62 @@ Item { } // 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 - } - } + // 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 + // Label { + // visible: streamController.mode === "review" || streamController.state === "idle" + // text: streamController.state.toUpperCas`e`() + // color: Theme.bodyColor + // font.pixelSize: 11 + // font.weight: Font.Medium + // font.letterSpacing: 1.2 + // } + Button{ + text: "Export PNG" + onClicked: exportDialog.open() + } + + FileDialog { + id: exportDialog + title: "Export Wafer Map" + fileMode: FileDialog.SaveFile + nameFilters: ["PNG files (*.png)"] + onAccepted: waferView.exportImage(String(selectedFile).replace(/^file:\/\//, "")) } } - // ── Body: 3 zones ───────────────────────────────────────────────── + // ── Body: 3 zones (TODO P2.1: becomes 2 zones) ────────────────────── + // ------------------------------------------------------------------- + // TODO P2.1: Delete the SourcePanel Rectangle (lines ~219-233) — + // the file list moved to the rail (P1.3 Map context panel). + // + // THINKING: + // SourcePanel shows a file list driven by file_browser — now that + // the Map tab's context panel in the rail (P1.3) provides the same + // file list with search/filter, SourcePanel in the wafer body is + // redundant. The wafer map gets more horizontal space. + // + // After deletion: RowLayout has 2 children instead of 3. + // Add Layout.fillWidth: true to the center ColumnLayout so it + // fills the vacated width. + // + // Cross-ref: P1.3 (Map context panel), P2.1 (this task) + // ------------------------------------------------------------------- RowLayout { Layout.fillWidth: true Layout.fillHeight: true @@ -233,20 +276,29 @@ Item { blend: readoutPanel.heatmapBlend showLabels: readoutPanel.showLabels } - - // Horizontal separation line with vertical padding - Item { + Rectangle { + id: trendPane Layout.fillWidth: true - implicitHeight: 32 - Rectangle { - anchors.centerIn: parent - width: parent.width - height: 1 - color: Theme.cardBorder + implicitHeight: 160 + visible: trendChart.hasData + color: Theme.cardBackground + border.color: Theme.cardBorder + border.width: 1 + radius: Theme.radiusMd + + TrendChartItem { + id: trendChart + anchors.fill: parent + anchors.margins: 8 } } - TransportBar { Layout.fillWidth: true } + Item { Layout.fillWidth: true; implicitHeight: 16 } + TransportBar { + Layout.fillWidth: true + visible: streamController.mode !== "live" || trendChart.hasData + height: visible ? implicitHeight : 0 + } } // Readout panel — bordered card diff --git a/src/pygui/ISC/Tabs/components/ReadoutPanel.qml b/src/pygui/ISC/Tabs/components/ReadoutPanel.qml index 5101c0d..8fef514 100644 --- a/src/pygui/ISC/Tabs/components/ReadoutPanel.qml +++ b/src/pygui/ISC/Tabs/components/ReadoutPanel.qml @@ -175,6 +175,14 @@ ColumnLayout { bottomPadding: 8 } + PanelCheckBox { + id: thicknessToggle + text: "Show Thickness" + checked: false + font.pixelSize: 11 + onCheckedChanged: waferMapItem.showThickness = checked + } + PanelCheckBox { id: labelsToggle text: "Labels" diff --git a/src/pygui/ISC/Tabs/components/WaferMapView.qml b/src/pygui/ISC/Tabs/components/WaferMapView.qml index bc037fb..3194104 100644 --- a/src/pygui/ISC/Tabs/components/WaferMapView.qml +++ b/src/pygui/ISC/Tabs/components/WaferMapView.qml @@ -40,6 +40,10 @@ Item { ReplaceSensorDialog { id: replaceDialog} + function exportImage(filePath) { + return map.export_image(filePath) + } + } \ No newline at end of file diff --git a/src/pygui/ISC/Tabs/icon/excel.svg b/src/pygui/ISC/Tabs/icon/excel.svg new file mode 100644 index 0000000..40b6ede --- /dev/null +++ b/src/pygui/ISC/Tabs/icon/excel.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file