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:
jack
2026-06-29 14:56:57 -07:00
parent a54d536efe
commit 5d079174e0
+537 -283
View File
@@ -1,10 +1,14 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.impl
import QtQuick.Layouts import QtQuick.Layouts
import ISC import ISC
import ISC.Tabs.components
import QtQuick.Dialogs import QtQuick.Dialogs
// ===== Home Workspace Shell ===== // ===== 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 { Rectangle {
id: root id: root
anchors.fill: parent anchors.fill: parent
@@ -13,27 +17,8 @@ Rectangle {
border.color: Theme.outerFrameBorder border.color: Theme.outerFrameBorder
border.width: Theme.borderStrong 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 ===== // ===== View State =====
property int selectedTabIndex: 0 property int selectedTabIndex: 0
property int selectedSideActionIndex: -1
property bool memoryRead: false property bool memoryRead: false
property bool waferDetected: false property bool waferDetected: false
@@ -41,34 +26,28 @@ Rectangle {
Connections { Connections {
target: deviceController target: deviceController
function onDetectResult(result){ function onDetectResult(result){
//result is a waferInfo dict on success, None on failure root.waferDetected = (result != null && result != undefined)
root.waferDetected = (result != null && result!= undefined)
} }
function onParsedDataReady(result) { function onParsedDataReady(result) {
if (result.success && result.csv_path) { if (result.success && result.csv_path) {
// Load the freshly read CSV into the player
streamController.loadFile(result.csv_path) streamController.loadFile(result.csv_path)
root.selectedTabIndex = 2
// Automatically switch to the "Wafer Map Tab" (index 3) }
root.selectedTabIndex = 3 }
}
Connections {
target: deviceController
function onReadResult(result) {
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")
} }
} }
} }
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) { function cleanFolderUrl(url) {
return decodeURIComponent(String(url).replace(/^file:\/\//, "")) return decodeURIComponent(String(url).replace(/^file:\/\//, ""))
@@ -90,298 +69,573 @@ Rectangle {
} }
} }
// --------------------------------------------------------------------------- // ===== Settings Popup =====
// TODO P1.1: Delete importFileDialog + storedDataDialog (and the Popup {
// `selectedTabIndex = 3` lines inside them) id: settingsPopup
// modal: true
// THINKING: dim: true
// These dialogs were only opened from the old side-rail buttons closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
// "IMPORT DATA" (index 4) and "STORED DATA" (index 5). With the width: Math.min(root.width - 80, 800)
// mockup rail, the Map tab's file list (P1.3) replaces both — it height: Math.min(root.height - 80, 600)
// drives `streamController.loadFile()` directly from a file_browser anchors.centerIn: Overlay.overlay
// Repeater. Keeping these dead dialogs would bloat the QML and
// confuse future contributors. background: Rectangle {
// radius: Theme.radiusMd
// `selectedTabIndex = 3` is also stale (Map tab is index 2 now). color: Theme.cardBackground
// The replacement flow: file_row.onClicked → loadFile() → border.color: Theme.cardBorder
// selectedTabIndex = 2. border.width: 1
// --------------------------------------------------------------------------- }
FileDialog {
id: importFileDialog contentItem: Rectangle {
title: "Import CSV / ZWafer file" color: Theme.pageBackground
nameFilters: ["CSV files (*.csv)", "ZWafer files (*.zwafer)", "All files (*)"] radius: Theme.radiusMd
onAccepted: { clip: true
var path = root.cleanFolderUrl(selectedFile)
streamController.setMode("review") ColumnLayout {
streamController.stopStream() anchors.fill: parent
streamController.loadFile(path) spacing: 0
root.selectedTabIndex = 3 // Switch to Wafer Map tab
root.selectedSideActionIndex = -1 Rectangle {
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
}
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"
}
}
}
} }
} }
FileDialog { // ===== About Dialog =====
id: storedDataDialog AboutDialog {
title: "Open Stored CSV File" id: aboutDialog
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 ====== // ===== Main Layout: Rail + Workspace =====
// ---------------------------------------------------------------------------
// 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
// ===== Left Action Rail ===== // ── LEFT RAIL ──────────────────────────────────────────────────────
// Left control rail.
Rectangle { Rectangle {
id: sideRail id: rail
Layout.preferredWidth: Theme.sideRailWidth Layout.preferredWidth: Theme.sideRailWidth
Layout.fillHeight: true Layout.fillHeight: true
color: Theme.sideRailBackground color: Theme.sideRailBackground
border.color: Theme.workspaceBorder border.color: Theme.sideBorder
border.width: Theme.borderThin border.width: 1
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 { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.panelPadding anchors.margins: Theme.sideRailMargin
spacing: Theme.sideRailSpacing spacing: Theme.sideRailSpacing
Repeater { // ── BOX 1: Pill Tab Bar ─────────────────────────────────────
model: root.sideActions Rectangle {
id: pillBar
Layout.fillWidth: true
Layout.preferredHeight: Theme.sidePillHeight
radius: 999
border.color: Theme.sideBorder
border.width: 1
color: "transparent"
Button { ListModel {
id: control id: pillModel
text: modelData ListElement { label: "STATUS"; icon: "Tabs/icons/status.svg"; expandW: 86 }
enabled: { ListElement { label: "DATA"; icon: "Tabs/icons/data.svg"; expandW: 66 }
switch (index) { ListElement { label: "MAP"; icon: "Tabs/icons/map.svg"; expandW: 58 }
case 0: return true // DETECT WAFER }
case 1: return root.waferDetected // READ MEMORY
case 2: return root.waferDetected // ERASE MEMORY RowLayout {
default: return true 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
} }
} }
property bool isActive: index === root.selectedSideActionIndex }
Layout.fillWidth: true }
Layout.preferredHeight: Math.max(Theme.sideButtonMinHeight, sideRail.computedButtonHeight)
hoverEnabled: true // ── BOX 2: Context-Sensitive Rail Panel ────────────────────
onClicked: { Rectangle {
root.selectedSideActionIndex = index Layout.fillWidth: true
root.selectedTabIndex = 0 // always jump to Status tab Layout.fillHeight: true
if (index === 0) { Layout.minimumHeight: 120
if (!deviceController.saveDataDir) { radius: Theme.sidePanelRadius
saveDirDialog.open() border.color: Theme.sideBorder
return 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
} }
root._doDetect()
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 }
} }
else if (index === 1) {
streamController.setMode("review") // ── Data tab: Data Operations ───────────────────
streamController.stopStream() ColumnLayout {
deviceController.readMemoryAsync() 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 }
} }
else if (index === 2) {
// ERASE MEMORY // ── Map tab: Source file browser ──────────────
streamController.setMode("review") SourcePanel {
streamController.stopStream() Layout.fillWidth: true
deviceController.eraseMemory(deviceController.selectedPort || "") Layout.fillHeight: true
} }
} }
}
}
background: Rectangle { // ── BOX 3: Hardware Status Footer ──────────────────────────
color: { Rectangle {
if (control.down) { Layout.fillWidth: true
return Theme.buttonPressed; Layout.preferredHeight: Theme.sideFooterConnection
} radius: Theme.sidePanelRadius
if (control.isActive) { border.color: Theme.sideBorder
return Theme.sideActiveBackground; border.width: 1
} color: Theme.sidePanelBackground
return "transparent";
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
} }
border.color: control.isActive ? Theme.cardBorder : "transparent"
border.width: 1
radius: Theme.radiusSm
Rectangle { Rectangle {
anchors.left: parent.left height: 18
anchors.top: parent.top width: badgeLabel.implicitWidth + 14
anchors.bottom: parent.bottom radius: 9
width: 3 color: {
visible: control.isActive var s = deviceController.connectionStatus
color: Theme.primaryAccent if (s === "Connected") return Theme.statusSuccessColor
radius: Theme.radiusXs 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
} }
} }
contentItem: Text { // Row 3: Mode chip — neutral gray tag
text: control.text Row {
color: control.isActive ? Theme.headingColor : Theme.bodyColor spacing: 6
font.bold: control.isActive Layout.fillWidth: true
font.pixelSize: 18
wrapMode: Text.WordWrap Rectangle {
horizontalAlignment: Text.AlignHCenter anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter 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
}
}
} }
} }
} }
} }
} }
// Main workspace and footer navigation. // ── WORKSPACE ─────────────────────────────────────────────────────
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
color: Theme.workspaceBackground color: Theme.workspaceBackground
border.color: Theme.workspaceBorder border.color: Theme.workspaceBorder
border.width: Theme.borderThin border.width: 1
ColumnLayout { StackLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.mainAreaPadding anchors.margins: Theme.mainAreaPadding
spacing: Theme.rightPaneGap currentIndex: root.selectedTabIndex
// ===== Active Tab Content Area ===== Loader {
Rectangle { source: "Tabs/StatusTab.qml"
Layout.fillWidth: true active: StackLayout.index === root.selectedTabIndex
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" : ""
}
}
}
}
} }
Loader {
// Keep the tab strip evenly distributed across the footer. source: "Tabs/DataTab.qml"
// ===== Footer Tab Strip ===== active: StackLayout.index === root.selectedTabIndex
Rectangle { }
Layout.fillWidth: true Loader {
Layout.preferredHeight: Theme.tabBarHeight + (Theme.tabBarPadding * 2) source: "Tabs/WaferMapTab.qml"
color: Theme.tabBarBackground active: StackLayout.index === root.selectedTabIndex
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
}
}
}
}
} }
} }
} }