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:
+54
-20
@@ -14,16 +14,26 @@ Rectangle {
|
|||||||
border.width: Theme.borderStrong
|
border.width: Theme.borderStrong
|
||||||
|
|
||||||
// ===== Navigation Model =====
|
// ===== 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 Tab Model =====
|
||||||
// Footer tabs drive the active workspace section.
|
property var bottomTabs: ["Status", "Data", "Wafer Map", "Settings"]
|
||||||
property var bottomTabs: ["Status", "Graph", "Data", "Wafer Map", "Compare", "Split", "Settings", "About"]
|
|
||||||
|
|
||||||
// ===== View State =====
|
// ===== View State =====
|
||||||
property int selectedTabIndex: 0
|
property int selectedTabIndex: 0
|
||||||
property int selectedSideActionIndex: -1 // nothing active on startup
|
property int selectedSideActionIndex: -1
|
||||||
|
|
||||||
property bool memoryRead: false
|
property bool memoryRead: false
|
||||||
property bool waferDetected: 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 {
|
FileDialog {
|
||||||
id: importFileDialog
|
id: importFileDialog
|
||||||
title: "Import CSV / ZWafer file"
|
title: "Import CSV / ZWafer file"
|
||||||
@@ -109,6 +135,27 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== Main Two-Column Layout ======
|
// ===== 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 {
|
RowLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: 0
|
spacing: 0
|
||||||
@@ -141,10 +188,8 @@ Rectangle {
|
|||||||
switch (index) {
|
switch (index) {
|
||||||
case 0: return true // DETECT WAFER
|
case 0: return true // DETECT WAFER
|
||||||
case 1: return root.waferDetected // READ MEMORY
|
case 1: return root.waferDetected // READ MEMORY
|
||||||
case 2: return root.memoryRead // OPEN CSV IN EXCEL
|
case 2: return root.waferDetected // ERASE MEMORY
|
||||||
case 3: return root.waferDetected // ERASE MEMORY
|
default: return true
|
||||||
case 4: return root.memoryRead // IMPORT DATA
|
|
||||||
default: return true // STORED DATA
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
property bool isActive: index === root.selectedSideActionIndex
|
property bool isActive: index === root.selectedSideActionIndex
|
||||||
@@ -167,22 +212,11 @@ Rectangle {
|
|||||||
deviceController.readMemoryAsync()
|
deviceController.readMemoryAsync()
|
||||||
}
|
}
|
||||||
else if (index === 2) {
|
else if (index === 2) {
|
||||||
deviceController.openCsvFile()
|
|
||||||
}
|
|
||||||
else if (index === 3) {
|
|
||||||
// ERASE MEMORY
|
// ERASE MEMORY
|
||||||
streamController.setMode("review")
|
streamController.setMode("review")
|
||||||
streamController.stopStream()
|
streamController.stopStream()
|
||||||
deviceController.eraseMemory(deviceController.selectedPort || "")
|
deviceController.eraseMemory(deviceController.selectedPort || "")
|
||||||
}
|
}
|
||||||
else if (index === 4) {
|
|
||||||
// IMPORT DATA
|
|
||||||
importFileDialog.open()
|
|
||||||
}
|
|
||||||
else if (index === 5) {
|
|
||||||
// STORED DATA
|
|
||||||
storedDataDialog.open()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.TableView
|
||||||
import ISC
|
import ISC
|
||||||
|
|
||||||
// ===== Data Tab =====
|
// ===== 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 ---
|
// --- Table container ---
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Dialogs
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import ISC
|
import ISC
|
||||||
import ISC.Tabs.components
|
import ISC.Tabs.components
|
||||||
|
import ISC.Wafer
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
@@ -23,6 +25,16 @@ Item {
|
|||||||
var ss = s % 60
|
var ss = s % 60
|
||||||
return (m < 10 ? "0" : "") + m + ":" + (ss < 10 ? "0" : "") + ss
|
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 {
|
ColumnLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -37,7 +49,7 @@ Item {
|
|||||||
// Mode toggle
|
// Mode toggle
|
||||||
TabBar {
|
TabBar {
|
||||||
id: modeBar
|
id: modeBar
|
||||||
currentIndex: 1 // Default to "Review"
|
currentIndex: 0 // Default to "Review"
|
||||||
spacing: 2
|
spacing: 2
|
||||||
padding: 2
|
padding: 2
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
@@ -46,6 +58,22 @@ Item {
|
|||||||
border.color: Theme.cardBorder
|
border.color: Theme.cardBorder
|
||||||
border.width: Theme.borderThin
|
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 {
|
TabButton {
|
||||||
text: "Live"
|
text: "Live"
|
||||||
enabled: deviceController.connectionStatus === "Connected"
|
enabled: deviceController.connectionStatus === "Connected"
|
||||||
@@ -64,33 +92,20 @@ Item {
|
|||||||
verticalAlignment: Text.AlignVCenter
|
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:
|
onCurrentIndexChanged:
|
||||||
if (currentIndex === 0){
|
if (currentIndex === 1){
|
||||||
// Entering Live mode, only work when wafer detected/connected
|
// Guard: revert to Review if not connected
|
||||||
|
if (deviceController.connectionStatus !== "Connected") {
|
||||||
|
currentIndex = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Entering Live mode
|
||||||
streamController.setMode("live")
|
streamController.setMode("live")
|
||||||
if (deviceController.connectionStatus === "Connected") {
|
|
||||||
var fc = ""
|
var fc = ""
|
||||||
if (deviceController.lastWaferInfo && deviceController.lastWaferInfo.length > 0) {
|
if (deviceController.lastWaferInfo && deviceController.lastWaferInfo.length > 0) {
|
||||||
fc = deviceController.lastWaferInfo[0]
|
fc = deviceController.lastWaferInfo[0]
|
||||||
}
|
}
|
||||||
streamController.startStream(deviceController.selectedPort, fc)
|
streamController.startStream(deviceController.selectedPort, fc)
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Entering Review Mode (automatically calls stopStream in backend)
|
// Entering Review Mode (automatically calls stopStream in backend)
|
||||||
streamController.setMode("review")
|
streamController.setMode("review")
|
||||||
@@ -172,34 +187,62 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LIVE timer
|
// LIVE timer
|
||||||
RowLayout {
|
// RowLayout {
|
||||||
spacing: 5
|
// spacing: 5
|
||||||
visible: streamController.mode === "live" && streamController.state !== "idle"
|
// visible: streamController.mode === "live" && streamController.state !== "idle"
|
||||||
Rectangle {
|
// Rectangle {
|
||||||
width: 7; height: 7; radius: 4
|
// width: 7; height: 7; radius: 4
|
||||||
color: Theme.liveColor
|
// color: Theme.liveColor
|
||||||
}
|
// }
|
||||||
Label {
|
// Label {
|
||||||
text: "LIVE " + root.fmtTime(root._liveSecs)
|
// text: "LIVE " + root.fmtTime(root._liveSecs)
|
||||||
color: Theme.liveColor
|
// color: Theme.liveColor
|
||||||
font.pixelSize: 10
|
// font.pixelSize: 10
|
||||||
font.weight: Font.Medium
|
// font.weight: Font.Medium
|
||||||
font.letterSpacing: 0.8
|
// font.letterSpacing: 0.8
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// IDLE label (review / not connected)
|
// IDLE label (review / not connected)
|
||||||
Label {
|
// Label {
|
||||||
visible: streamController.mode === "review" || streamController.state === "idle"
|
// visible: streamController.mode === "review" || streamController.state === "idle"
|
||||||
text: streamController.state.toUpperCase()
|
// text: streamController.state.toUpperCas`e`()
|
||||||
color: Theme.bodyColor
|
// color: Theme.bodyColor
|
||||||
font.pixelSize: 11
|
// font.pixelSize: 11
|
||||||
font.weight: Font.Medium
|
// font.weight: Font.Medium
|
||||||
font.letterSpacing: 1.2
|
// 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 {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
@@ -233,20 +276,29 @@ Item {
|
|||||||
blend: readoutPanel.heatmapBlend
|
blend: readoutPanel.heatmapBlend
|
||||||
showLabels: readoutPanel.showLabels
|
showLabels: readoutPanel.showLabels
|
||||||
}
|
}
|
||||||
|
|
||||||
// Horizontal separation line with vertical padding
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
implicitHeight: 32
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.centerIn: parent
|
id: trendPane
|
||||||
width: parent.width
|
Layout.fillWidth: true
|
||||||
height: 1
|
implicitHeight: 160
|
||||||
color: Theme.cardBorder
|
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
|
// Readout panel — bordered card
|
||||||
|
|||||||
@@ -175,6 +175,14 @@ ColumnLayout {
|
|||||||
bottomPadding: 8
|
bottomPadding: 8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PanelCheckBox {
|
||||||
|
id: thicknessToggle
|
||||||
|
text: "Show Thickness"
|
||||||
|
checked: false
|
||||||
|
font.pixelSize: 11
|
||||||
|
onCheckedChanged: waferMapItem.showThickness = checked
|
||||||
|
}
|
||||||
|
|
||||||
PanelCheckBox {
|
PanelCheckBox {
|
||||||
id: labelsToggle
|
id: labelsToggle
|
||||||
text: "Labels"
|
text: "Labels"
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ Item {
|
|||||||
|
|
||||||
ReplaceSensorDialog { id: replaceDialog}
|
ReplaceSensorDialog { id: replaceDialog}
|
||||||
|
|
||||||
|
function exportImage(filePath) {
|
||||||
|
return map.export_image(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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 |
Reference in New Issue
Block a user