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 color: Theme.pageBackground clip: true border.color: Theme.outerFrameBorder border.width: Theme.borderStrong // ===== View State ===== property int selectedTabIndex: 0 onSelectedTabIndexChanged: { if (selectedTabIndex === 2) { file_browser.refreshFiles(); streamController.unloadFile() } } property bool memoryRead: false property bool waferDetected: false // TODO P2.2: add `property bool waferLicensed: false`; set it in // onDetectResult below via deviceController.waferLicensed() (slot from // P1.2 — the detectResult payload is opaque in QML, can't read serial // from it). Then READ/ERASE buttons get // `enabled: root.waferDetected && root.waferLicensed`. // See docs/pending/license-gating-plan.md §2.2. Connections { target: deviceController function onDetectResult(result){ // detectResult payload is Signal(object): a dict may cross into QML // as an opaque wrapper whose fields read as undefined, so only // test truthiness (None on failure, dict on success). root.waferDetected = !!result } function onParsedDataReady(result) { if (result.success && result.csv_path) { streamController.loadFile(result.csv_path) // TODO P3.1: only auto-jump to Map when unlocked: // `&& licenseModel.licenses.length > 0` — otherwise a // successful read walks straight into the locked tab. // See plan §3.1. if (root.selectedTabIndex === 0) { 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") } } } function cleanFolderUrl(url) { return decodeURIComponent(String(url).replace(/^file:\/\//, "")) } function _doDetect() { root.memoryRead = false root.waferDetected = false streamController.setMode("review") streamController.stopStream() deviceController.detectWafer() } FolderDialog { id: saveDirDialog title: "Choose a folder to save Data" onAccepted: { deviceController.setSaveDataDir(root.cleanFolderUrl(selectedFolder)) root._doDetect() } } // ===== Split Dialog (Threshold Segmentation) ===== SplitDialog { id: splitDialog } // ===== 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" } } } } } // ===== 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 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 desc: "Monitor real-time device connection, serial communication, and execute wafer actions." } ListElement { label: "DATA" icon: "Tabs/icons/data.svg" expandW: 66 desc: "Manage, review, compare (DTW), and split historical sensor data records." } ListElement { label: "MAP" icon: "Tabs/icons/map.svg" expandW: 58 desc: "Visualize spatial sensor readings on a 2D thin-plate spline RBF interpolated heatmap." } } 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 } } } } AppToolTip { visible: pillBtn.hovered text: "" + model.label + ": " + model.desc } onClicked: root.selectedTabIndex = index } } } } // ── BOX 1.5: Run Comparison (Data tab only) ──────────────── Rectangle { id: runComparisonBox Layout.fillWidth: true Layout.preferredHeight: runCompCol.implicitHeight + 28 visible: root.selectedTabIndex === 1 radius: Theme.sidePanelRadius border.color: Theme.sideBorder border.width: 1 color: Theme.sidePanelBackground readonly property var dataTab: dataTabLoader.item component RunSlot: Rectangle { id: slotBox property string title property string filePath: "" property color accent: Theme.primaryAccent property bool active: false property bool locked: false property string placeholderText: "Click to select" signal cleared() signal armed() Layout.fillWidth: true implicitHeight: 58 radius: Theme.radiusSm color: Theme.fieldBackground opacity: slotBox.locked ? 0.65 : 1.0 border.width: slotBox.active ? 2 : 1 border.color: slotBox.active ? accent : Theme.sideBorder Behavior on border.color { ColorAnimation { duration: Theme.durationFast } } MouseArea { anchors.fill: parent enabled: !slotBox.locked cursorShape: slotBox.locked ? Qt.ArrowCursor : Qt.PointingHandCursor onClicked: slotBox.armed() } RowLayout { anchors.fill: parent anchors.margins: 10 spacing: 8 Rectangle { width: 8; height: 8; radius: 4 color: slotBox.accent Layout.alignment: Qt.AlignVCenter } ColumnLayout { Layout.fillWidth: true spacing: 2 Label { text: slotBox.title font.pixelSize: Theme.fontXs font.bold: true font.letterSpacing: 1.0 color: slotBox.active ? slotBox.accent : Theme.sideMutedText } Label { text: slotBox.filePath !== "" ? slotBox.filePath.split("/").pop() : (slotBox.locked ? slotBox.placeholderText : (slotBox.active ? "<- Select file below..." : "Click to select")) font.pixelSize: Theme.fontSm color: slotBox.filePath !== "" ? Theme.headingColor : Theme.bodyColor opacity: slotBox.filePath !== "" ? 1.0 : 0.6 elide: Text.ElideMiddle Layout.fillWidth: true } } Button { visible: slotBox.filePath !== "" && !slotBox.locked flat: true implicitWidth: 24; implicitHeight: 24 onClicked: slotBox.cleared() background: Rectangle { radius: Theme.radiusSm color: parent.hovered ? Theme.flatButtonHover : "transparent" } contentItem: IconImage { source: "Tabs/icons/x.svg" width: 14; height: 14 sourceSize.width: 14 sourceSize.height: 14 color: Theme.bodyColor anchors.centerIn: parent } } } } ColumnLayout { id: runCompCol anchors.fill: parent anchors.margins: 14 spacing: 10 RowLayout { Layout.fillWidth: true spacing: 8 Label { text: "RUN COMPARISON (DTW)" color: Theme.sideMutedText font.family: Theme.uiFontFamily font.pixelSize: Theme.fontSm font.letterSpacing: 1.5 font.bold: true Layout.fillWidth: true elide: Text.ElideRight } Button { id: resetRunBtn flat: true implicitWidth: 32 implicitHeight: 32 visible: true onClicked: if (runComparisonBox.dataTab) runComparisonBox.dataTab.resetComparison() background: Rectangle { radius: Theme.radiusSm color: resetRunBtn.hovered ? Theme.flatButtonHover : "transparent" } contentItem: IconImage { source: "Tabs/icons/rotate-ccw.svg" width: 18; height: 18 sourceSize.width: 18 sourceSize.height: 18 color: Theme.bodyColor anchors.centerIn: parent } AppToolTip { visible: resetRunBtn.hovered text: "Reset comparison" } } } PanelCheckBox { id: useMasterCheck Layout.fillWidth: true text: "Compare vs Master File" checked: runComparisonBox.dataTab ? runComparisonBox.dataTab.useMasterFile : false onToggled: { if (!runComparisonBox.dataTab) return runComparisonBox.dataTab.useMasterFile = checked if (checked) runComparisonBox.dataTab.activeBox = 2 } } RunSlot { readonly property bool useMaster: runComparisonBox.dataTab ? runComparisonBox.dataTab.useMasterFile : false title: useMaster ? "MASTER FILE" : "REFERENCE (RUN A)" accent: Theme.primaryAccent locked: useMaster placeholderText: useMaster ? (runComparisonBox.dataTab.runBWaferType !== "" ? "No master file for type " + runComparisonBox.dataTab.runBWaferType : "Select Run B to resolve master") : "Click to select" active: runComparisonBox.dataTab ? (!useMaster && runComparisonBox.dataTab.activeBox === 1) : false filePath: runComparisonBox.dataTab ? (useMaster ? runComparisonBox.dataTab.masterFileForRunB : runComparisonBox.dataTab.compareFileA) : "" onArmed: if (runComparisonBox.dataTab && !useMaster) runComparisonBox.dataTab.activeBox = 1 onCleared: { if (!runComparisonBox.dataTab) return runComparisonBox.dataTab.compareFileA = "" runComparisonBox.dataTab.activeBox = 1 runComparisonBox.dataTab.clearResults() } } RunSlot { title: "SESSION (RUN B)" accent: Theme.themeSkill active: runComparisonBox.dataTab ? runComparisonBox.dataTab.activeBox === 2 : false filePath: runComparisonBox.dataTab ? runComparisonBox.dataTab.compareFileB : "" onArmed: if (runComparisonBox.dataTab) runComparisonBox.dataTab.activeBox = 2 onCleared: { if (!runComparisonBox.dataTab) return runComparisonBox.dataTab.compareFileB = "" runComparisonBox.dataTab.activeBox = 2 runComparisonBox.dataTab.clearResults() } } Button { id: compareRunBtn Layout.fillWidth: true Layout.preferredHeight: 32 readonly property var dt: runComparisonBox.dataTab readonly property string fileA: !!dt ? (dt.useMasterFile ? dt.masterFileForRunB : dt.compareFileA) : "" readonly property bool masterMissing: !!dt && dt.useMasterFile && dt.masterFileForRunB === "" readonly property bool sameFile: !!dt && fileA !== "" && fileA === dt.compareFileB readonly property string gateError: !!dt ? dt.familyGateError : "" enabled: !!dt && fileA !== "" && dt.compareFileB !== "" && !sameFile && !dt.comparing && !masterMissing && gateError === "" AppToolTip { visible: (compareRunBtn.sameFile || compareRunBtn.masterMissing || compareRunBtn.gateError !== "") && compareRunBtn.hovered text: compareRunBtn.masterMissing ? "No master file registered for this wafer type (Settings → Master Files)" : compareRunBtn.gateError !== "" ? compareRunBtn.gateError : "Choose two different runs to compare" } onClicked: { dt.comparing = true dt.clearResults() streamController.compareFiles(fileA, dt.compareFileB) } background: Rectangle { radius: Theme.radiusSm color: compareRunBtn.enabled ? (compareRunBtn.hovered ? Qt.lighter(Theme.primaryAccent, 1.1) : Theme.primaryAccent) : Theme.buttonNeutralBackground Behavior on color { ColorAnimation { duration: Theme.durationFast } } } contentItem: Row { spacing: 8 anchors.centerIn: parent Label { anchors.verticalCenter: parent.verticalCenter text: !compareRunBtn.dt ? "Select both runs" : compareRunBtn.dt.comparing ? "Comparing…" : compareRunBtn.enabled ? "Run DTW Comparison" : (compareRunBtn.masterMissing ? "No master for this type" : compareRunBtn.sameFile ? "Choose different runs" : compareRunBtn.gateError !== "" ? "Wafer family mismatch" : "Select both runs") color: compareRunBtn.enabled ? Theme.tone100 : Theme.sideMutedText font.pixelSize: Theme.fontSm font.bold: true } BusyIndicator { running: !!compareRunBtn.dt && compareRunBtn.dt.comparing visible: !!compareRunBtn.dt && compareRunBtn.dt.comparing width: 14; height: 14 anchors.verticalCenter: parent.verticalCenter } } } } } // ── BOX 2: Context-Sensitive Rail Panel ──────────────────── StackLayout { Layout.fillWidth: true Layout.fillHeight: true Layout.minimumHeight: 120 currentIndex: root.selectedTabIndex === 0 ? 0 : 1 // ── Status tab: Hardware Actions + Log Actions ────────── StatusActionsPanel { Layout.fillWidth: true Layout.fillHeight: true } // ── Map tab: Source file browser ────────────── Rectangle { radius: Theme.sidePanelRadius border.color: Theme.sideBorder border.width: 1 color: Theme.sidePanelBackground SourcePanel { anchors.fill: parent anchors.margins: 14 } } } // ── BOX 3: Hardware Status Footer ────────────────────────── ConnectionFooter { Layout.fillWidth: true } // ── BOX 4: Utility Buttons ───────────────────────────────── UtilityFooter { id: utilFooter Layout.fillWidth: true settingsPopup: settingsPopup aboutDialog: aboutDialog } } } // ── WORKSPACE ───────────────────────────────────────────────────── Rectangle { Layout.fillWidth: true Layout.fillHeight: true color: Theme.workspaceBackground StackLayout { anchors.fill: parent anchors.margins: Theme.mainAreaPadding currentIndex: root.selectedTabIndex Loader { source: "Tabs/StatusTab.qml" active: StackLayout.index === root.selectedTabIndex } Loader { id: dataTabLoader source: "Tabs/DataTab.qml" active: StackLayout.index === root.selectedTabIndex } Loader { source: "Tabs/WaferMapTab.qml" // TODO P3.1: append `&& licenseModel.licenses.length > 0` // so the tab never instantiates while locked. See plan §3.1. active: StackLayout.index === root.selectedTabIndex } } } } }