feat(ui): decouple QML page modules and update tab layouts

Extract ReadoutPanel, WaferMapView, and TransportBar into reusable components and bind tabs to the new session controllers.
This commit is contained in:
jack
2026-06-25 13:48:20 -07:00
parent f4621f1faf
commit cfadbdf1c3
6 changed files with 205 additions and 79 deletions
+54 -20
View File
@@ -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 {
+24
View File
@@ -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
+111 -59
View File
@@ -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
@@ -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"
@@ -40,6 +40,10 @@ Item {
ReplaceSensorDialog { id: replaceDialog}
function exportImage(filePath) {
return map.export_image(filePath)
}
}
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.29289 1.29289C9.48043 1.10536 9.73478 1 10 1H18C19.6569 1 21 2.34315 21 4V9C21 9.55228 20.5523 10 20 10C19.4477 10 19 9.55228 19 9V4C19 3.44772 18.5523 3 18 3H11V8C11 8.55228 10.5523 9 10 9H5V20C5 20.5523 5.44772 21 6 21H7C7.55228 21 8 21.4477 8 22C8 22.5523 7.55228 23 7 23H6C4.34315 23 3 21.6569 3 20V8C3 7.73478 3.10536 7.48043 3.29289 7.29289L9.29289 1.29289ZM6.41421 7H9V4.41421L6.41421 7ZM19 12C19.5523 12 20 12.4477 20 13V19H23C23.5523 19 24 19.4477 24 20C24 20.5523 23.5523 21 23 21H19C18.4477 21 18 20.5523 18 20V13C18 12.4477 18.4477 12 19 12ZM11.8137 12.4188C11.4927 11.9693 10.8682 11.8653 10.4188 12.1863C9.96935 12.5073 9.86526 13.1318 10.1863 13.5812L12.2711 16.5L10.1863 19.4188C9.86526 19.8682 9.96935 20.4927 10.4188 20.8137C10.8682 21.1347 11.4927 21.0307 11.8137 20.5812L13.5 18.2205L15.1863 20.5812C15.5073 21.0307 16.1318 21.1347 16.5812 20.8137C17.0307 20.4927 17.1347 19.8682 16.8137 19.4188L14.7289 16.5L16.8137 13.5812C17.1347 13.1318 17.0307 12.5073 16.5812 12.1863C16.1318 11.8653 15.5073 11.9693 15.1863 12.4188L13.5 14.7795L11.8137 12.4188Z" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB