Files
pyGUI/src/pygui/ISC/HomePage.qml
T
jack cfadbdf1c3 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.
2026-06-25 13:48:20 -07:00

390 lines
16 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import ISC
import QtQuick.Dialogs
// ===== Home Workspace Shell =====
Rectangle {
id: root
anchors.fill: parent
color: Theme.pageBackground
clip: true
border.color: Theme.outerFrameBorder
border.width: Theme.borderStrong
// ===== Navigation Model =====
// ---------------------------------------------------------------------------
// 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 =====
property var bottomTabs: ["Status", "Data", "Wafer Map", "Settings"]
// ===== View State =====
property int selectedTabIndex: 0
property int selectedSideActionIndex: -1
property bool memoryRead: false
property bool waferDetected: false
Connections {
target: deviceController
function onDetectResult(result){
//result is a waferInfo dict on success, None on failure
root.waferDetected = (result != null && result!= undefined)
}
function onParsedDataReady(result) {
if (result.success && result.csv_path) {
// Load the freshly read CSV into the player
streamController.loadFile(result.csv_path)
// Automatically switch to the "Wafer Map Tab" (index 3)
root.selectedTabIndex = 3
}
}
}
Connections {
target: deviceController
function onReadResult(result) {
// result has "success" key on success, "error" key on failure
root.memoryRead = (result !== null &&
result !== undefined &&
result.success === true)
if (root.memoryRead) {
console.log(`[P1.1] Memory read complete: ${result.bytes} bytes`)
} else {
console.log(`[P1.1] Read failed: ${result.error || "unknown"}`)
}
}
}
function cleanFolderUrl(url) {
return decodeURIComponent(String(url).replace(/^file:\/\//, ""))
}
function _doDetect() {
root.memoryRead = false
streamController.setMode("review")
streamController.stopStream()
deviceController.detectWafer()
}
FolderDialog {
id: saveDirDialog
title: "Choose a folder to save Data"
onAccepted: {
deviceController.setSaveDataDir(root.cleanFolderUrl(selectedFolder))
root._doDetect()
}
}
// ---------------------------------------------------------------------------
// 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"
nameFilters: ["CSV files (*.csv)", "ZWafer files (*.zwafer)", "All files (*)"]
onAccepted: {
var path = root.cleanFolderUrl(selectedFile)
streamController.setMode("review")
streamController.stopStream()
streamController.loadFile(path)
root.selectedTabIndex = 3 // Switch to Wafer Map tab
root.selectedSideActionIndex = -1
}
}
FileDialog {
id: storedDataDialog
title: "Open Stored CSV File"
nameFilters: ["CSV files (*.csv)", "All files (*)"]
onAccepted: {
var path = root.cleanFolderUrl(selectedFile)
streamController.setMode("review")
streamController.stopStream()
streamController.loadFile(path)
root.selectedTabIndex = 3 // Switch to Wafer Map tab
root.selectedSideActionIndex = -1
}
}
// ===== 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
// ===== Left Action Rail =====
// Left control rail.
Rectangle {
id: sideRail
Layout.preferredWidth: Theme.sideRailWidth
Layout.fillHeight: true
color: Theme.sideRailBackground
border.color: Theme.workspaceBorder
border.width: Theme.borderThin
readonly property int actionCount: root.sideActions.length
readonly property real computedButtonHeight: Math.min(Theme.sideButtonHeight, (height - (Theme.panelPadding * 2) - (Theme.sideRailSpacing * Math.max(0, actionCount - 1))) / Math.max(1, actionCount))
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.panelPadding
spacing: Theme.sideRailSpacing
Repeater {
model: root.sideActions
Button {
id: control
text: modelData
enabled: {
switch (index) {
case 0: return true // DETECT WAFER
case 1: return root.waferDetected // READ MEMORY
case 2: return root.waferDetected // ERASE MEMORY
default: return true
}
}
property bool isActive: index === root.selectedSideActionIndex
Layout.fillWidth: true
Layout.preferredHeight: Math.max(Theme.sideButtonMinHeight, sideRail.computedButtonHeight)
hoverEnabled: true
onClicked: {
root.selectedSideActionIndex = index
root.selectedTabIndex = 0 // always jump to Status tab
if (index === 0) {
if (!deviceController.saveDataDir) {
saveDirDialog.open()
return
}
root._doDetect()
}
else if (index === 1) {
streamController.setMode("review")
streamController.stopStream()
deviceController.readMemoryAsync()
}
else if (index === 2) {
// ERASE MEMORY
streamController.setMode("review")
streamController.stopStream()
deviceController.eraseMemory(deviceController.selectedPort || "")
}
}
background: Rectangle {
color: {
if (control.down) {
return Theme.buttonPressed;
}
if (control.isActive) {
return Theme.sideActiveBackground;
}
return "transparent";
}
border.color: control.isActive ? Theme.cardBorder : "transparent"
border.width: 1
radius: Theme.radiusSm
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: 3
visible: control.isActive
color: Theme.primaryAccent
radius: Theme.radiusXs
}
}
contentItem: Text {
text: control.text
color: control.isActive ? Theme.headingColor : Theme.bodyColor
font.bold: control.isActive
font.pixelSize: 18
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
}
}
}
// Main workspace and footer navigation.
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Theme.workspaceBackground
border.color: Theme.workspaceBorder
border.width: Theme.borderThin
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.mainAreaPadding
spacing: Theme.rightPaneGap
// ===== Active Tab Content Area =====
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 220
color: Theme.responseBackground
border.color: Theme.responseBorder
border.width: 1
radius: Theme.radiusMd
StackLayout {
anchors.fill: parent
currentIndex: root.selectedTabIndex
// ===== Tab Content Routing =====
Repeater {
model: root.bottomTabs
Item {
property string tabName: modelData
Loader {
anchors.fill: parent
active: parent.tabName === "Settings"
source: parent.tabName === "Settings" ? "Tabs/SettingsTab.qml" : ""
}
Loader {
anchors.fill: parent
active: parent.tabName === "Status"
source: parent.tabName === "Status" ? "Tabs/StatusTab.qml" : ""
}
Loader {
anchors.fill: parent
active: parent.tabName === "Data"
source: parent.tabName === "Data" ? "Tabs/DataTab.qml" : ""
}
Label {
anchors.centerIn: parent
visible: parent.tabName !== "Settings" && parent.tabName !== "Status" && parent.tabName !== "Data" && parent.tabName !== "Wafer Map" && parent.tabName !== "Graph"
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" : ""
}
Loader {
anchors.fill: parent
active: parent.tabName === "Graph"
source: parent.tabName === "Graph" ? "Tabs/GraphTab.qml" : ""
}
}
}
}
}
// Keep the tab strip evenly distributed across the footer.
// ===== Footer Tab Strip =====
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Theme.tabBarHeight + (Theme.tabBarPadding * 2)
color: Theme.tabBarBackground
border.color: Theme.workspaceBorder
border.width: Theme.borderThin
radius: Theme.radiusMd
RowLayout {
anchors.fill: parent
anchors.margins: Theme.tabBarPadding
spacing: Theme.tabSpacing
Repeater {
model: root.bottomTabs
Button {
id: botTabBtn
property bool isActive: index === root.selectedTabIndex
text: modelData
Layout.fillWidth: true
Layout.preferredHeight: Theme.tabBarHeight
Layout.minimumWidth: Theme.tabButtonMinWidth
onClicked: root.selectedTabIndex = index
hoverEnabled: true
background: Rectangle {
color: botTabBtn.isActive ? Theme.tabActiveBackground : (botTabBtn.hovered ? Theme.tabHoverBackground : Theme.tabBackground)
border.color: Theme.tabBorder
border.width: 1
radius: Theme.tabRadius
}
contentItem: Text {
text: botTabBtn.text
color: botTabBtn.isActive ? Theme.tabActiveText : Theme.tabText
font.pixelSize: Theme.tabFontSize
font.bold: botTabBtn.isActive
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
}
}
}
}
}
}
}
}