diff --git a/src/pygui/ISC/HomePage.qml b/src/pygui/ISC/HomePage.qml index d6380b3..83574d2 100644 --- a/src/pygui/ISC/HomePage.qml +++ b/src/pygui/ISC/HomePage.qml @@ -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,62 +17,37 @@ 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 Connections { target: deviceController - function onDetectResult(result){ - //result is a waferInfo dict on success, None on failure - root.waferDetected = (result != null && result!= undefined) + function onDetectResult(result){ + 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) { + 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) { return decodeURIComponent(String(url).replace(/^file:\/\//, "")) @@ -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 + } + + contentItem: Rectangle { + color: Theme.pageBackground + radius: Theme.radiusMd + clip: true + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + 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 { - 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 - } + // ===== About Dialog ===== + AboutDialog { + id: aboutDialog } - // ===== 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 - // --------------------------------------------------------------------------- + // ===== Main Layout: Rail + Workspace ===== RowLayout { anchors.fill: parent spacing: 0 - // ===== Left Action Rail ===== - // Left control rail. + // ── LEFT RAIL ────────────────────────────────────────────────────── Rectangle { - id: sideRail + id: rail 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)) + border.color: Theme.sideBorder + border.width: 1 ColumnLayout { anchors.fill: parent - anchors.margins: Theme.panelPadding + anchors.margins: Theme.sideRailMargin spacing: Theme.sideRailSpacing - Repeater { - model: root.sideActions + // ── 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" - 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 + 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 } } - 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 + } + } + + // ── 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 } - 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") - streamController.stopStream() - deviceController.readMemoryAsync() + + // ── 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 } } - else if (index === 2) { - // ERASE MEMORY - streamController.setMode("review") - streamController.stopStream() - deviceController.eraseMemory(deviceController.selectedPort || "") + + // ── Map tab: Source file browser ────────────── + SourcePanel { + Layout.fillWidth: true + Layout.fillHeight: true } } + } + } - background: Rectangle { - color: { - if (control.down) { - return Theme.buttonPressed; - } - if (control.isActive) { - return Theme.sideActiveBackground; - } - return "transparent"; + // ── 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 } - 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 + 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 } } - 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 + // 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 + } + } } } } } } - // Main workspace and footer navigation. + // ── WORKSPACE ───────────────────────────────────────────────────── Rectangle { Layout.fillWidth: true Layout.fillHeight: true color: Theme.workspaceBackground border.color: Theme.workspaceBorder - border.width: Theme.borderThin + border.width: 1 - ColumnLayout { + StackLayout { anchors.fill: parent anchors.margins: Theme.mainAreaPadding - spacing: Theme.rightPaneGap + currentIndex: root.selectedTabIndex - // ===== 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" : "" - } - } - } - } + Loader { + source: "Tabs/StatusTab.qml" + active: StackLayout.index === root.selectedTabIndex } - - // 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 - } - } - } - } + Loader { + source: "Tabs/DataTab.qml" + active: StackLayout.index === root.selectedTabIndex + } + Loader { + source: "Tabs/WaferMapTab.qml" + active: StackLayout.index === root.selectedTabIndex } } }