Compare commits

..

8 Commits

Author SHA1 Message Date
jack 5105ab1ab6 feat(tabs): update all tab views, dialogs, and session controller 2026-06-29 14:58:10 -07:00
jack db21f201f6 feat(transport): redesign transport bar 2026-06-29 14:57:52 -07:00
jack c9cb7ab8d8 feat(panels): redesign ReadoutPanel and SourcePanel to match mockup
ReadoutPanel:
- Move section headers inside card containers
- Redesign readout table with uppercase labels, code fonts, color mapping
- Add full-width dividers, fix vertical spacing with fillHeight spacer
- Unify typography with Theme tokens

SourcePanel:
- Bump font sizes to new standard (fontMd primary, fontSm secondary)
- Replace unicode button text with edit.svg/refresh.svg IconImages
- Add styled hover tooltips on edit and refresh buttons
2026-06-29 14:57:13 -07:00
jack 5d079174e0 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
2026-06-29 14:56:57 -07:00
jack a54d536efe chore(icons): add edit and refresh SVGs, consolidate excel icon path
- Add edit.svg and refresh.svg icon assets
- Update excel.svg in canonical icons/ directory
- Remove duplicate icon from legacy icon/ directory
2026-06-29 14:56:26 -07:00
jack c622a4cc23 feat(components): add RailActionButton, AboutDialog, and register new types
- Add RailActionButton reusable side-rail button component
- Add AboutDialog popup component
- Register new types in qmldir files
- Register TransportBar in components qmldir
2026-06-29 14:56:16 -07:00
jack 3fef31efff feat(theme): update design tokens, typography scale, and color palette
- Add new tone palette entries and sensor color tokens
- Add codeFontFamily and uiFontFamily definitions
- Update motion/easing tokens
- Add transport bar, side rail, and status color tokens
- Revise geometry tokens (radius, spacing, sizing)
2026-06-29 14:55:50 -07:00
jack f9737259c1 add icons 2026-06-29 10:56:33 -07:00
33 changed files with 2363 additions and 980 deletions
+137
View File
@@ -0,0 +1,137 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import ISC
Popup {
id: root
modal: true
dim: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
width: 420
height: 340
anchors.centerIn: Overlay.overlay
background: Rectangle {
radius: Theme.radiusMd
color: Theme.cardBackground
border.color: Theme.cardBorder
border.width: 1
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 24
spacing: 16
// ── Title ──
Label {
text: "About"
font.pixelSize: Theme.font2xl
font.bold: true
color: Theme.headingColor
}
// ── App info ──
ColumnLayout {
spacing: 4
Label {
text: "ISenseCloud (ISC)"
font.pixelSize: Theme.fontLg
color: Theme.headingColor
}
Label {
text: "Version 0.1.0"
font.pixelSize: Theme.fontSm
color: Theme.bodyColor
}
Label {
text: "Temperature-sensing wafer monitoring"
font.pixelSize: Theme.fontSm
color: Theme.bodyColor
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
// ── Separator ──
Rectangle {
Layout.fillWidth: true
height: 1
color: Theme.cardBorder
}
// ── License grid ──
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 100
radius: Theme.radiusSm
color: Theme.panelBackground
border.color: Theme.cardBorder
border.width: 1
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 2
Label {
text: "LICENSE"
color: Theme.panelTitleText
font.pixelSize: Theme.fontXs
font.letterSpacing: 0.5
}
// Grid header
RowLayout {
Layout.fillWidth: true
spacing: 4
Label { text: "Wafer SN"; font.pixelSize: Theme.fontXs; font.bold: true; color: Theme.subheadingColor; Layout.preferredWidth: 100 }
Label { text: "Mfg Date"; font.pixelSize: Theme.fontXs; font.bold: true; color: Theme.subheadingColor; Layout.preferredWidth: 80 }
Label { text: "Level"; font.pixelSize: Theme.fontXs; font.bold: true; color: Theme.subheadingColor; Layout.preferredWidth: 60 }
}
Rectangle {
Layout.fillWidth: true
height: 1
color: Theme.softBorder
}
Label {
text: "No license loaded"
color: Theme.bodyColor
font.pixelSize: Theme.fontSm
Layout.fillWidth: true
Layout.fillHeight: true
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
}
}
// ── Close ──
Button {
text: "Close"
Layout.alignment: Qt.AlignRight
Layout.preferredWidth: 100
Layout.preferredHeight: 32
onClicked: root.close()
background: Rectangle {
radius: Theme.radiusSm
color: parent.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground
border.color: Theme.fieldBorder
border.width: 1
}
contentItem: Label {
text: parent.text
color: Theme.buttonNeutralText
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
}
}
+541 -287
View File
@@ -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
}
}
}
+30 -20
View File
@@ -1,7 +1,6 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.TableView
import ISC
// ===== Data Tab =====
@@ -18,7 +17,7 @@ Item {
target: deviceController
function onParsedDataReady(result) {
if (result && result.success) {
dataTable.forceLayout()
dataTable.forceLayout();
}
}
}
@@ -31,7 +30,7 @@ Item {
Label {
text: "No Data"
font.pixelSize: 24
font.pixelSize: Theme.font2xl
font.bold: true
color: Theme.headingColor
anchors.horizontalCenter: parent.horizontalCenter
@@ -39,7 +38,7 @@ Item {
Label {
text: "Read and parse wafer data to see temperature readings here."
font.pixelSize: 14
font.pixelSize: Theme.fontMd
color: Theme.bodyColor
anchors.horizontalCenter: parent.horizontalCenter
wrapMode: Text.WordWrap
@@ -60,17 +59,18 @@ Item {
Label {
text: "Temperature Data"
font.pixelSize: 18
font.pixelSize: Theme.fontXl
font.bold: true
color: Theme.headingColor
}
Item { Layout.fillWidth: true }
Item {
Layout.fillWidth: true
}
Label {
text: deviceController.dataRowCount + " rows × " +
deviceController.dataColCount + " sensors"
font.pixelSize: 13
text: deviceController.dataRowCount + " rows × " + deviceController.dataColCount + " sensors"
font.pixelSize: Theme.fontSm
color: Theme.bodyColor
}
}
@@ -82,7 +82,8 @@ Item {
Button {
text: "Open in Excel"
icon.source: "icon/excel.svg"
icon.source: "icons/excel.svg"
icon.color: Theme.headingColor
onClicked: deviceController.openCsvFile()
}
@@ -96,7 +97,9 @@ Item {
onClicked: splitDialog.open()
}
Item { Layout.fillWidth: true}
Item {
Layout.fillWidth: true
}
}
// --- Table container ---
Rectangle {
@@ -131,7 +134,7 @@ Item {
// 'display' is the Qt.DisplayRole value from headerData()
text: display !== undefined ? display : ""
color: Theme.headingColor
font.pixelSize: 11
font.pixelSize: Theme.fontXs
font.bold: true
elide: Text.ElideRight
}
@@ -148,13 +151,16 @@ Item {
reuseItems: true
columnWidthProvider: function (col) {
if (col === 0) return 52 // row index column
const sensorCols = Math.max(1, dataTable.columns - 1)
const available = dataTable.width - 52
return Math.max(52, Math.min(90, available / sensorCols))
if (col === 0)
return 52; // row index column
const sensorCols = Math.max(1, dataTable.columns - 1);
const available = dataTable.width - 52;
return Math.max(52, Math.min(90, available / sensorCols));
}
rowHeightProvider: function() { return 24 }
rowHeightProvider: function () {
return 24;
}
delegate: Rectangle {
color: row % 2 === 0 ? Theme.panelBackground : Theme.cardBackground
@@ -163,13 +169,17 @@ Item {
anchors.centerIn: parent
text: display !== undefined ? display : ""
color: Theme.bodyColor
font.pixelSize: 11
font.pixelSize: Theme.fontXs
elide: Text.ElideRight
}
}
ScrollBar.horizontal: ScrollBar { policy: ScrollBar.AsNeeded }
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AsNeeded
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
}
}
}
+40 -37
View File
@@ -27,18 +27,20 @@ Item {
Label {
text: "Line Chart"
color: Theme.headingColor
font.pixelSize: 13
font.pixelSize: Theme.fontSm
font.bold: true
Layout.alignment: Qt.AlignVCenter
}
Item { Layout.fillWidth: true }
Item {
Layout.fillWidth: true
}
// Refresh button — pulls data from deviceController
Button {
id: refreshBtn
text: "REFRESH"
font.pixelSize: 10
font.pixelSize: Theme.fontXs
font.weight: Font.Medium
implicitHeight: 26
implicitWidth: 72
@@ -68,7 +70,7 @@ Item {
Label {
text: "Source:"
color: Theme.bodyColor
font.pixelSize: 10
font.pixelSize: Theme.fontXs
Layout.alignment: Qt.AlignVCenter
}
@@ -77,7 +79,7 @@ Item {
model: ["Parsed Data", "Live Trend"]
currentIndex: 0
implicitHeight: 26
font.pixelSize: 11
font.pixelSize: Theme.fontXs
background: Rectangle {
color: Theme.fieldBackground
border.color: Theme.fieldBorder
@@ -95,13 +97,15 @@ Item {
onCurrentIndexChanged: reloadData()
}
Item { Layout.fillWidth: true }
Item {
Layout.fillWidth: true
}
Label {
id: statusLabel
text: "Ready"
color: Theme.bodyColor
font.pixelSize: 10
font.pixelSize: Theme.fontXs
font.italic: true
Layout.alignment: Qt.AlignVCenter
}
@@ -125,8 +129,7 @@ Item {
gridColor: Theme.softBorder
axisColor: Theme.bodyColor
textColor: Theme.headingColor
seriesColors: [Theme.sensorLow, Theme.sensorHigh, Theme.sensorInRange,
"#FFA726", "#AB47BC", "#00BCD4"]
seriesColors: [Theme.sensorLow, Theme.sensorHigh, Theme.sensorInRange, "#FFA726", "#AB47BC", "#00BCD4"]
showLegend: true
}
}
@@ -137,44 +140,44 @@ Item {
function reloadData() {
if (sourceSelector.currentIndex === 0) {
// Parsed data from DeviceController
loadParsedData()
loadParsedData();
} else {
// Live trend from SessionController
loadLiveTrend()
loadLiveTrend();
}
}
function loadParsedData() {
statusLabel.text = "Loading..."
var result = deviceController.getChartData()
statusLabel.text = "Loading...";
var result = deviceController.getChartData();
if (!result || !result.success) {
graph.seriesData = []
graph.sensorNames = []
graph.title = "Sensor Temperature Over Time"
statusLabel.text = "No data — read a wafer first"
return
graph.seriesData = [];
graph.sensorNames = [];
graph.title = "Sensor Temperature Over Time";
statusLabel.text = "No data — read a wafer first";
return;
}
graph.seriesData = result.series || []
graph.sensorNames = result.sensor_names || []
graph.title = "Sensor Temperature Over Time"
statusLabel.text = (result.series ? result.series.length : 0) + " sensors loaded"
graph.seriesData = result.series || [];
graph.sensorNames = result.sensor_names || [];
graph.title = "Sensor Temperature Over Time";
statusLabel.text = (result.series ? result.series.length : 0) + " sensors loaded";
}
function loadLiveTrend() {
// Live trend: connects to streamController.trendData
// The trend data comes as a JSON array of per-frame averages
statusLabel.text = "Connecting to live trend..."
statusLabel.text = "Connecting to live trend...";
// We need at least one data point
if (streamController.stats && streamController.stats.avg !== undefined) {
// Build a single-series graph from the trend buffer
// For now show a placeholder — the trendData signal handles updates
graph.title = "Live Average Temperature"
graph.yLabel = "Avg Temperature (°C)"
graph.xLabel = "Time (s)"
statusLabel.text = "Live trend — waiting for data..."
graph.title = "Live Average Temperature";
graph.yLabel = "Avg Temperature (°C)";
graph.xLabel = "Time (s)";
statusLabel.text = "Live trend — waiting for data...";
} else {
statusLabel.text = "No live data — start a stream on the Wafer Map tab"
statusLabel.text = "No live data — start a stream on the Wafer Map tab";
}
}
@@ -184,7 +187,7 @@ Item {
target: deviceController
function onParsedDataReady(result) {
if (sourceSelector.currentIndex === 0) {
root.reloadData()
root.reloadData();
}
}
}
@@ -194,14 +197,14 @@ Item {
function onTrendData(avgsJson) {
if (sourceSelector.currentIndex === 1) {
try {
var avgs = JSON.parse(avgsJson)
var avgs = JSON.parse(avgsJson);
if (avgs && avgs.length > 0) {
graph.seriesData = [avgs]
graph.sensorNames = ["Average"]
graph.title = "Live Average Temperature"
graph.yLabel = "Avg Temperature (°C)"
graph.xLabel = "Frame"
statusLabel.text = "Live: " + avgs.length + " data points"
graph.seriesData = [avgs];
graph.sensorNames = ["Average"];
graph.title = "Live Average Temperature";
graph.yLabel = "Avg Temperature (°C)";
graph.xLabel = "Frame";
statusLabel.text = "Live: " + avgs.length + " data points";
}
} catch (e) {
// ignore parse errors
@@ -213,7 +216,7 @@ Item {
// On startup, try to load parsed data if available
Component.onCompleted: {
if (deviceController.dataRowCount > 0) {
reloadData()
reloadData();
}
}
}
+22 -14
View File
@@ -24,9 +24,11 @@ Dialog {
property int selectedRow: -1
property int metadataEditRow: -1
readonly property string masterTypeFieldText: {
if (metadataEditRow < 0) return "";
if (metadataEditRow < 0)
return "";
const rec = tableModel[metadataEditRow];
if (!rec) return "";
if (!rec)
return "";
return rec.masterType ?? "";
}
@@ -113,7 +115,7 @@ Dialog {
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
font.bold: true
font.pixelSize: 13
font.pixelSize: Theme.fontSm
}
}
@@ -129,6 +131,12 @@ Dialog {
color: Theme.fieldBackground
border.width: dialogTextInput.activeFocus ? Theme.borderStrong : Theme.borderThin
border.color: dialogTextInput.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
Behavior on border.color {
ColorAnimation { duration: Theme.durationFast }
}
}
}
@@ -163,7 +171,7 @@ Dialog {
anchors.left: parent.left
anchors.leftMargin: 16
text: root.title
font.pixelSize: 15
font.pixelSize: Theme.fontLg
font.bold: true
color: Theme.headingColor
}
@@ -221,7 +229,7 @@ Dialog {
text: file_browser.currentDirectory
color: Theme.fieldText
elide: Text.ElideMiddle
font.pixelSize: 13
font.pixelSize: Theme.fontSm
}
}
}
@@ -242,7 +250,7 @@ Dialog {
anchors.centerIn: parent
text: root.headerTitles[index] ?? ""
font.bold: true
font.pixelSize: 12
font.pixelSize: Theme.fontSm
color: Theme.headingColor
}
}
@@ -329,7 +337,7 @@ Dialog {
const mt = root.normalizedRows[row]?.masterType ?? "";
return mt || "—";
}
font.pixelSize: 14
font.pixelSize: Theme.fontMd
font.bold: true
color: {
const mt = root.normalizedRows[row]?.masterType ?? "";
@@ -346,7 +354,7 @@ Dialog {
sourceComponent: Button {
hoverEnabled: true
text: "Edit…"
font.pixelSize: 12
font.pixelSize: Theme.fontSm
onClicked: root.openMetadataEditor(row)
background: Rectangle {
@@ -361,7 +369,7 @@ Dialog {
color: Theme.buttonNeutralText
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pixelSize: 12
font.pixelSize: Theme.fontSm
}
}
}
@@ -388,7 +396,7 @@ Dialog {
return "";
}
elide: Text.ElideRight
font.pixelSize: 13
font.pixelSize: Theme.fontSm
color: Theme.bodyColor
}
}
@@ -409,7 +417,7 @@ Dialog {
return record.notes ?? "";
}
elide: Text.ElideRight
font.pixelSize: 13
font.pixelSize: Theme.fontSm
color: Theme.bodyColor
}
}
@@ -456,7 +464,7 @@ Dialog {
verticalAlignment: Text.AlignVCenter
text: root.selectedPath
elide: Text.ElideLeft
font.pixelSize: 13
font.pixelSize: Theme.fontSm
color: root.selectedPath ? Theme.fieldText : Theme.fieldPlaceholder
}
}
@@ -504,7 +512,7 @@ Dialog {
anchors.left: parent.left
anchors.leftMargin: 16
text: metadataEditDialog.title
font.pixelSize: 15
font.pixelSize: Theme.fontLg
font.bold: true
color: Theme.headingColor
}
@@ -530,7 +538,7 @@ Dialog {
maximumLineCount: 3
elide: Text.ElideRight
color: Theme.bodyColor
font.pixelSize: 12
font.pixelSize: Theme.fontSm
}
Label {
+44 -12
View File
@@ -27,8 +27,7 @@ Item {
}
// ===== Settings Data Helpers =====
readonly property var masterFamilies: ["A", "B", "C", "D", "E", "F", "P", "X", "Z"]
// ListModel for master families defined inline in the ColumnLayout below
function masterPath(family) {
const table = settingsModel.masters;
return table && table[family] ? table[family] : "";
@@ -58,7 +57,7 @@ Item {
label: Label {
text: settingsGroup.title
color: Theme.panelTitleText
font.pixelSize: 16
font.pixelSize: Theme.fontLg
font.bold: true
}
}
@@ -75,6 +74,12 @@ Item {
color: Theme.fieldBackground
border.width: textInput.activeFocus ? Theme.borderStrong : Theme.borderThin
border.color: textInput.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
Behavior on border.color {
ColorAnimation { duration: Theme.durationFast }
}
}
}
@@ -111,13 +116,23 @@ Item {
border.width: Theme.borderThin
border.color: toggle.checked ? Theme.primaryAccent : Theme.fieldBorder
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
Behavior on border.color {
ColorAnimation { duration: Theme.durationFast }
}
Text {
anchors.centerIn: parent
text: "✓"
font.pixelSize: 14
font.pixelSize: Theme.fontMd
font.bold: true
color: Theme.panelBackground
visible: toggle.checked
opacity: toggle.checked ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation { duration: Theme.durationFast }
}
// Small nudge to center the checkmark perfectly
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
@@ -129,7 +144,7 @@ Item {
color: Theme.headingColor
verticalAlignment: Text.AlignVCenter
leftPadding: toggle.indicator.width + toggle.spacing
font.pixelSize: 13
font.pixelSize: Theme.fontSm
}
}
@@ -159,6 +174,7 @@ Item {
anchors.top: settingsScroll.top
anchors.right: settingsScroll.right
anchors.bottom: settingsScroll.bottom
anchors.leftMargin: 6
policy: ScrollBar.AsNeeded
contentItem: Rectangle {
@@ -174,16 +190,19 @@ Item {
}
}
// Gap reserved between the centered content and the right-edge scrollbar.
readonly property int scrollGap: 100
ColumnLayout {
id: container
width: Math.min(settingsScroll.availableWidth, Theme.settingsPanelMaxWidth)
width: Math.min(settingsScroll.availableWidth - settingsScroll.scrollGap, Theme.settingsPanelMaxWidth)
x: (settingsScroll.availableWidth - width) / 2
spacing: Theme.settingsSectionSpacing
// ===== Page Title =====
Label {
text: "Settings"
font.pixelSize: 30
font.pixelSize: Theme.font3xl
font.bold: true
color: Theme.headingColor
}
@@ -241,11 +260,24 @@ Item {
anchors.fill: parent
spacing: Theme.settingsGridSpacing
ListModel {
id: masterModel
ListElement { letter: "A" }
ListElement { letter: "B" }
ListElement { letter: "C" }
ListElement { letter: "D" }
ListElement { letter: "E" }
ListElement { letter: "F" }
ListElement { letter: "P" }
ListElement { letter: "X" }
ListElement { letter: "Z" }
}
Repeater {
model: root.masterFamilies
model: masterModel
RowLayout {
property string family: modelData
property string family: model.letter
Layout.fillWidth: true
spacing: Theme.settingsRowSpacing
@@ -361,7 +393,7 @@ Item {
Label {
text: Theme.isDarkMode ? "Dark" : "Light"
color: Theme.bodyColor
font.pixelSize: 13
font.pixelSize: Theme.fontSm
Layout.alignment: Qt.AlignVCenter
Behavior on color {
@@ -423,7 +455,7 @@ Item {
Label {
anchors.centerIn: parent
text: "↓ Scroll for more"
font.pixelSize: 13
font.pixelSize: Theme.fontSm
font.bold: true
color: Theme.bodyColor
opacity: (settingsScroll.ScrollBar.vertical.position <= 0 && settingsScroll.contentHeight > settingsScroll.height) ? (scrollHintTimer.running ? 1 : 0) : 0
+146 -66
View File
@@ -27,24 +27,24 @@ ColumnLayout {
property bool dataParsed: false
function cleanFolderUrl(url) {
return decodeURIComponent(String(url).replace(/^file:\/\//, ""))
return decodeURIComponent(String(url).replace(/^file:\/\//, ""));
}
function currentFamilyCode() {
var info = deviceController.lastWaferInfo
return info && info.length > 0 ? (info[0] || "") : ""
var info = deviceController.lastWaferInfo;
return info && info.length > 0 ? (info[0] || "") : "";
}
function parseAndSavePendingRead() {
deviceController.parseAndSaveData(root.currentFamilyCode(), deviceController.selectedPort || "")
deviceController.parseAndSaveData(root.currentFamilyCode(), deviceController.selectedPort || "");
}
FolderDialog {
id: saveDirDialog
title: "Choose a folder to save."
onAccepted: {
deviceController.setSaveDataDir(root.cleanFolderUrl(selectedFolder))
root.parseAndSavePendingRead()
deviceController.setSaveDataDir(root.cleanFolderUrl(selectedFolder));
root.parseAndSavePendingRead();
}
}
@@ -59,9 +59,11 @@ ColumnLayout {
Layout.preferredHeight: 92
color: Theme.cardBackground
border.color: {
if (deviceController.connectionStatus === "Connected") return Theme.statusSuccessColor
if (deviceController.connectionStatus === "Disconnected") return Theme.statusErrorColor
return Theme.cardBorder
if (deviceController.connectionStatus === "Connected")
return Theme.statusSuccessColor;
if (deviceController.connectionStatus === "Disconnected")
return Theme.statusErrorColor;
return Theme.cardBorder;
}
radius: Theme.radiusSm
@@ -73,19 +75,21 @@ ColumnLayout {
Text {
text: deviceController.connectionStatus
color: {
if (deviceController.connectionStatus === "Connected") return Theme.statusSuccessColor
if (deviceController.connectionStatus === "Disconnected") return Theme.statusErrorColor
return Theme.bodyColor
if (deviceController.connectionStatus === "Connected")
return Theme.statusSuccessColor;
if (deviceController.connectionStatus === "Disconnected")
return Theme.statusErrorColor;
return Theme.bodyColor;
}
font.bold: true
font.pixelSize: 16
font.pixelSize: Theme.fontLg
Layout.fillWidth: true
}
Text {
text: "Port: " + (deviceController.selectedPort || "None selected")
color: Theme.bodyColor
font.pixelSize: 14
font.pixelSize: Theme.fontMd
Layout.fillWidth: true
}
@@ -97,7 +101,7 @@ ColumnLayout {
Text {
text: "Save dir: " + (deviceController.saveDataDir || "None selected")
color: Theme.bodyColor
font.pixelSize: 12
font.pixelSize: Theme.fontSm
opacity: 0.7
elide: Text.ElideMiddle
Layout.fillWidth: true
@@ -107,7 +111,7 @@ ColumnLayout {
Button {
id: browseBtn
text: "BROWSE..."
font.pixelSize: 9
font.pixelSize: Theme.font2xs
font.weight: Font.Medium
implicitHeight: 20
implicitWidth: 65
@@ -149,22 +153,52 @@ ColumnLayout {
ColumnLayout {
Layout.fillWidth: true
spacing: 4
Text { text: "Family Code"; color: Theme.subheadingColor; font.pixelSize: 12 }
Text { id: waferInfoFamily; text: "—"; color: Theme.bodyColor; font.pixelSize: 16; font.bold: true }
Text {
text: "Family Code"
color: Theme.subheadingColor
font.pixelSize: Theme.fontSm
}
Text {
id: waferInfoFamily
text: "—"
color: Theme.bodyColor
font.pixelSize: Theme.fontLg
font.bold: true
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 4
Text { text: "Serial Number"; color: Theme.subheadingColor; font.pixelSize: 12 }
Text { id: waferSerial; text: "—"; color: Theme.bodyColor; font.pixelSize: 16; font.bold: true }
Text {
text: "Serial Number"
color: Theme.subheadingColor
font.pixelSize: Theme.fontSm
}
Text {
id: waferSerial
text: "—"
color: Theme.bodyColor
font.pixelSize: Theme.fontLg
font.bold: true
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 4
Text { text: "Sensor Count"; color: Theme.subheadingColor; font.pixelSize: 12 }
Text { id: waferSensors; text: "—"; color: Theme.bodyColor; font.pixelSize: 16; font.bold: true }
Text {
text: "Sensor Count"
color: Theme.subheadingColor
font.pixelSize: Theme.fontSm
}
Text {
id: waferSensors
text: "—"
color: Theme.bodyColor
font.pixelSize: Theme.fontLg
font.bold: true
}
}
}
@@ -175,15 +209,35 @@ ColumnLayout {
ColumnLayout {
Layout.fillWidth: true
spacing: 4
Text { text: "Runtime"; color: Theme.subheadingColor; font.pixelSize: 12 }
Text { id: waferRuntime; text: "—"; color: Theme.bodyColor; font.pixelSize: 16; font.bold: true }
Text {
text: "Runtime"
color: Theme.subheadingColor
font.pixelSize: Theme.fontSm
}
Text {
id: waferRuntime
text: "—"
color: Theme.bodyColor
font.pixelSize: Theme.fontLg
font.bold: true
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 4
Text { text: "Cycles"; color: Theme.subheadingColor; font.pixelSize: 12 }
Text { id: waferCycles; text: "—"; color: Theme.bodyColor; font.pixelSize: 16; font.bold: true }
Text {
text: "Cycles"
color: Theme.subheadingColor
font.pixelSize: Theme.fontSm
}
Text {
id: waferCycles
text: "—"
color: Theme.bodyColor
font.pixelSize: Theme.fontLg
font.bold: true
}
}
}
}
@@ -203,22 +257,49 @@ ColumnLayout {
ColumnLayout {
Layout.fillWidth: true
spacing: 4
Text { text: "Rows"; color: Theme.subheadingColor; font.pixelSize: 12 }
Text { text: String(root.dataRows); color: Theme.bodyColor; font.pixelSize: 16; font.bold: true }
Text {
text: "Rows"
color: Theme.subheadingColor
font.pixelSize: Theme.fontSm
}
Text {
text: String(root.dataRows)
color: Theme.bodyColor
font.pixelSize: Theme.fontLg
font.bold: true
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 4
Text { text: "Sensors"; color: Theme.subheadingColor; font.pixelSize: 12 }
Text { text: String(root.dataCols); color: Theme.bodyColor; font.pixelSize: 16; font.bold: true }
Text {
text: "Sensors"
color: Theme.subheadingColor
font.pixelSize: Theme.fontSm
}
Text {
text: String(root.dataCols)
color: Theme.bodyColor
font.pixelSize: Theme.fontLg
font.bold: true
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 4
Text { text: "CSV"; color: Theme.subheadingColor; font.pixelSize: 12 }
Text { text: root.csvPath; color: Theme.bodyColor; font.pixelSize: 12; elide: Text.ElideMiddle }
Text {
text: "CSV"
color: Theme.subheadingColor
font.pixelSize: Theme.fontSm
}
Text {
text: root.csvPath
color: Theme.bodyColor
font.pixelSize: Theme.fontSm
elide: Text.ElideMiddle
}
}
}
}
@@ -250,7 +331,7 @@ ColumnLayout {
Label {
text: "ACTIVITY LOG"
color: Theme.bodyColor
font.pixelSize: 10
font.pixelSize: Theme.fontXs
font.letterSpacing: 1.8
font.weight: Font.Medium
Layout.fillWidth: true
@@ -259,7 +340,7 @@ ColumnLayout {
Button {
text: "REFRESH SESSION"
font.pixelSize: 10
font.pixelSize: Theme.fontXs
implicitHeight: 24
hoverEnabled: true
background: Rectangle {
@@ -278,7 +359,7 @@ ColumnLayout {
Button {
text: "CLEAR LOG"
font.pixelSize: 10
font.pixelSize: Theme.fontXs
implicitHeight: 24
hoverEnabled: true
background: Rectangle {
@@ -317,7 +398,7 @@ ColumnLayout {
text: qsTr("Welcome — use the side rail to detect a wafer, or import data to review.")
readOnly: true
font.family: "monospace"
font.pixelSize: 12
font.pixelSize: Theme.fontSm
wrapMode: TextArea.WordWrap
color: Theme.bodyColor
background: null
@@ -328,7 +409,7 @@ ColumnLayout {
Connections {
target: deviceController
function onActivityLogUpdated(newLog) {
activityLog.text = newLog
activityLog.text = newLog;
}
}
}
@@ -340,7 +421,7 @@ ColumnLayout {
function onLogMessage(message) {
if (message == "CHOOSE_SAVE_DIR") {
saveDirDialog.open()
saveDirDialog.open();
}
}
@@ -349,36 +430,35 @@ ColumnLayout {
// Status tab always visible now
}
function onDetectResult(result) {
if (result && result.familyCode) {
root.waferDetected = true
waferInfoFamily.text = result.familyCode
waferSerial.text = result.serialNumber
waferSensors.text = String(result.sensorCount)
waferRuntime.text = result.runtime + "s"
waferCycles.text = String(result.cycleCount)
root.waferDetected = true;
waferInfoFamily.text = result.familyCode;
waferSerial.text = result.serialNumber;
waferSensors.text = String(result.sensorCount);
waferRuntime.text = result.runtime + "s";
waferCycles.text = String(result.cycleCount);
}
// On failure, keep the last displayed wafer info and do not change waferDetected.
}
function onStatusRestored() {
// Show restored wafer info if available
var info = deviceController.lastWaferInfo
var info = deviceController.lastWaferInfo;
if (info && info.length > 0) {
root.waferDetected = true
waferInfoFamily.text = info[0] || "—"
waferSerial.text = info[1] || "—"
waferSensors.text = String(info[2] || 0)
waferRuntime.text = (info[3] || 0) + "s"
waferCycles.text = String(info[4] || 0)
root.waferDetected = true;
waferInfoFamily.text = info[0] || "—";
waferSerial.text = info[1] || "—";
waferSensors.text = String(info[2] || 0);
waferRuntime.text = (info[3] || 0) + "s";
waferCycles.text = String(info[4] || 0);
}
// Show data summary if data was previously parsed
if (deviceController.dataRowCount > 0) {
root.dataParsed = true
root.dataRows = deviceController.dataRowCount
root.dataCols = deviceController.dataColCount
root.csvPath = "" // CSV path not persisted as full path, just show it was parsed
root.dataParsed = true;
root.dataRows = deviceController.dataRowCount;
root.dataCols = deviceController.dataColCount;
root.csvPath = ""; // CSV path not persisted as full path, just show it was parsed
}
}
}
@@ -386,13 +466,13 @@ ColumnLayout {
Connections {
target: deviceController
function onReadResult(result) {
root.dataParsed = false
root.dataRows = 0
root.dataCols = 0
root.csvPath = ""
root.dataParsed = false;
root.dataRows = 0;
root.dataCols = 0;
root.csvPath = "";
if (result && result.success === true)
saveDirDialog.open()
saveDirDialog.open();
}
}
@@ -400,12 +480,12 @@ ColumnLayout {
target: deviceController
function onParsedDataReady(result) {
if (result && result.success) {
root.dataParsed = true
root.dataRows = result.rows
root.dataCols = result.cols
root.csvPath = result.csv_path || ""
root.dataParsed = true;
root.dataRows = result.rows;
root.dataCols = result.cols;
root.csvPath = result.csv_path || "";
} else {
root.dataParsed = false
root.dataParsed = false;
}
}
}
+67 -61
View File
@@ -18,12 +18,13 @@ Item {
repeat: true
running: streamController.mode === "live" && streamController.state !== "idle"
onTriggered: root._liveSecs++
onRunningChanged: if (!running) root._liveSecs = 0
onRunningChanged: if (!running)
root._liveSecs = 0
}
function fmtTime(s) {
var m = Math.floor(s / 60)
var ss = s % 60
return (m < 10 ? "0" : "") + m + ":" + (ss < 10 ? "0" : "") + ss
var m = Math.floor(s / 60);
var ss = s % 60;
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
@@ -32,7 +33,7 @@ Item {
Connections {
target: streamController
function onTrendData(avgsJson) {
trendChart.setDataFromJson(avgsJson)
trendChart.setDataFromJson(avgsJson);
}
}
@@ -60,7 +61,8 @@ Item {
}
TabButton {
text: "Review"
implicitWidth: 64; implicitHeight: 28
implicitWidth: 64
implicitHeight: 28
background: Rectangle {
color: parent.checked ? Theme.tabActiveBackground : "transparent"
radius: Theme.radiusSm - 1
@@ -68,7 +70,7 @@ Item {
contentItem: Text {
text: parent.text
color: parent.checked ? Theme.headingColor : Theme.bodyColor
font.pixelSize: 11
font.pixelSize: Theme.fontSm
font.weight: parent.checked ? Font.Medium : Font.Normal
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
@@ -78,7 +80,8 @@ Item {
text: "Live"
enabled: deviceController.connectionStatus === "Connected"
opacity: enabled ? 1.0 : 0.4
implicitWidth: 58; implicitHeight: 28
implicitWidth: 58
implicitHeight: 28
background: Rectangle {
color: parent.checked ? Theme.tabActiveBackground : "transparent"
radius: Theme.radiusSm - 1
@@ -86,29 +89,28 @@ Item {
contentItem: Text {
text: parent.text
color: parent.checked ? Theme.headingColor : Theme.bodyColor
font.pixelSize: 11
font.pixelSize: Theme.fontSm
font.weight: parent.checked ? Font.Medium : Font.Normal
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
onCurrentIndexChanged:
if (currentIndex === 1){
onCurrentIndexChanged: if (currentIndex === 1) {
// Guard: revert to Review if not connected
if (deviceController.connectionStatus !== "Connected") {
currentIndex = 0
return
currentIndex = 0;
return;
}
// Entering Live mode
streamController.setMode("live")
var fc = ""
streamController.setMode("live");
var fc = "";
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 {
// Entering Review Mode (automatically calls stopStream in backend)
streamController.setMode("review")
streamController.setMode("review");
}
}
@@ -120,33 +122,44 @@ Item {
Label {
id: liveIndicator
text: "◉"
color: (deviceController.connectionStatus === "Connected" && streamController.state !== "idle")
? Theme.liveColor : Theme.bodyColor
font.pixelSize: 13
color: (deviceController.connectionStatus === "Connected" && streamController.state !== "idle") ? Theme.liveColor : Theme.bodyColor
font.pixelSize: Theme.fontSm
SequentialAnimation on opacity {
running: deviceController.connectionStatus === "Connected" && streamController.state !== "idle"
loops: Animation.Infinite
NumberAnimation { to: 0.2; duration: 600; easing.type: Easing.InOutQuad }
NumberAnimation { to: 1.0; duration: 600; easing.type: Easing.InOutQuad }
NumberAnimation {
to: 0.2
duration: 600
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 1.0
duration: 600
easing.type: Easing.InOutQuad
}
onRunningChanged: {
if (!running) liveIndicator.opacity = 1.0
if (!running)
liveIndicator.opacity = 1.0;
}
}
}
Label {
text: "Live stream"
color: Theme.bodyColor
font.pixelSize: 11
font.pixelSize: Theme.fontXs
}
}
Item { Layout.fillWidth: true }
Item {
Layout.fillWidth: true
}
// SET badge + avg temp
Rectangle {
visible: streamController.state === "set"
height: 22; radius: Theme.radiusSm
height: 22
radius: Theme.radiusSm
color: "transparent"
border.color: Theme.liveColor
border.width: 1
@@ -154,10 +167,9 @@ Item {
Label {
id: setLabel
anchors.centerIn: parent
text: "SET " + (streamController.stats.avg !== undefined
? streamController.stats.avg + " avg" : "")
text: "SET " + (streamController.stats.avg !== undefined ? streamController.stats.avg + " avg" : "")
color: Theme.liveColor
font.pixelSize: 10
font.pixelSize: Theme.fontXs
font.weight: Font.Medium
font.letterSpacing: 0.8
}
@@ -168,19 +180,27 @@ Item {
spacing: 5
visible: streamController.recording
Rectangle {
width: 7; height: 7; radius: 4
width: 7
height: 7
radius: 4
color: Theme.recordColor
SequentialAnimation on opacity {
running: streamController.recording
loops: Animation.Infinite
NumberAnimation { to: 0.2; duration: 600 }
NumberAnimation { to: 1.0; duration: 600 }
NumberAnimation {
to: 0.2
duration: 600
}
NumberAnimation {
to: 1.0
duration: 600
}
}
}
Label {
text: "REC"
color: Theme.recordColor
font.pixelSize: 10
font.pixelSize: Theme.fontXs
font.weight: Font.Medium
font.letterSpacing: 1.2
}
@@ -197,7 +217,7 @@ Item {
// Label {
// text: "LIVE " + root.fmtTime(root._liveSecs)
// color: Theme.liveColor
// font.pixelSize: 10
// font.pixelSize: Theme.fontXs
// font.weight: Font.Medium
// font.letterSpacing: 0.8
// }
@@ -208,7 +228,7 @@ Item {
// visible: streamController.mode === "review" || streamController.state === "idle"
// text: streamController.state.toUpperCas`e`()
// color: Theme.bodyColor
// font.pixelSize: 11
// font.pixelSize: Theme.fontXs
// font.weight: Font.Medium
// font.letterSpacing: 1.2
// }
@@ -248,21 +268,7 @@ Item {
Layout.fillHeight: true
spacing: 16
// Source panel — bordered card
Rectangle {
Layout.preferredWidth: 220
Layout.fillHeight: true
color: Theme.cardBackground
border.color: Theme.cardBorder
border.width: 1
radius: Theme.radiusMd
clip: true
SourcePanel {
anchors.fill: parent
anchors.margins: 10
}
}
// Source panel — moved to side rail (P2.1)
ColumnLayout {
Layout.fillWidth: true
@@ -280,7 +286,7 @@ Item {
id: trendPane
Layout.fillWidth: true
implicitHeight: 160
visible: trendChart.hasData
visible: trendChart.hasData && streamController.mode === "live"
color: Theme.cardBackground
border.color: Theme.cardBorder
border.width: 1
@@ -293,28 +299,28 @@ Item {
}
}
Item { Layout.fillWidth: true; implicitHeight: 16 }
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 — floating wrapper box
Rectangle {
Layout.preferredWidth: 220
Layout.fillHeight: true
color: Theme.cardBackground
border.color: Theme.cardBorder
border.width: 1
radius: Theme.radiusMd
color: "transparent"
border.color: "transparent"
border.width: 0
clip: true
ReadoutPanel {
id: readoutPanel
anchors.fill: parent
anchors.margins: 10
anchors.margins: 0
}
}
}
@@ -0,0 +1,70 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.impl
import ISC
// ===== Rail Action Button =====
// Reusable pill-style button for rail action panels.
// Icon left, uppercase label, fixed height, hover/active backgrounds.
Rectangle {
id: root
property string label: ""
property string iconSource: ""
property bool destructive: false
property color _textColor: !root.enabled ? Theme.sideMutedText
: root.destructive ? Theme.statusErrorColor
: Theme.headingColor
signal clicked()
implicitHeight: Theme.sideButtonHeight
radius: 8
color: mouseArea.containsMouse || mouseArea.pressed
? Theme.sideActiveBackground : "transparent"
border.width: 0
Behavior on color {
ColorAnimation { duration: 100 }
}
Row {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
spacing: 10
IconImage {
anchors.verticalCenter: parent.verticalCenter
width: 18
height: 18
source: root.iconSource
sourceSize.width: 18
sourceSize.height: 18
color: root._textColor
opacity: root.enabled ? 0.85 : 0.35
}
Label {
anchors.verticalCenter: parent.verticalCenter
text: root.label
color: root._textColor
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontSm
font.bold: true
font.letterSpacing: 1.2
opacity: root.enabled ? 1.0 : 0.4
elide: Text.ElideRight
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: root.enabled
cursorShape: root.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
if (root.enabled) root.clicked()
}
}
}
+286 -79
View File
@@ -3,8 +3,11 @@ import QtQuick.Controls
import QtQuick.Layouts
import ISC
// ===== Readout Panel =====
// Right-rail panel with three bordered sections: READOUT, DISPLAY, THRESHOLDS.
// Cards match the left-rail SOURCE / CONNECTION FLOW style.
ColumnLayout {
spacing: 0
spacing: 6
property var s: streamController.stats
property alias showLabels: labelsToggle.checked
property alias heatmapBlend: heatmapSlider.value
@@ -21,13 +24,23 @@ ColumnLayout {
border.width: Theme.borderThin
border.color: toggle.checked ? Theme.primaryAccent : Theme.fieldBorder
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
Behavior on border.color {
ColorAnimation { duration: Theme.durationFast }
}
Text {
anchors.centerIn: parent
text: "✓"
font.pixelSize: 13
font.pixelSize: Theme.fontSm
font.bold: true
color: Theme.panelBackground
visible: toggle.checked
opacity: toggle.checked ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation { duration: Theme.durationFast }
}
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
@@ -36,150 +49,290 @@ ColumnLayout {
contentItem: Text {
text: toggle.text
color: Theme.headingColor
font.family: Theme.uiFontFamily
verticalAlignment: Text.AlignVCenter
leftPadding: toggle.indicator.width + toggle.spacing
font.pixelSize: toggle.font.pixelSize || 11
font.pixelSize: toggle.font.pixelSize || Theme.fontXs
}
}
// ── READOUT ───────────────────────────────────────────────────────────
component BadgePill: Rectangle {
implicitWidth: badgeLabel.implicitWidth + 10
implicitHeight: 10
radius: 9
color: Theme.fieldBackground
border.color: Theme.cardBorder
border.width: 1
property alias text: badgeLabel.text
Label {
text: "READOUT"
id: badgeLabel
anchors.centerIn: parent
color: Theme.bodyColor
font.pixelSize: 10
font.letterSpacing: 1.8
font.pixelSize: Theme.font2xs
font.weight: Font.Medium
topPadding: 6
bottomPadding: 8
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: statsLayout.implicitHeight + 20
color: Theme.subtleSectionBackground
radius: Theme.radiusSm
border.color: Theme.cardBorder
implicitHeight: readoutCard.implicitHeight + 24
color: Theme.sidePanelBackground
radius: Theme.sidePanelRadius
border.color: Theme.sideBorder
border.width: 1
ColumnLayout {
id: statsLayout
anchors { fill: parent; margins: 10 }
spacing: 8
id: readoutCard
anchors {
left: parent.left
right: parent.right
top: parent.top
topMargin: 12
}
spacing: 0
// Header Inside the Box
Label {
text: "READOUT"
color: Theme.sideMutedText
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontXs
font.letterSpacing: 1.5
font.bold: true
Layout.leftMargin: 14
Layout.rightMargin: 14
Layout.topMargin: 2
Layout.bottomMargin: 10
}
// Min Temp Row
RowLayout {
Layout.fillWidth: true
Label { text: "Min Temp"; color: Theme.bodyColor; font.pixelSize: 11 }
Layout.leftMargin: 14
Layout.rightMargin: 14
Layout.preferredHeight: 32
Label {
text: "MIN TEMP"
color: Theme.sideMutedText
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontXs
font.bold: true
}
Item { Layout.fillWidth: true }
RowLayout {
spacing: 4
Label {
text: s.min !== undefined ? s.min : "—"
color: Theme.sensorLow
font.pixelSize: 13; font.weight: Font.Bold
font.family: Theme.codeFontFamily
font.pixelSize: Theme.fontMd
font.bold: true
}
Label {
text: s.minIndex !== undefined ? "#" + s.minIndex : ""
color: Theme.bodyColor
font.pixelSize: 10
visible: (s.minIndex !== undefined) && s.minIndex >= 0
text: "#" + (s.minIndex !== undefined ? s.minIndex : "")
color: Theme.sideMutedText
font.family: Theme.uiFontFamily
font.pixelSize: Theme.font2xs
Layout.alignment: Qt.AlignBottom
bottomPadding: 3
}
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: Theme.cardBorder
}
// Max Temp Row
RowLayout {
Layout.fillWidth: true
Label { text: "Max Temp"; color: Theme.bodyColor; font.pixelSize: 11 }
Layout.leftMargin: 14
Layout.rightMargin: 14
Layout.preferredHeight: 32
Label {
text: "MAX TEMP"
color: Theme.sideMutedText
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontXs
font.bold: true
}
Item { Layout.fillWidth: true }
RowLayout {
spacing: 4
Label {
text: s.max !== undefined ? s.max : "—"
color: Theme.sensorHigh
font.pixelSize: 13; font.weight: Font.Bold
font.family: Theme.codeFontFamily
font.pixelSize: Theme.fontMd
font.bold: true
}
Label {
text: s.maxIndex !== undefined ? "#" + s.maxIndex : ""
color: Theme.bodyColor
font.pixelSize: 10
visible: (s.maxIndex !== undefined) && s.maxIndex >= 0
text: "#" + (s.maxIndex !== undefined ? s.maxIndex : "")
color: Theme.sideMutedText
font.family: Theme.uiFontFamily
font.pixelSize: Theme.font2xs
Layout.alignment: Qt.AlignBottom
bottomPadding: 3
}
}
}
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder }
Rectangle {
Layout.fillWidth: true
height: 1
color: Theme.cardBorder
}
// Differential Row
RowLayout {
Layout.fillWidth: true
Label { text: "Differential"; color: Theme.bodyColor; font.pixelSize: 11 }
Layout.leftMargin: 14
Layout.rightMargin: 14
Layout.preferredHeight: 32
Label {
text: "DIFF"
color: Theme.sideMutedText
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontXs
font.bold: true
}
Item { Layout.fillWidth: true }
Label {
text: s.diff !== undefined ? s.diff : "—"
color: Theme.headingColor
font.pixelSize: 13; font.weight: Font.Bold
color: Theme.isDarkMode ? "#A78BFA" : "#8B5CF6"
font.family: Theme.codeFontFamily
font.pixelSize: Theme.fontMd
font.bold: true
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: Theme.cardBorder
}
// Average Row
RowLayout {
Layout.fillWidth: true
Label { text: "Average"; color: Theme.bodyColor; font.pixelSize: 11 }
Layout.leftMargin: 14
Layout.rightMargin: 14
Layout.preferredHeight: 32
Label {
text: "AVERAGE"
color: Theme.sideMutedText
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontXs
font.bold: true
}
Item { Layout.fillWidth: true }
Label {
text: s.avg !== undefined ? s.avg : "—"
color: Theme.headingColor
font.pixelSize: 13; font.weight: Font.Bold
font.family: Theme.codeFontFamily
font.pixelSize: Theme.fontMd
font.bold: true
}
}
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder }
Rectangle {
Layout.fillWidth: true
height: 1
color: Theme.cardBorder
}
// Sigma Row
RowLayout {
Layout.fillWidth: true
Label { text: "Sigma (σ)"; color: Theme.bodyColor; font.pixelSize: 11 }
Layout.leftMargin: 14
Layout.rightMargin: 14
Layout.preferredHeight: 32
Label {
text: "SIGMA (Σ)"
color: Theme.sideMutedText
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontXs
font.bold: true
}
Item { Layout.fillWidth: true }
Label {
text: s.sigma !== undefined ? s.sigma : "—"
color: Theme.headingColor
font.pixelSize: 13; font.weight: Font.Bold
font.family: Theme.codeFontFamily
font.pixelSize: Theme.fontMd
font.bold: true
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: Theme.cardBorder
}
// 3-Sigma Row
RowLayout {
Layout.fillWidth: true
Label { text: "3σ Value"; color: Theme.bodyColor; font.pixelSize: 11 }
Layout.leftMargin: 14
Layout.rightMargin: 14
Layout.preferredHeight: 32
Label {
text: "3Σ VALUE"
color: Theme.sideMutedText
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontXs
font.bold: true
}
Item { Layout.fillWidth: true }
Label {
text: s.threeSigma !== undefined ? s.threeSigma : "—"
color: Theme.bodyColor
font.pixelSize: 12; font.weight: Font.Medium
color: Theme.headingColor
font.family: Theme.codeFontFamily
font.pixelSize: Theme.fontMd
font.bold: true
}
}
}
}
Item { height: 12 }
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder }
Item { height: 12 }
Rectangle {
Layout.fillWidth: true
implicitHeight: displayCard.implicitHeight + 24
color: Theme.sidePanelBackground
radius: Theme.sidePanelRadius
border.color: Theme.sideBorder
border.width: 1
ColumnLayout {
id: displayCard
anchors {
left: parent.left
right: parent.right
top: parent.top
margins: 12
}
spacing: 8
// ── DISPLAY ───────────────────────────────────────────────────────────
Label {
text: "DISPLAY"
color: Theme.bodyColor
font.pixelSize: 10
font.letterSpacing: 1.8
font.weight: Font.Medium
topPadding: 6
bottomPadding: 8
color: Theme.sideMutedText
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontXs
font.letterSpacing: 1.5
font.bold: true
Layout.bottomMargin: 4
}
PanelCheckBox {
id: thicknessToggle
text: "Show Thickness"
checked: false
font.pixelSize: 11
font.pixelSize: Theme.fontXs
onCheckedChanged: waferMapItem.showThickness = checked
}
@@ -187,61 +340,104 @@ ColumnLayout {
id: labelsToggle
text: "Labels"
checked: true
font.pixelSize: 11
font.pixelSize: Theme.fontXs
}
PanelCheckBox {
id: clusterAverageToggle
text: "Average Clusters"
checked: streamController.clusterAveragingEnabled
font.pixelSize: 11
font.pixelSize: Theme.fontXs
onCheckedChanged: streamController.clusterAveragingEnabled = checked
}
Rectangle {
Layout.fillWidth: true
height: 1
color: Theme.cardBorder
}
RowLayout {
Layout.fillWidth: true
spacing: 6
Label {
text: "Heatmap"
color: Theme.bodyColor
font.pixelSize: 11
font.pixelSize: Theme.fontXs
Layout.preferredWidth: 52
}
Slider {
id: heatmapSlider
from: 0; to: 1; value: 0
from: 0
to: 1
value: 0
Layout.fillWidth: true
ToolTip.visible: hovered
ToolTip.text: Math.round(value * 100) + "%"
handle: Rectangle {
x: heatmapSlider.leftPadding + heatmapSlider.visualPosition * (heatmapSlider.availableWidth - width)
y: heatmapSlider.topPadding + heatmapSlider.availableHeight / 2 - height / 2
implicitWidth: 18
implicitHeight: 18
radius: 9
color: Theme.primaryAccent
scale: heatmapSlider.pressed ? 1.15 : 1.0
Behavior on scale {
NumberAnimation { duration: Theme.durationFast; easing.type: Theme.easeStandard }
}
}
}
Label {
text: Math.round(heatmapSlider.value * 100) + "%"
color: Theme.bodyColor
font.pixelSize: Theme.fontXs
font.weight: Font.Medium
Layout.preferredWidth: 32
horizontalAlignment: Text.AlignRight
}
}
}
}
Item { height: 12 }
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder }
Item { height: 12 }
Rectangle {
Layout.fillWidth: true
implicitHeight: thresholdCard.implicitHeight + 24
color: Theme.sidePanelBackground
radius: Theme.sidePanelRadius
border.color: Theme.sideBorder
border.width: 1
ColumnLayout {
id: thresholdCard
anchors {
left: parent.left
right: parent.right
top: parent.top
margins: 12
}
spacing: 6
// ── THRESHOLDS ────────────────────────────────────────────────────────
Label {
text: "THRESHOLDS"
color: Theme.bodyColor
font.pixelSize: 10
font.letterSpacing: 1.8
font.weight: Font.Medium
topPadding: 6
bottomPadding: 8
color: Theme.sideMutedText
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontXs
font.letterSpacing: 1.5
font.bold: true
Layout.bottomMargin: 4
}
Label {
text: "Set Point (°C)"
color: Theme.bodyColor
font.pixelSize: 11
bottomPadding: 3
font.pixelSize: Theme.fontXs
}
TextField {
id: spField
Layout.fillWidth: true
text: "149.0"
font.pixelSize: 12
font.pixelSize: Theme.fontSm
inputMethodHints: Qt.ImhFormattedNumbersOnly
color: Theme.fieldText
placeholderTextColor: Theme.fieldPlaceholder
@@ -252,23 +448,26 @@ ColumnLayout {
color: Theme.fieldBackground
border.width: spField.activeFocus ? Theme.borderStrong : Theme.borderThin
border.color: spField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
Behavior on border.color {
ColorAnimation { duration: Theme.durationFast }
}
}
onEditingFinished: pushThresholds()
}
Item { height: 10 }
Label {
text: "Margin (±°C)"
color: Theme.bodyColor
font.pixelSize: 11
bottomPadding: 3
font.pixelSize: Theme.fontXs
}
TextField {
id: mgField
Layout.fillWidth: true
text: "1.0"
font.pixelSize: 12
font.pixelSize: Theme.fontSm
inputMethodHints: Qt.ImhFormattedNumbersOnly
color: Theme.fieldText
placeholderTextColor: Theme.fieldPlaceholder
@@ -279,25 +478,33 @@ ColumnLayout {
color: Theme.fieldBackground
border.width: mgField.activeFocus ? Theme.borderStrong : Theme.borderThin
border.color: mgField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
Behavior on border.color {
ColorAnimation { duration: Theme.durationFast }
}
}
onEditingFinished: pushThresholds()
}
Item { height: 10 }
PanelCheckBox {
id: autoCheck
text: "Auto range (mean ± 1σ)"
checked: true
font.pixelSize: 11
font.pixelSize: Theme.fontXs
onCheckedChanged: pushThresholds()
}
}
}
Item { Layout.fillHeight: true }
Item {
Layout.fillHeight: true
}
function pushThresholds() {
var sp = parseFloat(spField.text) || 149.0
var mg = parseFloat(mgField.text) || 1.0
streamController.setThresholds(sp, mg, autoCheck.checked)
var sp = parseFloat(spField.text) || 149.0;
var mg = parseFloat(mgField.text) || 1.0;
streamController.setThresholds(sp, mg, autoCheck.checked);
}
}
@@ -17,14 +17,15 @@ Dialog {
property bool hasOverride: streamController.overriddenSensors.indexOf(sensorIndex) >= 0
function openFor(index) {
sensorIndex = index
var dot = streamController.sensorDots[index]
if (!dot) return
sensorLabel = dot.label !== undefined ? String(dot.label) : String(index + 1)
currentValue = dot.value
valueField.text = ""
offsetField.text = ""
open()
sensorIndex = index;
var dot = streamController.sensorDots[index];
if (!dot)
return;
sensorLabel = dot.label !== undefined ? String(dot.label) : String(index + 1);
currentValue = dot.value;
valueField.text = "";
offsetField.text = "";
open();
}
background: Rectangle {
@@ -42,11 +43,10 @@ Dialog {
RowLayout {
Layout.fillWidth: true
Label {
text: "Sensor #" + (root.sensorIndex + 1) +
(root.sensorLabel ? " (" + root.sensorLabel + ")" : "")
text: "Sensor #" + (root.sensorIndex + 1) + (root.sensorLabel ? " (" + root.sensorLabel + ")" : "")
color: Theme.headingColor
font.bold: true
font.pixelSize: 13
font.pixelSize: Theme.fontSm
Layout.fillWidth: true
}
Rectangle {
@@ -61,7 +61,7 @@ Dialog {
anchors.centerIn: parent
text: "OVERRIDE"
color: "white"
font.pixelSize: 10
font.pixelSize: Theme.fontXs
font.bold: true
font.letterSpacing: 1
}
@@ -71,13 +71,21 @@ Dialog {
Label {
text: "Live: " + root.currentValue.toFixed(2)
color: Theme.bodyColor
font.pixelSize: 12
font.pixelSize: Theme.fontSm
}
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder }
Rectangle {
Layout.fillWidth: true
height: 1
color: Theme.cardBorder
}
// ── replace field ─────────────────────────────────────────
Label { text: "Replace with value"; color: Theme.bodyColor; font.pixelSize: 11 }
Label {
text: "Replace with value"
color: Theme.bodyColor
font.pixelSize: Theme.fontXs
}
TextField {
id: valueField
Layout.fillWidth: true
@@ -92,11 +100,21 @@ Dialog {
color: Theme.fieldBackground
border.width: valueField.activeFocus ? Theme.borderStrong : Theme.borderThin
border.color: valueField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
Behavior on border.color {
ColorAnimation { duration: Theme.durationFast }
}
}
}
// ── offset field ──────────────────────────────────────────
Label { text: "Or add offset (± delta)"; color: Theme.bodyColor; font.pixelSize: 11 }
Label {
text: "Or add offset (± delta)"
color: Theme.bodyColor
font.pixelSize: Theme.fontXs
}
TextField {
id: offsetField
Layout.fillWidth: true
@@ -111,10 +129,20 @@ Dialog {
color: Theme.fieldBackground
border.width: offsetField.activeFocus ? Theme.borderStrong : Theme.borderThin
border.color: offsetField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
Behavior on border.color {
ColorAnimation { duration: Theme.durationFast }
}
}
}
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder }
Rectangle {
Layout.fillWidth: true
height: 1
color: Theme.cardBorder
}
// ── buttons ───────────────────────────────────────────────
RowLayout {
@@ -140,20 +168,20 @@ Dialog {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.bold: true
font.pixelSize: 13
font.pixelSize: Theme.fontSm
}
onClicked: {
if (valueField.text !== "") {
var v = parseFloat(valueField.text)
var v = parseFloat(valueField.text);
if (!isNaN(v))
streamController.replaceSensor(root.sensorIndex, v)
streamController.replaceSensor(root.sensorIndex, v);
}
if (offsetField.text !== "") {
var d = parseFloat(offsetField.text)
var d = parseFloat(offsetField.text);
if (!isNaN(d))
streamController.offsetSensor(root.sensorIndex, d)
streamController.offsetSensor(root.sensorIndex, d);
}
root.close()
root.close();
}
}
Button {
@@ -175,11 +203,11 @@ Dialog {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.bold: true
font.pixelSize: 13
font.pixelSize: Theme.fontSm
}
onClicked: {
streamController.clearSensorEdit(root.sensorIndex)
root.close()
streamController.clearSensorEdit(root.sensorIndex);
root.close();
}
}
Button {
@@ -198,7 +226,7 @@ Dialog {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.bold: true
font.pixelSize: 13
font.pixelSize: Theme.fontSm
}
onClicked: root.close()
}
+121 -77
View File
@@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Controls.impl
import ISC
import ".."
@@ -14,10 +15,11 @@ ColumnLayout {
Label {
text: "SOURCE"
color: Theme.bodyColor
font.pixelSize: 10
font.letterSpacing: 1.8
font.weight: Font.Medium
color: Theme.sideMutedText
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontSm
font.letterSpacing: 1.5
font.bold: true
topPadding: 6
bottomPadding: 8
Layout.fillWidth: true
@@ -26,51 +28,79 @@ ColumnLayout {
Button {
id: editBtn
text: "✎"
implicitWidth: 26
implicitHeight: 26
implicitWidth: 28
implicitHeight: 28
hoverEnabled: true
font.pixelSize: 14
background: Rectangle {
color: editBtn.pressed ? Theme.buttonPressed
: editBtn.hovered ? Theme.buttonNeutralHover
: "transparent"
color: editBtn.pressed ? Theme.buttonPressed : editBtn.hovered ? Theme.buttonNeutralHover : "transparent"
radius: Theme.radiusSm
}
contentItem: Text {
text: parent.text
contentItem: IconImage {
source: "../icons/edit.svg"
sourceSize.width: 16
sourceSize.height: 16
color: Theme.bodyColor
font: parent.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: {
file_browser.refreshFiles()
csvEditorDialog.open()
file_browser.refreshFiles();
csvEditorDialog.open();
}
ToolTip {
visible: editBtn.hovered
text: "Edit"
delay: 400
font.pixelSize: Theme.fontXs
font.family: Theme.uiFontFamily
contentItem: Text {
text: parent.text
color: Theme.headingColor
font: parent.font
}
background: Rectangle {
color: Theme.cardBackground
border.color: Theme.sideBorder
border.width: 1
radius: Theme.radiusXs
}
}
}
Button {
id: refreshBtn
text: "⟳"
implicitWidth: 26
implicitHeight: 26
implicitWidth: 28
implicitHeight: 28
hoverEnabled: true
font.pixelSize: 14
background: Rectangle {
color: refreshBtn.pressed ? Theme.buttonPressed
: refreshBtn.hovered ? Theme.buttonNeutralHover
: "transparent"
color: refreshBtn.pressed ? Theme.buttonPressed : refreshBtn.hovered ? Theme.buttonNeutralHover : "transparent"
radius: Theme.radiusSm
}
contentItem: Text {
text: parent.text
contentItem: IconImage {
source: "../icons/refresh.svg"
sourceSize.width: 16
sourceSize.height: 16
color: Theme.bodyColor
font: parent.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: file_browser.refreshFiles()
ToolTip {
visible: refreshBtn.hovered
text: "Refresh"
delay: 400
font.pixelSize: Theme.fontXs
font.family: Theme.uiFontFamily
contentItem: Text {
text: parent.text
color: Theme.headingColor
font: parent.font
}
background: Rectangle {
color: Theme.cardBackground
border.color: Theme.sideBorder
border.width: 1
radius: Theme.radiusXs
}
}
}
}
@@ -79,14 +109,12 @@ ColumnLayout {
id: dirBtn
Layout.fillWidth: true
hoverEnabled: true
implicitHeight: 32
implicitHeight: 36
leftPadding: 8
rightPadding: 8
background: Rectangle {
radius: Theme.radiusSm
color: dirBtn.pressed ? Theme.buttonNeutralPressed
: dirBtn.hovered ? Theme.buttonNeutralHover
: Theme.buttonNeutralBackground
color: dirBtn.pressed ? Theme.buttonNeutralPressed : dirBtn.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground
border.width: Theme.borderThin
border.color: Theme.fieldBorder
}
@@ -95,12 +123,12 @@ ColumnLayout {
Label {
text: "▤"
color: Theme.bodyColor
font.pixelSize: 12
font.pixelSize: Theme.fontSm
}
Label {
text: file_browser.currentDirectory.split("/").pop() || file_browser.currentDirectory
color: Theme.headingColor
font.pixelSize: 11
font.pixelSize: Theme.fontSm
elide: Text.ElideMiddle
Layout.fillWidth: true
}
@@ -113,7 +141,7 @@ ColumnLayout {
id: filter
Layout.fillWidth: true
placeholderText: "Filter…"
font.pixelSize: 11
font.pixelSize: Theme.fontSm
color: Theme.fieldText
placeholderTextColor: Theme.fieldPlaceholder
selectedTextColor: Theme.fieldBackground
@@ -123,6 +151,12 @@ ColumnLayout {
color: Theme.fieldBackground
border.width: filter.activeFocus ? Theme.borderStrong : Theme.borderThin
border.color: filter.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
Behavior on border.color {
ColorAnimation { duration: Theme.durationFast }
}
}
}
@@ -146,17 +180,14 @@ ColumnLayout {
focusPolicy: Qt.NoFocus
readonly property bool matchesFilter: {
if (filter.text === "") return true
var q = filter.text.toLowerCase()
return (modelData.baseName + modelData.waferType + modelData.date)
.toLowerCase().indexOf(q) >= 0
if (filter.text === "")
return true;
var q = filter.text.toLowerCase();
return (modelData.baseName + modelData.waferType + modelData.date).toLowerCase().indexOf(q) >= 0;
}
// Active only in review mode; no highlight during live streaming
readonly property bool isActive:
streamController.mode === "review" &&
streamController.loadedFile !== "" &&
modelData.fileName === streamController.loadedFile
readonly property bool isActive: streamController.mode === "review" && streamController.loadedFile !== "" && modelData.fileName === streamController.loadedFile
visible: matchesFilter
height: matchesFilter ? implicitHeight : 0
@@ -164,10 +195,11 @@ ColumnLayout {
onClicked: streamController.loadFile(modelData.fileName)
background: Rectangle {
color: fileItem.isActive
? Qt.rgba(1, 1, 1, 0.06)
: fileItem.hovered ? Qt.rgba(1, 1, 1, 0.04) : "transparent"
color: fileItem.isActive ? Qt.rgba(1, 1, 1, 0.06) : fileItem.hovered ? Qt.rgba(1, 1, 1, 0.04) : "transparent"
radius: Theme.radiusSm
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
// Left accent bar — shown only when active
Rectangle {
@@ -175,15 +207,21 @@ ColumnLayout {
width: 3
radius: 1.5
anchors {
top: parent.top; bottom: parent.bottom; left: parent.left
topMargin: 5; bottomMargin: 5
top: parent.top
bottom: parent.bottom
left: parent.left
topMargin: 5
bottomMargin: 5
}
color: {
var t = modelData.waferType
if (t === "A" || t === "E" || t === "P") return "#3B82F6"
if (t === "B" || t === "C" || t === "D") return "#10B981"
if (t === "Z") return "#8B5CF6"
return Theme.headingColor
var t = modelData.waferType;
if (t === "A" || t === "E" || t === "P")
return "#3B82F6";
if (t === "B" || t === "C" || t === "D")
return "#10B981";
if (t === "Z")
return "#8B5CF6";
return Theme.headingColor;
}
}
}
@@ -193,22 +231,26 @@ ColumnLayout {
// Wafer-type avatar (circle)
Rectangle {
implicitWidth: 28; implicitHeight: 28
implicitWidth: 28
implicitHeight: 28
Layout.preferredWidth: 28
Layout.preferredHeight: 28
Layout.alignment: Qt.AlignVCenter
radius: 14
color: {
var t = modelData.waferType
if (t === "A" || t === "E" || t === "P") return "#1D4ED8"
if (t === "B" || t === "C" || t === "D") return "#065F46"
if (t === "Z") return "#7C3AED"
return "#374151"
var t = modelData.waferType;
if (t === "A" || t === "E" || t === "P")
return "#1D4ED8";
if (t === "B" || t === "C" || t === "D")
return "#065F46";
if (t === "Z")
return "#7C3AED";
return "#374151";
}
Label {
anchors.centerIn: parent
text: modelData.waferType || "?"
font.pixelSize: 12
font.pixelSize: Theme.fontMd
font.weight: Font.Bold
color: "#FFFFFF"
}
@@ -223,7 +265,7 @@ ColumnLayout {
Label {
text: modelData.serialNumber ? (modelData.waferType + modelData.serialNumber) : modelData.baseName
color: Theme.headingColor
font.pixelSize: 13
font.pixelSize: Theme.fontMd
font.weight: Font.Bold
elide: Text.ElideRight
Layout.fillWidth: true
@@ -235,24 +277,24 @@ ColumnLayout {
Layout.fillWidth: true
Label {
text: {
var d = modelData.date || ""
return d.length >= 10 ? d.substring(0, 10) : (d || "—")
var d = modelData.date || "";
return d.length >= 10 ? d.substring(0, 10) : (d || "—");
}
color: Theme.bodyColor
font.pixelSize: 10
font.pixelSize: Theme.fontSm
}
Label {
visible: (modelData.timeStr || "") !== ""
text: "·"
color: Theme.bodyColor
font.pixelSize: 10
font.pixelSize: Theme.fontSm
opacity: 0.45
}
Label {
visible: (modelData.timeStr || "") !== ""
text: modelData.timeStr || ""
color: Theme.bodyColor
font.pixelSize: 10
font.pixelSize: Theme.fontSm
opacity: 0.65
}
}
@@ -260,18 +302,20 @@ ColumnLayout {
// Tertiary: edited metadata (chamber · notes) if any
Label {
visible: {
var c = modelData.chamber || ""
var n = modelData.notes || ""
return c !== "" || n !== ""
var c = modelData.chamber || "";
var n = modelData.notes || "";
return c !== "" || n !== "";
}
text: {
var parts = []
if (modelData.chamber) parts.push(modelData.chamber)
if (modelData.notes) parts.push(modelData.notes)
return parts.join(" · ")
var parts = [];
if (modelData.chamber)
parts.push(modelData.chamber);
if (modelData.notes)
parts.push(modelData.notes);
return parts.join(" · ");
}
color: Theme.bodyColor
font.pixelSize: 9
font.pixelSize: Theme.fontXs
opacity: 0.7
elide: Text.ElideRight
Layout.fillWidth: true
@@ -285,7 +329,7 @@ ColumnLayout {
Label {
text: file_browser.files.length + " files · CSV only"
color: Theme.bodyColor
font.pixelSize: 10
font.pixelSize: Theme.fontSm
}
SelectFileDialog {
+296 -60
View File
@@ -3,101 +3,337 @@ import QtQuick.Controls
import QtQuick.Layouts
import ISC
RowLayout {
spacing: 2
// ===== Control Bar =====
// Mode-aware footer: cross-fades between Live and Review layouts.
// Both variants render as a single centered pill — no full-width bar.
Item {
id: bar
Layout.fillWidth: true
implicitHeight: hasContent ? 80 : 0
Behavior on implicitHeight {
NumberAnimation { duration: Theme.durationBase; easing.type: Theme.easeStandard }
}
clip: true
component TBtn: Button {
implicitWidth: 44
implicitHeight: 32
hoverEnabled: true
font.pixelSize: 13
background: Rectangle {
color: parent.pressed ? Theme.transportButtonHover
: parent.hovered ? Theme.transportButtonBg
: Theme.transportBackground
radius: Theme.radiusSm
}
contentItem: Text {
text: parent.text
color: Theme.headingColor
font: parent.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
// Hide entirely when in review mode with no file loaded
readonly property bool hasContent: {
if (streamController.mode === "live") return true;
return streamController.loadedFile !== "";
}
readonly property bool isLive: streamController.mode === "live"
// ── LIVE MODE ─────────────────────────────────────────────────
Item {
id: liveContent
anchors.fill: parent
opacity: bar.isLive ? 1.0 : 0.0
visible: opacity > 0
Behavior on opacity {
NumberAnimation { duration: Theme.durationBase; easing.type: Theme.easeStandard }
}
// Frame counter badge on the left
// Single centered pill for Received + Errors + Stop
Rectangle {
id: frameCounter
implicitWidth: frameCounterText.implicitWidth + 16
implicitHeight: 32
color: Theme.fieldBackground
border.color: Theme.cardBorder
border.width: Theme.borderThin
radius: Theme.radiusSm
Text {
id: frameCounterText
anchors.centerIn: parent
text: "Frame " + (streamController.frameIndex + 1) + " / " + streamController.frameTotal
color: Theme.headingColor
font.pixelSize: 11
implicitWidth: liveRow.implicitWidth + 48
implicitHeight: 68
radius: Theme.radiusMd
color: Theme.sidePanelBackground
border.color: Theme.sideBorder
border.width: 1
Row {
id: liveRow
anchors.centerIn: parent
spacing: 24
// Received counter
Text {
anchors.verticalCenter: parent.verticalCenter
text: "Received: " + streamController.receivedCount
color: Theme.sideMutedText
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontMd
font.weight: Font.Medium
}
// Separator
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 1
height: 36
color: Theme.sideBorder
}
// Left fill — centers the button cluster
Item { Layout.fillWidth: true }
// Errors counter
Text {
anchors.verticalCenter: parent.verticalCenter
text: "Errors: " + streamController.errorCount
color: streamController.errorCount > 0
? Theme.statusWarningColor : Theme.sideMutedText
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontMd
font.weight: Font.Medium
}
TBtn { text: "⏮"; onClicked: streamController.step(-1) }
TBtn { text: "⏹"; onClicked: streamController.stop() }
TBtn { text: "⏸"; onClicked: streamController.pause() }
// Play: slightly wider; leftPadding nudge compensates for ▶ optical offset
TBtn {
text: "▶"
implicitWidth: 52
// Separator
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 1
height: 36
color: Theme.sideBorder
}
// STOP button
Button {
id: stopBtn
anchors.verticalCenter: parent.verticalCenter
implicitWidth: 112
implicitHeight: 48
hoverEnabled: true
text: "STOP"
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontSm
font.weight: Font.Medium
font.letterSpacing: 1.0
background: Rectangle {
radius: 8
color: stopBtn.pressed ? Theme.transportButtonHover
: Theme.transportButtonBg
border.color: Theme.sideBorder
border.width: 1
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
}
contentItem: Text {
text: parent.text
color: Theme.headingColor
font: parent.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
leftPadding: 2
}
onClicked: streamController.play()
onClicked: streamController.stopStream()
}
}
}
}
TBtn { text: "⏭"; onClicked: streamController.step(1) }
// Right fill — balances centering
Item { Layout.fillWidth: true }
// ── REVIEW MODE ──────────────────────────────────────────────
Item {
id: reviewContent
anchors.fill: parent
opacity: !bar.isLive ? 1.0 : 0.0
visible: opacity > 0
Behavior on opacity {
NumberAnimation { duration: Theme.durationBase; easing.type: Theme.easeStandard }
}
// Speed cycle
// Single centered pill for frame counter + transport + speed
Rectangle {
anchors.centerIn: parent
implicitWidth: reviewRow.implicitWidth + 48
implicitHeight: 68
radius: Theme.radiusMd
color: Theme.sidePanelBackground
border.color: Theme.sideBorder
border.width: 1
Row {
id: reviewRow
anchors.centerIn: parent
spacing: 24
// Frame counter
Text {
anchors.verticalCenter: parent.verticalCenter
text: "Frame " + (streamController.frameIndex + 1)
+ " / " + streamController.frameTotal
color: Theme.sideMutedText
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontMd
font.weight: Font.Medium
leftPadding: 8
}
// Separator 1
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 1
height: 36
color: Theme.sideBorder
}
// Media Buttons Row
Row {
anchors.verticalCenter: parent.verticalCenter
spacing: 12
// Skip-start
Button {
id: skipStartBtn
anchors.verticalCenter: parent.verticalCenter
implicitWidth: 56
implicitHeight: 56
hoverEnabled: true
text: "⏮"
font.pixelSize: 24
background: Rectangle {
radius: 8
color: skipStartBtn.pressed ? Theme.transportButtonHover
: skipStartBtn.hovered ? Theme.transportButtonBg
: "transparent"
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
}
contentItem: Text {
text: parent.text
color: Theme.headingColor
font: parent.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: streamController.step(-1)
}
// Stop
Button {
id: reviewStopBtn
anchors.verticalCenter: parent.verticalCenter
implicitWidth: 56
implicitHeight: 56
hoverEnabled: true
text: "■"
font.pixelSize: 22
background: Rectangle {
radius: 8
color: reviewStopBtn.pressed ? Theme.transportButtonHover
: reviewStopBtn.hovered ? Theme.transportButtonBg
: "transparent"
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
}
contentItem: Text {
text: parent.text
color: Theme.headingColor
font: parent.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: streamController.stop()
}
// Play / Pause toggle
Button {
id: playPauseBtn
anchors.verticalCenter: parent.verticalCenter
implicitWidth: 64
implicitHeight: 56
hoverEnabled: true
text: streamController.playing ? "⏸" : "▶"
font.pixelSize: 24
background: Rectangle {
radius: 8
color: playPauseBtn.pressed ? Theme.transportButtonHover
: playPauseBtn.hovered ? Theme.transportButtonBg
: "transparent"
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
}
contentItem: Text {
text: parent.text
color: Theme.headingColor
font: parent.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
leftPadding: parent.text === "▶" ? 4 : 0
}
onClicked: {
if (streamController.playing) {
streamController.pause();
} else {
streamController.play();
}
}
}
// Skip-end
Button {
id: skipEndBtn
anchors.verticalCenter: parent.verticalCenter
implicitWidth: 56
implicitHeight: 56
hoverEnabled: true
text: "⏭"
font.pixelSize: 24
background: Rectangle {
radius: 8
color: skipEndBtn.pressed ? Theme.transportButtonHover
: skipEndBtn.hovered ? Theme.transportButtonBg
: "transparent"
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
}
contentItem: Text {
text: parent.text
color: Theme.headingColor
font: parent.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: streamController.step(1)
}
}
// Separator 2
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 1
height: 36
color: Theme.sideBorder
}
// Speed capsule button
Button {
id: speedBtn
property var speeds: [1, 2, 5]
anchors.verticalCenter: parent.verticalCenter
property var speeds: [1, 2, 4]
property int idx: 0
implicitWidth: 44
implicitHeight: 32
implicitWidth: 72
implicitHeight: 48
hoverEnabled: true
text: speeds[idx] + "×"
font.pixelSize: 11
text: speeds[idx] + "x"
font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontSm
font.weight: Font.Medium
background: Rectangle {
color: parent.pressed ? Theme.transportButtonHover
: parent.hovered ? Theme.transportButtonBg
: Theme.transportBackground
radius: Theme.radiusSm
radius: 8
color: speedBtn.pressed ? Theme.transportButtonHover
: Theme.transportButtonBg
border.color: Theme.sideBorder
border.width: 1
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
}
contentItem: Text {
text: parent.text
color: Theme.bodyColor
color: Theme.headingColor
font: parent.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: {
idx = (idx + 1) % speeds.length
streamController.setSpeed(speeds[idx])
idx = (idx + 1) % speeds.length;
streamController.setSpeed(speeds[idx]);
}
}
}
}
}
}
+8 -10
View File
@@ -3,7 +3,6 @@ import QtQuick.Controls
import ISC
import ISC.Wafer
Item {
id: root
property real blend: 0.0
@@ -29,21 +28,20 @@ Item {
highColor: Theme.sensorHigh
textColor: Theme.headingColor
TapHandler {
onTapped: (ev) => {
var idx = map.which_marker(ev.position.x, ev.position.y)
if (idx >= 0) replaceDialog.openFor(idx)
onTapped: ev => {
var idx = map.which_marker(ev.position.x, ev.position.y);
if (idx >= 0)
replaceDialog.openFor(idx);
}
}
}
ReplaceSensorDialog { id: replaceDialog}
ReplaceSensorDialog {
id: replaceDialog
}
function exportImage(filePath) {
return map.export_image(filePath)
return map.export_image(filePath);
}
}
+1
View File
@@ -5,3 +5,4 @@ ReadoutPanel 1.0 ReadoutPanel.qml
TransportBar 1.0 TransportBar.qml
WaferMapView 1.0 WaferMapView.qml
ReplaceSensorDialog 1.0 ReplaceSensorDialog.qml
RailActionButton 1.0 RailActionButton.qml
-4
View File
@@ -1,4 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.3 KiB

+15
View File
@@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<path d="M12 16v-4" />
<path d="M12 8h.01" />
</svg>

After

Width:  |  Height:  |  Size: 294 B

+16
View File
@@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M8 3 4 7l4 4" />
<path d="M4 7h16" />
<path d="m16 21 4-4-4-4" />
<path d="M20 17H4" />
</svg>

After

Width:  |  Height:  |  Size: 313 B

+15
View File
@@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<ellipse cx="12" cy="5" rx="9" ry="3" />
<path d="M3 5V19A9 3 0 0 0 21 19V5" />
<path d="M3 12A9 3 0 0 0 21 12" />
</svg>

After

Width:  |  Height:  |  Size: 329 B

+14
View File
@@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m21 21-4.34-4.34" />
<circle cx="11" cy="11" r="8" />
</svg>

After

Width:  |  Height:  |  Size: 275 B

+14
View File
@@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M17 3a2.85 2.85 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
<path d="m15 5 4 4" />
</svg>

After

Width:  |  Height:  |  Size: 297 B

+17
View File
@@ -0,0 +1,17 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M10 11v6" />
<path d="M14 11v6" />
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" />
<path d="M3 6h18" />
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
</svg>

After

Width:  |  Height:  |  Size: 389 B

+5
View File
@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-1.5, 0)">
<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="currentColor"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+17
View File
@@ -0,0 +1,17 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M3 9h18" />
<path d="M3 15h18" />
<path d="M9 3v18" />
<path d="M15 3v18" />
</svg>

After

Width:  |  Height:  |  Size: 355 B

+14
View File
@@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" />
<circle cx="12" cy="12" r="3" />
</svg>

After

Width:  |  Height:  |  Size: 360 B

+16
View File
@@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
<path d="M3 21v-5h5" />
</svg>

After

Width:  |  Height:  |  Size: 393 B

+14
View File
@@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" />
<circle cx="12" cy="12" r="3" />
</svg>

After

Width:  |  Height:  |  Size: 586 B

+14
View File
@@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M12 3v18" />
</svg>

After

Width:  |  Height:  |  Size: 285 B

+14
View File
@@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<path d="M12 6v6l4 2" />
</svg>

After

Width:  |  Height:  |  Size: 271 B

+105 -53
View File
@@ -7,49 +7,89 @@ import QtQuick
// =============================================================================
// Sections:
// 1. Mode flag
// 2. Tone palette (raw tones; everything else derives from these)
// 3. Surfaces (page / card / panel / workspace backgrounds)
// 4. Borders
// 5. Text (headings, body, panel titles)
// 6. Controls (fields, buttons, checkbox)
// 7. Tracks (pill toggle + scrollbar)
// 8. Tabs (footer tab bar)
// 9. Side rail
// 10. Status (success / warning / error)
// 11. Geometry (radius / border width / spacing / sizing)
// 2. Typography
// 3. Motion
// 4. Tone palette (raw tones; everything else derives from these)
// 5. Surfaces (page / card / panel / workspace backgrounds)
// 6. Borders
// 7. Text (headings, body, panel titles)
// 8. Controls (fields, buttons, checkbox)
// 9. Tracks (pill toggle + scrollbar)
// 10. Tabs (footer tab bar)
// 11. Side rail
// 12. Status (success / warning / error)
// 13. Geometry (radius / border width / spacing / sizing)
// =============================================================================
// Based on Codex themes:
// Dark (oscurange): surface #0B0B0F, ink #E6E6E6
// Light (absolutely): surface #F9F9F7, ink #2D2D2B
// Accent (both): #F9B98C
QtObject {
// ── 1. Mode ──────────────────────────────────────────────────────────────
property bool isDarkMode: true
// ── 2. Tone palette (base values; consume via semantic tokens below) ─────
readonly property color tone100: isDarkMode ? "#111111" : "#FAFAFA"
readonly property color tone150: isDarkMode ? "#161616" : "#F6F6F6"
readonly property color tone200: isDarkMode ? "#1A1A1A" : "#F0F0F0"
readonly property color tone250: isDarkMode ? "#212121" : "#EAEAEA"
readonly property color tone300: isDarkMode ? "#282828" : "#E2E2E2"
readonly property color tone350: isDarkMode ? "#303030" : "#D6D6D6"
readonly property color toneText: isDarkMode ? "#F2F2F2" : "#111111"
readonly property color toneMute: isDarkMode ? "#A8A8A8" : "#8A8A8A"
readonly property color toneBorder: isDarkMode ? "#2E2E2E" : "#D4D4D4"
// ── 2. Typography ──────────────────────────────────────────────────────
// pixelSize scale — map to closest token, tune globally
readonly property int font2xs: 9 // tiny metadata
readonly property int fontXs: 11 // captions, micro labels
readonly property int fontSm: 13 // sidebar labels, secondary
readonly property int fontMd: 14 // body, field values
readonly property int fontLg: 16 // section headers
readonly property int fontXl: 20 // panel / dialog titles
readonly property int font2xl: 24 // hero titles
readonly property int font3xl: 30 // display
readonly property int fontWeightRegular: 400
readonly property int fontWeightMedium: 500
readonly property int fontWeightBold: 700
// Legacy shim
readonly property int tabFontSize: fontSm
// ── 3. Motion ──────────────────────────────────────────────────────────
readonly property int durationFast: 120 // micro-interactions (focus ring, hover flash)
readonly property int durationBase: 180 // standard transitions (pill switch, row select)
readonly property int durationSlow: 260 // larger transitions (panel open/close)
readonly property var easeStandard: Easing.OutCubic
// ── 4. Tone palette (Codex theme v1 inspired) ───────────────────────────
readonly property string uiFontFamily: "Geist, Inter, system-ui, -apple-system, BlinkMacSystemFont, sans-serif"
readonly property string codeFontFamily: "\"Geist Mono\", ui-monospace, \"SFMono-Regular\", monospace"
readonly property color themeSurface: isDarkMode ? "#0B0B0F" : "#F9F9F7"
readonly property color themeInk: isDarkMode ? "#E6E6E6" : "#2D2D2B"
readonly property color themeAccent: isDarkMode ? "#F9B98C" : "#B87333"
readonly property color themeSkill: isDarkMode ? "#479FFA" : "#3B82F6" // blue family both modes
readonly property color themeAdded: isDarkMode ? "#40C977" : "#00C853"
readonly property color themeRemoved: isDarkMode ? "#FA423E" : "#FF5F38"
readonly property color tone100: themeSurface
readonly property color tone150: isDarkMode ? "#101014" : "#F3F3F1"
readonly property color tone200: isDarkMode ? "#151518" : "#EEEEEC"
readonly property color tone250: isDarkMode ? "#1B1B1F" : "#E8E8E6"
readonly property color tone300: isDarkMode ? "#222228" : "#E0E0DE"
readonly property color tone350: isDarkMode ? "#2B2B32" : "#D7D7D4"
readonly property color toneText: themeInk
readonly property color toneMute: isDarkMode ? "#A8A8A1" : "#85857E"
readonly property color toneBorder: isDarkMode ? "#2D2D34" : "#D2D2CE"
// ── 3. Surfaces ──────────────────────────────────────────────────────────
readonly property color pageBackground: tone100
readonly property color cardBackground: tone200
readonly property color panelBackground: tone200
readonly property color workspaceBackground: tone200
readonly property color responseBackground: tone200
readonly property color subtleSectionBackground: tone300
readonly property color cardBackground: tone150
readonly property color panelBackground: tone150
readonly property color workspaceBackground: tone150
readonly property color responseBackground: tone150
readonly property color subtleSectionBackground: tone250
// ── 4. Borders ───────────────────────────────────────────────────────────
readonly property color cardBorder: toneBorder
readonly property color cardSurfaceBorder: isDarkMode ? "#3C3C3C" : "#C8C8C8"
readonly property color cardSurfaceBorder: isDarkMode ? "#3A3A42" : "#C7C7C2"
readonly property color responseBorder: toneBorder
readonly property color workspaceBorder: toneBorder
readonly property color innerFrameBorder: toneBorder
readonly property color outerFrameBorder: isDarkMode ? "#383838" : "#CACACA"
readonly property color softBorder: isDarkMode ? "#252525" : "#E0E0E0"
readonly property color outerFrameBorder: isDarkMode ? "#3A3A42" : "#C7C7C2"
readonly property color softBorder: isDarkMode ? "#24242A" : "#E4E4E1"
// ── 5. Text ──────────────────────────────────────────────────────────────
readonly property color headingColor: toneText
@@ -60,53 +100,61 @@ QtObject {
// ── 6. Controls ──────────────────────────────────────────────────────────
// Fields
readonly property color fieldBackground: tone100
readonly property color fieldBackground: isDarkMode ? "#101014" : "#FFFFFF"
readonly property color fieldText: toneText
readonly property color fieldPlaceholder: toneMute
readonly property color fieldBorder: toneBorder
readonly property color fieldBorderFocus: toneText
readonly property color fieldBorderFocus: themeAccent
// Neutral buttons
readonly property color buttonNeutralBackground: tone300
readonly property color buttonNeutralHover: isDarkMode ? "#303030" : "#DDDDDD"
readonly property color buttonNeutralPressed: tone100
readonly property color buttonNeutralBackground: tone250
readonly property color buttonNeutralHover: tone300
readonly property color buttonNeutralPressed: isDarkMode ? "#0F0F13" : "#FFFFFF"
readonly property color buttonNeutralText: toneText
readonly property color buttonPressed: tone300
readonly property color primaryAccent: toneText
readonly property color primaryAccent: themeAccent
// Checkbox
readonly property color checkboxText: toneText
// ── 7. Tracks (pill toggle, scrollbar thumb) ─────────────────────────────
readonly property color trackBackground: isDarkMode ? "#25252B" : "#D1D5DB"
readonly property color trackBackground: isDarkMode ? "#25252B" : "#E2E2DE"
// ── 8. Tabs (footer tab bar) ─────────────────────────────────────────────
readonly property color tabBarBackground: tone200
readonly property color tabBackground: "transparent"
readonly property color tabActiveBackground: isDarkMode ? "#333333" : "#FFFFFF"
readonly property color tabHoverBackground: tone300
readonly property color tabActiveBackground: isDarkMode ? "#25252B" : "#FFFFFF"
readonly property color tabHoverBackground: tone250
readonly property color tabBorder: "transparent"
readonly property color tabText: toneMute
readonly property color tabActiveText: toneText
// ── 9. Side rail ─────────────────────────────────────────────────────────
readonly property color sideRailBackground: tone200
readonly property color sideActiveBackground: tone300
readonly property color sideRailBackground: tone150
readonly property color sidePanelBackground: isDarkMode ? "#151518" : "#F3F3F1"
readonly property color sideActiveBackground: isDarkMode ? "#25252B" : "#FFFFFF"
readonly property color sideBorder: toneBorder
readonly property color sideMutedText: toneMute
readonly property color sideAccent: themeAccent
readonly property color sideRowHover: tone250
readonly property color sideFieldBackground: isDarkMode ? "#101014" : "#FFFFFF"
// ── 10a. Transport / toolbar surfaces ────────────────────────────────────
readonly property color transportBackground: isDarkMode ? "#0D0D0D" : "#E8E8E8"
readonly property color transportButtonBg: isDarkMode ? "#2A2A2A" : "#D4D4D4"
readonly property color transportButtonHover: isDarkMode ? "#3A3A3A" : "#C4C4C4"
readonly property color liveColor: isDarkMode ? "#22C55E" : "#16A34A"
readonly property color recordColor: isDarkMode ? "#EF4444" : "#DC2626"
readonly property color transportBackground: isDarkMode ? "#101014" : "#EFEFED"
readonly property color transportButtonBg: tone250
readonly property color transportButtonHover: tone300
readonly property color liveColor: themeAdded
readonly property color recordColor: themeRemoved
// ── 10. Status ───────────────────────────────────────────────────────────
readonly property color statusSuccessColor: isDarkMode ? "#63D471" : "#2E9E44"
readonly property color statusWarningColor: isDarkMode ? "#F5C15C" : "#C88A18"
readonly property color statusErrorColor: isDarkMode ? "#FF6B6B" : "#D64545"
readonly property color statusSuccessColor: themeAdded
readonly property color statusWarningColor: isDarkMode ? "#E8A817" : "#B8860B" // amber/yellow, separate from accent
readonly property color statusErrorColor: themeRemoved
// -- 10b. Sensor bands (wafer map dots)
readonly property color sensorInRange: statusSuccessColor
// ponytail: high-temp shares error red — semantically correct (dangerous value),
// re-hue if colorblind testing demands a different channel.
readonly property color sensorHigh: statusErrorColor
readonly property color sensorLow: isDarkMode ? "#5B9DF5" : "#2F6FE0"
readonly property color sensorLow: themeSkill
readonly property color waferRingColor: toneBorder
readonly property color waferAxisColor: softBorder
@@ -124,16 +172,20 @@ QtObject {
readonly property int rightPaneGap: 6
readonly property int panelPadding: 12
// Side rail layout
readonly property int sideRailWidth: 190
readonly property int sideRailSpacing: 14
readonly property int sideButtonHeight: 146
readonly property int sideButtonMinHeight: 88
readonly property int sideRailWidth: 280
readonly property int sideRailMargin: 16
readonly property int sideRailSpacing: 16
readonly property int sideButtonHeight: 44
readonly property int sidePanelRadius: 10
readonly property int sideFooterConnection: 100
readonly property int sideFooterUtility: 46
// Tab bar layout
readonly property int tabBarHeight: 34
readonly property int sidePillHeight: 54
readonly property int sidePillButtonHeight: 36
readonly property int tabBarPadding: 6
readonly property int tabSpacing: 2
readonly property int tabRadius: 8
readonly property int tabFontSize: 12
readonly property int tabButtonMinWidth: 80
// Settings page layout
readonly property int settingsPanelMaxWidth: 760
+1
View File
@@ -4,6 +4,7 @@ module ISC
# ===== Root Components =====
Main 1.0 Main.qml
HomePage 1.0 HomePage.qml
AboutDialog 1.0 AboutDialog.qml
# ===== Singleton Theme =====
singleton Theme 1.0 Theme.qml
@@ -37,10 +37,12 @@ class SessionController(QObject):
loadedFileChanged = Signal()
clusterAveragingEnabledChanged = Signal()
settingsChanged = Signal() # emitted when session state changes that should be persisted
liveStatsChanged = Signal() # emitted when receivedCount or errorCount change
# trend: per-frame avg for live graph
trendData = Signal(str) # JSON array of floats
# private: marshal a worker-thread frame onto the main thread
_liveFrame = Signal(object) # Frame
_liveError = Signal() # worker-thread error ping
def __init__(self, parent: QObject | None = None,
settings: dict | None = None) -> None:
@@ -72,6 +74,7 @@ class SessionController(QObject):
self._dirty = False
self._liveFrame.connect(self._on_live_frame, Qt.ConnectionType.QueuedConnection)
self._liveError.connect(self._on_live_error, Qt.ConnectionType.QueuedConnection)
self._sensor_editor = SensorEditor()
self._last_raw_frame: Frame | None = None
@@ -79,6 +82,8 @@ class SessionController(QObject):
self._cluster_averaging_enabled = False
self._active_clusters: list[list[int]] = []
self._stats_tracker = ReplayStatsTracker()
self._received_count: int = 0
self._error_count: int = 0
# Restore persisted state if available
self._load_settings(settings or {})
@@ -105,6 +110,9 @@ class SessionController(QObject):
@Property(int, notify=frameUpdated)
def frameTotal(self) -> int: return self._player.total
@Property(bool, notify=stateChanged)
def playing(self) -> bool: return self._play_timer.isActive()
@Property(str, notify=stateChanged)
def state(self) -> str:
if self._last:
@@ -210,6 +218,14 @@ class SessionController(QObject):
self.clusterAveragingEnabledChanged.emit()
self._reprocess_current()
@Property(int, notify=liveStatsChanged)
def receivedCount(self) -> int:
return self._received_count
@Property(int, notify=liveStatsChanged)
def errorCount(self) -> int:
return self._error_count
# ---- mode + thresholds ----
@Slot(str)
@slot_error_boundary
@@ -292,11 +308,13 @@ class SessionController(QObject):
@slot_error_boundary
def play(self) -> None:
self._play_timer.start(self._next_interval_ms())
self.stateChanged.emit()
@Slot()
@slot_error_boundary
def pause(self) -> None:
self._play_timer.stop()
self.stateChanged.emit()
@Slot()
@slot_error_boundary
@@ -304,6 +322,7 @@ class SessionController(QObject):
self._play_timer.stop()
self._player.seek(0)
self._emit_current()
self.stateChanged.emit()
@Slot(int)
@slot_error_boundary
@@ -311,6 +330,7 @@ class SessionController(QObject):
self._play_timer.stop()
self._player.step(delta)
self._emit_current()
self.stateChanged.emit()
@Slot(int)
@slot_error_boundary
@@ -335,6 +355,7 @@ class SessionController(QObject):
def _advance(self) -> None:
if self._player.at_end:
self._play_timer.stop()
self.stateChanged.emit()
return
self._player.step(1)
self._emit_current()
@@ -419,6 +440,9 @@ class SessionController(QObject):
self._stats_tracker.reset()
self._last = None
self._last_raw_frame = None
self._received_count = 0
self._error_count = 0
self.liveStatsChanged.emit()
transport = SerialPort.open_port(port, timeout=1)
# Send 'D2' command padded to 512 bytes to start the stream
@@ -427,6 +451,7 @@ class SessionController(QObject):
def on_error(exc: Exception):
log.error("Live stream error: %s", exc)
self._liveError.emit()
self._reader = StreamReader(
transport, parse_binary_frame,
@@ -444,6 +469,9 @@ class SessionController(QObject):
def stopStream(self) -> None:
self._repaint_timer.stop()
self._trend_timer.stop()
self._received_count = 0
self._error_count = 0
self.liveStatsChanged.emit()
if self._reader:
transport = self._reader._transport
self._reader.stop()
@@ -465,6 +493,8 @@ class SessionController(QObject):
@slot_error_boundary
def _on_live_frame(self, frame: Frame) -> None:
import json
self._received_count += 1
self.liveStatsChanged.emit()
self._stats_tracker.track(frame.seq, frame.values)
# Main thread (queued). Record raw, process edited; mark dirty for repaint.
self._last_raw_frame = frame
@@ -498,6 +528,11 @@ class SessionController(QObject):
self.frameUpdated.emit()
self.stateChanged.emit()
def _on_live_error(self) -> None:
"""Main-thread callback for worker-thread stream errors."""
self._error_count += 1
self.liveStatsChanged.emit()
# ---- recording ----
@Slot(str, str)
@slot_error_boundary