feat(home): redesign left rail with pill tab bar, connection flow, and action panels
- Add animated pill tab bar (STATUS / DATA / MAP) - Restructure context panel with StackLayout for per-tab content - Redesign CONNECTION FLOW widget: remove dotted trail, add status dot + descriptive text, neutral gray mode chip - Add HARDWARE ACTIONS and DATA OPERATIONS panels - Add settings/about footer with icon buttons
This commit is contained in:
+541
-287
@@ -1,10 +1,14 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Controls.impl
|
||||
import QtQuick.Layouts
|
||||
import ISC
|
||||
import ISC.Tabs.components
|
||||
import QtQuick.Dialogs
|
||||
|
||||
// ===== Home Workspace Shell =====
|
||||
// Unified left rail: pill tab bar → context panel → status footer → utility buttons.
|
||||
// Workspace fills the remaining area with the active tab's content.
|
||||
Rectangle {
|
||||
id: root
|
||||
anchors.fill: parent
|
||||
@@ -13,27 +17,8 @@ Rectangle {
|
||||
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
|
||||
@@ -41,31 +26,25 @@ Rectangle {
|
||||
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
|
||||
root.selectedTabIndex = 2
|
||||
}
|
||||
}
|
||||
}
|
||||
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`)
|
||||
console.log("[P1.1] Memory read complete:", result.bytes, "bytes")
|
||||
} else {
|
||||
console.log(`[P1.1] Read failed: ${result.error || "unknown"}`)
|
||||
console.log("[P1.1] Read failed:", result.error || "unknown")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,298 +69,573 @@ 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"
|
||||
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
|
||||
}
|
||||
// ===== Settings Popup =====
|
||||
Popup {
|
||||
id: settingsPopup
|
||||
modal: true
|
||||
dim: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
width: Math.min(root.width - 80, 800)
|
||||
height: Math.min(root.height - 80, 600)
|
||||
anchors.centerIn: Overlay.overlay
|
||||
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusMd
|
||||
color: Theme.cardBackground
|
||||
border.color: Theme.cardBorder
|
||||
border.width: 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))
|
||||
contentItem: Rectangle {
|
||||
color: Theme.pageBackground
|
||||
radius: Theme.radiusMd
|
||||
clip: true
|
||||
|
||||
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
|
||||
spacing: 0
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: 3
|
||||
visible: control.isActive
|
||||
color: Theme.primaryAccent
|
||||
radius: Theme.radiusXs
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 40
|
||||
color: Theme.panelBackground
|
||||
border.color: Theme.cardBorder
|
||||
border.width: 1
|
||||
radius: Theme.radiusMd
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 14
|
||||
anchors.rightMargin: 4
|
||||
|
||||
Label {
|
||||
text: "Settings"
|
||||
font.pixelSize: Theme.fontLg
|
||||
font.bold: true
|
||||
color: Theme.headingColor
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: control.text
|
||||
color: control.isActive ? Theme.headingColor : Theme.bodyColor
|
||||
font.bold: control.isActive
|
||||
font.pixelSize: 18
|
||||
wrapMode: Text.WordWrap
|
||||
Button {
|
||||
text: "\u2715"
|
||||
flat: true
|
||||
Layout.preferredWidth: 32
|
||||
Layout.preferredHeight: 32
|
||||
onClicked: settingsPopup.close()
|
||||
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusXs
|
||||
color: parent.hovered ? Theme.buttonNeutralHover : "transparent"
|
||||
}
|
||||
contentItem: Label {
|
||||
text: parent.text
|
||||
color: Theme.bodyColor
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.rightMargin: 6
|
||||
clip: true
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
source: "Tabs/SettingsTab.qml"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main workspace and footer navigation.
|
||||
// ===== About Dialog =====
|
||||
AboutDialog {
|
||||
id: aboutDialog
|
||||
}
|
||||
|
||||
// ===== Main Layout: Rail + Workspace =====
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
// ── LEFT RAIL ──────────────────────────────────────────────────────
|
||||
Rectangle {
|
||||
id: rail
|
||||
Layout.preferredWidth: Theme.sideRailWidth
|
||||
Layout.fillHeight: true
|
||||
color: Theme.sideRailBackground
|
||||
border.color: Theme.sideBorder
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.sideRailMargin
|
||||
spacing: Theme.sideRailSpacing
|
||||
|
||||
// ── BOX 1: Pill Tab Bar ─────────────────────────────────────
|
||||
Rectangle {
|
||||
id: pillBar
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Theme.sidePillHeight
|
||||
radius: 999
|
||||
border.color: Theme.sideBorder
|
||||
border.width: 1
|
||||
color: "transparent"
|
||||
|
||||
ListModel {
|
||||
id: pillModel
|
||||
ListElement { label: "STATUS"; icon: "Tabs/icons/status.svg"; expandW: 86 }
|
||||
ListElement { label: "DATA"; icon: "Tabs/icons/data.svg"; expandW: 66 }
|
||||
ListElement { label: "MAP"; icon: "Tabs/icons/map.svg"; expandW: 58 }
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 3
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: pillModel
|
||||
delegate: Button {
|
||||
id: pillBtn
|
||||
checked: root.selectedTabIndex === index
|
||||
flat: true
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: pillBtn.checked ? model.expandW : 44
|
||||
Behavior on Layout.preferredWidth {
|
||||
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
radius: 50
|
||||
color: pillBtn.checked ? Theme.sidePanelBackground : "transparent"
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.durationFast }
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: Row {
|
||||
id: pillContent
|
||||
spacing: pillBtn.checked ? 6 : 0
|
||||
width: pillIcon.width + (pillBtn.checked ? pillContent.spacing + pillLabel.width : 0)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: (parent.width - width) / 2
|
||||
Behavior on x {
|
||||
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
IconImage {
|
||||
id: pillIcon
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 16; height: 16
|
||||
source: model.icon
|
||||
sourceSize.width: 16; sourceSize.height: 16
|
||||
color: pillBtn.checked ? Theme.headingColor
|
||||
: pillBtn.hovered ? Qt.lighter(Theme.sideMutedText, 1.7)
|
||||
: Theme.sideMutedText
|
||||
opacity: pillBtn.checked ? 0.9 : pillBtn.hovered ? 0.7 : 0.4
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.durationFast }
|
||||
}
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: Theme.durationFast }
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
id: pillLabel
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: pillBtn.checked ? implicitWidth : 0
|
||||
text: model.label
|
||||
color: pillBtn.checked ? Theme.headingColor : Theme.sideMutedText
|
||||
font.family: Theme.uiFontFamily
|
||||
font.pixelSize: Theme.fontSm
|
||||
font.bold: true
|
||||
font.letterSpacing: 1.0
|
||||
opacity: pillBtn.checked ? 1.0 : 0.0
|
||||
clip: true
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
|
||||
}
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.durationBase; easing.type: Theme.easeStandard }
|
||||
}
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: root.selectedTabIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── BOX 2: Context-Sensitive Rail Panel ────────────────────
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.minimumHeight: 120
|
||||
radius: Theme.sidePanelRadius
|
||||
border.color: Theme.sideBorder
|
||||
border.width: 1
|
||||
color: Theme.sidePanelBackground
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 14
|
||||
spacing: 8
|
||||
|
||||
StackLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
currentIndex: root.selectedTabIndex
|
||||
|
||||
// ── Status tab: Hardware Actions ────────────────
|
||||
ColumnLayout {
|
||||
spacing: 6
|
||||
|
||||
Label {
|
||||
text: "HARDWARE ACTIONS"
|
||||
color: Theme.sideMutedText
|
||||
font.family: Theme.uiFontFamily
|
||||
font.pixelSize: Theme.fontSm
|
||||
topPadding: 6
|
||||
font.letterSpacing: 1.5
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
RailActionButton {
|
||||
label: "DETECT WAFER"
|
||||
iconSource: "../icons/detect.svg"
|
||||
Layout.fillWidth: true
|
||||
enabled: !deviceController.isDetecting
|
||||
onClicked: root._doDetect()
|
||||
}
|
||||
|
||||
RailActionButton {
|
||||
label: "READ MEMORY"
|
||||
iconSource: "../icons/read.svg"
|
||||
Layout.fillWidth: true
|
||||
onClicked: deviceController.readMemoryAsync()
|
||||
}
|
||||
|
||||
RailActionButton {
|
||||
label: "ERASE MEMORY"
|
||||
iconSource: "../icons/erase.svg"
|
||||
Layout.fillWidth: true
|
||||
destructive: true
|
||||
onClicked: deviceController.eraseMemory(
|
||||
deviceController.selectedPort)
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
}
|
||||
|
||||
// ── Data tab: Data Operations ───────────────────
|
||||
ColumnLayout {
|
||||
spacing: 6
|
||||
|
||||
Label {
|
||||
text: "DATA OPERATIONS"
|
||||
color: Theme.sideMutedText
|
||||
font.family: Theme.uiFontFamily
|
||||
font.pixelSize: Theme.fontSm
|
||||
topPadding: 6
|
||||
font.letterSpacing: 1.5
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
RailActionButton {
|
||||
label: "OPEN IN EXCEL"
|
||||
iconSource: "../icons/excel.svg"
|
||||
Layout.fillWidth: true
|
||||
onClicked: deviceController.openCsvFile()
|
||||
}
|
||||
|
||||
RailActionButton {
|
||||
label: "COMPARE CSV"
|
||||
iconSource: "../icons/compare.svg"
|
||||
Layout.fillWidth: true
|
||||
onClicked: console.log("[P3.1] Compare dialog not yet built")
|
||||
}
|
||||
|
||||
RailActionButton {
|
||||
label: "SPLIT DATA"
|
||||
iconSource: "../icons/split.svg"
|
||||
Layout.fillWidth: true
|
||||
onClicked: console.log("[P3.2] Split dialog not yet built")
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
}
|
||||
|
||||
// ── Map tab: Source file browser ──────────────
|
||||
SourcePanel {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── BOX 3: Hardware Status Footer ──────────────────────────
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Theme.sideFooterConnection
|
||||
radius: Theme.sidePanelRadius
|
||||
border.color: Theme.sideBorder
|
||||
border.width: 1
|
||||
color: Theme.sidePanelBackground
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 14
|
||||
spacing: 6
|
||||
|
||||
// Row 1: Title + status badge (unchanged)
|
||||
RowLayout {
|
||||
spacing: 8
|
||||
Layout.fillWidth: true
|
||||
|
||||
Label {
|
||||
text: "CONNECTION FLOW"
|
||||
color: Theme.sideMutedText
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.letterSpacing: 1.2
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 18
|
||||
width: badgeLabel.implicitWidth + 14
|
||||
radius: 9
|
||||
color: {
|
||||
var s = deviceController.connectionStatus
|
||||
if (s === "Connected") return Theme.statusSuccessColor
|
||||
if (s === "Disconnected") return Theme.statusErrorColor
|
||||
return Theme.statusWarningColor
|
||||
}
|
||||
|
||||
Label {
|
||||
id: badgeLabel
|
||||
anchors.centerIn: parent
|
||||
text: deviceController.connectionStatus || "Disconnected"
|
||||
color: "#111111"
|
||||
font.pixelSize: Theme.font2xs
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
}
|
||||
|
||||
// Row 2: Status dot + descriptive text (replaces dotted trail)
|
||||
Row {
|
||||
spacing: 8
|
||||
Layout.fillWidth: true
|
||||
|
||||
Rectangle {
|
||||
id: statusDot
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 8
|
||||
height: 8
|
||||
radius: 4
|
||||
color: deviceController.connectionStatus === "Connected"
|
||||
? Theme.statusSuccessColor : Theme.statusErrorColor
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
running: deviceController.connectionStatus === "Connected"
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation { to: 0.4; duration: 1200 }
|
||||
NumberAnimation { to: 1.0; duration: 1200 }
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: {
|
||||
if (deviceController.connectionStatus === "Connected")
|
||||
return deviceController.selectedPort || "Connected"
|
||||
if (deviceController.selectedPort)
|
||||
return deviceController.selectedPort
|
||||
return "No port selected"
|
||||
}
|
||||
color: Theme.bodyColor
|
||||
font.family: Theme.uiFontFamily
|
||||
font.pixelSize: Theme.fontXs
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
// Row 3: Mode chip — neutral gray tag
|
||||
Row {
|
||||
spacing: 6
|
||||
Layout.fillWidth: true
|
||||
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: modeChipLabel.implicitWidth + 12
|
||||
height: 18
|
||||
radius: 4
|
||||
color: Theme.fieldBackground
|
||||
border.color: Theme.sideBorder
|
||||
border.width: 1
|
||||
|
||||
Label {
|
||||
id: modeChipLabel
|
||||
anchors.centerIn: parent
|
||||
text: streamController.mode
|
||||
? streamController.mode.toUpperCase() : "REVIEW"
|
||||
color: Theme.sideMutedText
|
||||
font.family: Theme.uiFontFamily
|
||||
font.pixelSize: Theme.font2xs
|
||||
font.bold: true
|
||||
font.letterSpacing: 0.8
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── BOX 4: Utility Buttons ─────────────────────────────────
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Theme.sideFooterUtility
|
||||
radius: Theme.sidePanelRadius
|
||||
border.color: Theme.sideBorder
|
||||
border.width: 1
|
||||
color: Theme.sidePanelBackground
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
spacing: 4
|
||||
|
||||
Button {
|
||||
id: settingsBtn
|
||||
flat: true
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
icon.source: "Tabs/icons/settings.svg"
|
||||
onClicked: settingsPopup.open()
|
||||
|
||||
background: Rectangle {
|
||||
radius: 8
|
||||
color: settingsBtn.hovered
|
||||
? Theme.sideActiveBackground : "transparent"
|
||||
}
|
||||
|
||||
contentItem: Row {
|
||||
spacing: 6
|
||||
anchors.centerIn: parent
|
||||
|
||||
IconImage {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 16
|
||||
height: 16
|
||||
source: "Tabs/icons/settings.svg"
|
||||
sourceSize.width: 16
|
||||
sourceSize.height: 16
|
||||
color: Theme.headingColor
|
||||
opacity: 0.75
|
||||
}
|
||||
|
||||
Label {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "SETTINGS"
|
||||
color: Theme.headingColor
|
||||
font.family: Theme.uiFontFamily
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.bold: true
|
||||
font.letterSpacing: 1.2
|
||||
opacity: 0.8
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 1
|
||||
height: 24
|
||||
color: Theme.sideBorder
|
||||
}
|
||||
|
||||
Button {
|
||||
id: aboutBtn
|
||||
flat: true
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
icon.source: "Tabs/icons/about.svg"
|
||||
onClicked: aboutDialog.open()
|
||||
|
||||
background: Rectangle {
|
||||
radius: 8
|
||||
color: aboutBtn.hovered
|
||||
? Theme.sideActiveBackground : "transparent"
|
||||
}
|
||||
|
||||
contentItem: Row {
|
||||
spacing: 6
|
||||
anchors.centerIn: parent
|
||||
|
||||
IconImage {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 16
|
||||
height: 16
|
||||
source: "Tabs/icons/about.svg"
|
||||
sourceSize.width: 16
|
||||
sourceSize.height: 16
|
||||
color: Theme.headingColor
|
||||
opacity: 0.75
|
||||
}
|
||||
|
||||
Label {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "ABOUT"
|
||||
color: Theme.headingColor
|
||||
font.family: Theme.uiFontFamily
|
||||
font.pixelSize: Theme.fontXs
|
||||
font.bold: true
|
||||
font.letterSpacing: 1.2
|
||||
opacity: 0.8
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── WORKSPACE ─────────────────────────────────────────────────────
|
||||
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
|
||||
anchors.margins: Theme.mainAreaPadding
|
||||
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" : ""
|
||||
source: "Tabs/StatusTab.qml"
|
||||
active: StackLayout.index === root.selectedTabIndex
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
active: parent.tabName === "Status"
|
||||
source: parent.tabName === "Status" ? "Tabs/StatusTab.qml" : ""
|
||||
source: "Tabs/DataTab.qml"
|
||||
active: StackLayout.index === root.selectedTabIndex
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
source: "Tabs/WaferMapTab.qml"
|
||||
active: StackLayout.index === root.selectedTabIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user